{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "# **ICESat-2 Crossover Track Analysis**\n",
    "\n",
    "To increase the temporal resolution of\n",
    "our ice elevation change analysis\n",
    "(i.e. at time periods less than\n",
    "the 91 day repeat cycle of ICESat-2),\n",
    "we can look at the locations where the\n",
    "ICESat-2 tracks intersect and get the\n",
    "height values there!\n",
    "Uses [pygmt.x2sys_cross](https://www.pygmt.org/v0.2.0/api/generated/pygmt.x2sys_cross.html).\n",
    "\n",
    "References:\n",
    "- Wessel, P. (2010). Tools for analyzing intersecting tracks: The x2sys package.\n",
    "Computers & Geosciences, 36(3), 348–354. https://doi.org/10.1016/j.cageo.2009.05.009"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import itertools\n",
    "import os\n",
    "\n",
    "import dask\n",
    "import deepicedrain\n",
    "import geopandas as gpd\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import pint\n",
    "import pint_pandas\n",
    "import pygmt\n",
    "import shapely.geometry\n",
    "import tqdm\n",
    "import uncertainties"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "ureg = pint.UnitRegistry()\n",
    "pint_pandas.PintType.ureg = ureg\n",
    "tag: str = \"X2SYS\"\n",
    "os.environ[\"X2SYS_HOME\"] = os.path.abspath(tag)\n",
    "client = dask.distributed.Client(\n",
    "    n_workers=4, threads_per_worker=1, env={\"X2SYS_HOME\": os.environ[\"X2SYS_HOME\"]}\n",
    ")\n",
    "client"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "min_date, max_date = (\"2019-03-29\", \"2020-12-24\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialize X2SYS database in the X2SYS/ICESAT2 folder\n",
    "pygmt.x2sys_init(\n",
    "    tag=\"ICESAT2\",\n",
    "    fmtfile=f\"{tag}/ICESAT2/xyht\",\n",
    "    suffix=\"tsv\",\n",
    "    units=[\"de\", \"se\"],  # distance in metres, speed in metres per second\n",
    "    gap=\"d250e\",  # distance gap up to 250 metres allowed\n",
    "    force=True,\n",
    "    verbose=\"q\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Select a subglacial lake to examine"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "# Save or load dhdt data from Parquet file\n",
    "placename: str = \"whillans_upstream\"  # \"slessor_downstream\"\n",
    "df_dhdt: pd.DataFrame = pd.read_parquet(f\"ATLXI/df_dhdt_{placename.lower()}.parquet\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Choose one Antarctic active subglacial lake polygon with EPSG:3031 coordinates\n",
    "lake_name: str = \"Whillans IX\"\n",
    "lake_catalog = deepicedrain.catalog.subglacial_lakes()\n",
    "lake_ids, transect_id = (\n",
    "    pd.json_normalize(lake_catalog.metadata[\"lakedict\"])\n",
    "    .query(\"lakename == @lake_name\")[[\"ids\", \"transect\"]]\n",
    "    .iloc[0]\n",
    ")\n",
    "lake = (\n",
    "    lake_catalog.read()\n",
    "    .loc[lake_ids]\n",
    "    .dissolve(by=np.zeros(shape=len(lake_ids), dtype=\"int64\"), as_index=False)\n",
    "    .squeeze()\n",
    ")\n",
    "\n",
    "region = deepicedrain.Region.from_gdf(gdf=lake, name=lake_name)\n",
    "draining: bool = lake.inner_dhdt < 0\n",
    "\n",
    "print(lake)\n",
    "lake.geometry"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "# Subset data to lake of interest\n",
    "placename: str = region.name.lower().replace(\" \", \"_\")\n",
    "df_lake: pd.DataFrame = region.subset(data=df_dhdt)  # bbox subset\n",
    "gdf_lake = gpd.GeoDataFrame(\n",
    "    df_lake, geometry=gpd.points_from_xy(x=df_lake.x, y=df_lake.y, crs=3031)\n",
    ")\n",
    "df_lake: pd.DataFrame = df_lake.loc[gdf_lake.within(lake.geometry)]  # polygon subset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "# Run crossover analysis on all tracks\n",
    "track_dict: dict = deepicedrain.split_tracks(df=df_lake)\n",
    "rgts, tracks = track_dict.keys(), track_dict.values()\n",
    "# Parallelized paired crossover analysis\n",
    "futures: list = []\n",
    "for rgt1, rgt2 in itertools.combinations(rgts, r=2):\n",
    "    # skip if same referencegroundtrack but different laser pair\n",
    "    # as they are parallel and won't cross\n",
    "    if rgt1[:4] == rgt2[:4]:\n",
    "        continue\n",
    "    track1 = track_dict[rgt1][[\"x\", \"y\", \"h_corr\", \"utc_time\"]]\n",
    "    track2 = track_dict[rgt2][[\"x\", \"y\", \"h_corr\", \"utc_time\"]]\n",
    "    shape1 = shapely.geometry.LineString(coordinates=track1[[\"x\", \"y\"]].to_numpy())\n",
    "    shape2 = shapely.geometry.LineString(coordinates=track2[[\"x\", \"y\"]].to_numpy())\n",
    "    if not shape1.intersects(shape2):\n",
    "        continue\n",
    "    future = client.submit(\n",
    "        key=f\"{rgt1}x{rgt2}\",\n",
    "        func=pygmt.x2sys_cross,\n",
    "        tracks=[track1, track2],\n",
    "        tag=\"ICESAT2\",\n",
    "        # region=[-460000, -400000, -560000, -500000],\n",
    "        interpolation=\"l\",  # linear interpolation\n",
    "        coe=\"e\",  # external crossovers\n",
    "        trackvalues=True,  # Get track 1 height (h_1) and track 2 height (h_2)\n",
    "        # trackvalues=False,  # Get crossover error (h_X) and mean height value (h_M)\n",
    "        # outfile=\"xover_236_562.tsv\"\n",
    "    )\n",
    "    futures.append(future)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "crossovers: dict = {}\n",
    "for f in tqdm.tqdm(\n",
    "    iterable=dask.distributed.as_completed(futures=futures), total=len(futures)\n",
    "):\n",
    "    if f.status != \"error\":  # skip those track pairs which don't intersect\n",
    "        crossovers[f.key] = f.result().dropna().reset_index(drop=True)\n",
    "\n",
    "df_cross: pd.DataFrame = pd.concat(objs=crossovers, names=[\"track1_track2\", \"id\"])\n",
    "df: pd.DataFrame = df_cross.reset_index(level=\"track1_track2\").reset_index(drop=True)\n",
    "# Report on how many unique crossover intersections there were\n",
    "# df.plot.scatter(x=\"x\", y=\"y\")  # quick plot of our crossover points\n",
    "print(\n",
    "    f\"{len(df.groupby(by=['x', 'y']))} crossover intersection point locations found \"\n",
    "    f\"with {len(df)} crossover height-time pairs \"\n",
    "    f\"over {len(tracks)} tracks\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Calculate crossover error\n",
    "df[\"h_X\"]: pd.Series = df.h_2 - df.h_1  # crossover error (i.e. height difference)\n",
    "df[\"t_D\"]: pd.Series = df.t_2 - df.t_1  # elapsed time in ns (i.e. time difference)\n",
    "ns_in_yr: int = 365.25 * 24 * 60 * 60 * 1_000_000_000  # nanoseconds in a year\n",
    "df[\"dhdt\"]: pd.Series = df.h_X / (df.t_D.astype(np.int64) / ns_in_yr)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "# Get some summary statistics of our crossover errors\n",
    "sumstats: pd.DataFrame = df[[\"h_X\", \"t_D\", \"dhdt\"]].describe()\n",
    "# Find location with highest absolute crossover error, and most sudden height change\n",
    "max_h_X: pd.Series = df.iloc[np.nanargmax(df.h_X.abs())]  # highest crossover error\n",
    "max_dhdt: pd.Series = df.iloc[df.dhdt.argmax()]  # most sudden change in height"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2D Map view of crossover points\n",
    "\n",
    "Bird's eye view of the crossover points\n",
    "overlaid on top of the ICESat-2 tracks."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 2D plot of crossover locations\n",
    "var: str = \"h_X\"\n",
    "fig = pygmt.Figure()\n",
    "# Setup basemap\n",
    "plotregion = pygmt.info(table=df[[\"x\", \"y\"]], spacing=1000)\n",
    "pygmt.makecpt(cmap=\"batlow\", series=[sumstats[var][\"25%\"], sumstats[var][\"75%\"]])\n",
    "# Map frame in metre units\n",
    "fig.basemap(frame=\"f\", region=plotregion, projection=\"X8c\")\n",
    "# Plot actual track points in green\n",
    "for track in tracks:\n",
    "    tracklabel = f\"{track.iloc[0].referencegroundtrack} {track.iloc[0].pairtrack}\"\n",
    "    fig.plot(\n",
    "        x=track.x,\n",
    "        y=track.y,\n",
    "        pen=\"thinnest,green,.\",\n",
    "        style=f'qN+1:+l\"{tracklabel}\"+f3p,Helvetica,darkgreen',\n",
    "    )\n",
    "# Plot crossover point locations\n",
    "fig.plot(x=df.x, y=df.y, color=df.h_X, cmap=True, style=\"c0.1c\", pen=\"thinnest\")\n",
    "# Plot lake boundary in blue\n",
    "lakex, lakey = lake.geometry.exterior.coords.xy\n",
    "fig.plot(x=lakex, y=lakey, pen=\"thin,blue,-.\")\n",
    "# Map frame in kilometre units\n",
    "fig.basemap(\n",
    "    frame=[\n",
    "        f'WSne+t\"Crossover points at {region.name}\"',\n",
    "        'xaf+l\"Polar Stereographic X (km)\"',\n",
    "        'yaf+l\"Polar Stereographic Y (km)\"',\n",
    "    ],\n",
    "    region=plotregion / 1000,\n",
    "    projection=\"X8c\",\n",
    ")\n",
    "fig.colorbar(position=\"JMR+e\", frame=['x+l\"Crossover Error\"', \"y+lm\"])\n",
    "fig.savefig(f\"figures/{placename}/crossover_area_{placename}_{min_date}_{max_date}.png\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Plot Crossover Elevation time-series\n",
    "\n",
    "Plot elevation change over time at:\n",
    "\n",
    "1. One single crossover point location\n",
    "2. Many crossover locations over an area"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Tidy up dataframe first using pd.wide_to_long\n",
    "# I.e. convert 't_1', 't_2', 'h_1', 'h_2' columns into just 't' and 'h'.\n",
    "df_th: pd.DataFrame = deepicedrain.wide_to_long(\n",
    "    df=df.loc[:, [\"track1_track2\", \"x\", \"y\", \"t_1\", \"t_2\", \"h_1\", \"h_2\"]],\n",
    "    stubnames=[\"t\", \"h\"],\n",
    "    j=\"track\",\n",
    ")\n",
    "df_th: pd.DataFrame = df_th.drop_duplicates(ignore_index=True)\n",
    "df_th: pd.DataFrame = df_th.sort_values(by=\"t\").reset_index(drop=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Plot at single location with **maximum** absolute crossover height error (max_h_X)\n",
    "df_max = df_th.query(expr=\"x == @max_h_X.x & y == @max_h_X.y\")\n",
    "track1, track2 = df_max.track1_track2.iloc[0].split(\"x\")\n",
    "print(f\"{max_h_X.h_X:.2f} metres height change at {max_h_X.x}, {max_h_X.y}\")\n",
    "plotregion = np.array(\n",
    "    [df_max.t.min(), df_max.t.max(), *pygmt.info(table=df_max[[\"h\"]], spacing=2.5)[:2]]\n",
    ")\n",
    "plotregion += np.array([-pd.Timedelta(2, unit=\"W\"), +pd.Timedelta(2, unit=\"W\"), 0, 0])\n",
    "\n",
    "fig = pygmt.Figure()\n",
    "with pygmt.config(\n",
    "    FONT_ANNOT_PRIMARY=\"9p\", FORMAT_TIME_PRIMARY_MAP=\"abbreviated\", FORMAT_DATE_MAP=\"o\"\n",
    "):\n",
    "    fig.basemap(\n",
    "        projection=\"X12c/8c\",\n",
    "        region=plotregion,\n",
    "        frame=[\n",
    "            f'WSne+t\"Max elevation change over time at {region.name}\"',\n",
    "            \"pxa1Of1o+lDate\",  # primary time axis, 1 mOnth annotation and minor axis\n",
    "            \"sx1Y\",  # secondary time axis, 1 Year intervals\n",
    "            'yaf+l\"Elevation at crossover (m)\"',\n",
    "        ],\n",
    "    )\n",
    "fig.text(\n",
    "    text=f\"Track {track1} and {track2} crossover\",\n",
    "    position=\"TC\",\n",
    "    offset=\"jTC0c/0.2c\",\n",
    "    verbose=\"q\",\n",
    ")\n",
    "# Plot data points\n",
    "fig.plot(x=df_max.t, y=df_max.h, style=\"c0.15c\", color=\"darkblue\", pen=\"thin\")\n",
    "# Plot dashed line connecting points\n",
    "fig.plot(x=df_max.t, y=df_max.h, pen=f\"faint,blue,-\")\n",
    "fig.savefig(\n",
    "    f\"figures/{placename}/crossover_point_{placename}_{track1}_{track2}_{min_date}_{max_date}.png\"\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Plot all crossover height points over time over the lake area\n",
    "fig = deepicedrain.plot_crossovers(df=df_th, regionname=region.name)\n",
    "fig.savefig(f\"figures/{placename}/crossover_many_{placename}_{min_date}_{max_date}.png\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Calculate height anomaly at crossover point as\n",
    "# height at t=n minus height at t=0 (first observation date at crossover point)\n",
    "anomfunc = lambda h: h - h.iloc[0]  # lambda h: h - h.mean()\n",
    "df_th[\"h_anom\"] = df_th.groupby(by=\"track1_track2\").h.transform(func=anomfunc)\n",
    "# Calculate ice volume displacement (dvol) in unit metres^3\n",
    "# and rolling mean height anomaly (h_roll) in unit metres\n",
    "surface_area: pint.Quantity = lake.geometry.area * ureg.metre ** 2\n",
    "ice_dvol: pd.Series = deepicedrain.ice_volume_over_time(\n",
    "    df_elev=df_th.astype(dtype={\"h_anom\": \"pint[metre]\"}),\n",
    "    surface_area=surface_area,\n",
    "    time_col=\"t\",\n",
    "    outfile=f\"figures/{placename}/ice_dvol_dt_{placename}.txt\",\n",
    ")\n",
    "df_th[\"h_roll\"]: pd.Series = uncertainties.unumpy.nominal_values(\n",
    "    arr=ice_dvol.pint.magnitude / surface_area.magnitude\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Plot all crossover height point anomalies over time over the lake area\n",
    "fig = deepicedrain.plot_crossovers(\n",
    "    df=df_th,\n",
    "    regionname=region.name,\n",
    "    elev_var=\"h_anom\",\n",
    "    outline_points=f\"figures/{placename}/{placename}.gmt\",\n",
    ")\n",
    "fig.plot(x=df_th.t, y=df_th.h_roll, pen=\"thick,-\")  # plot rolling mean height anomaly\n",
    "fig.savefig(\n",
    "    f\"figures/{placename}/crossover_anomaly_{placename}_{min_date}_{max_date}.png\"\n",
    ")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Combined ice volume displacement plot\n",
    "\n",
    "Showing how subglacial water cascades down a drainage basin!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "lines_to_next_cell": 0
   },
   "outputs": [],
   "source": [
    "fig = pygmt.Figure()\n",
    "fig.basemap(\n",
    "    region=f\"2019-02-28/2020-09-30/-0.3/0.5\",\n",
    "    frame=[\"wSnE\", \"xaf\", 'yaf+l\"Ice Volume Displacement (km@+3@+)\"'],\n",
    ")\n",
    "pygmt.makecpt(cmap=\"davosS\", color_model=\"+c\", series=(-2, 4, 0.5))\n",
    "for i, (_placename, linestyle) in enumerate(\n",
    "    iterable=zip(\n",
    "        [\"whillans_ix\", \"subglacial_lake_whillans\", \"lake_12\", \"whillans_7\"],\n",
    "        [\"\", \".-\", \"-\", \"..-\"],\n",
    "    )\n",
    "):\n",
    "    fig.plot(\n",
    "        data=f\"figures/{_placename}/ice_dvol_dt_{_placename}.txt\",\n",
    "        cmap=True,\n",
    "        pen=f\"thick,{linestyle}\",\n",
    "        zvalue=i,\n",
    "        label=_placename,\n",
    "        columns=\"0,3\",  # time column (0), ice_dvol column (3)\n",
    "    )\n",
    "fig.text(\n",
    "    position=\"TL\",\n",
    "    offset=\"j0.2c\",\n",
    "    text=\"Whillans Ice Stream Central Catchment active subglacial lakes\",\n",
    ")\n",
    "fig.legend(position=\"jML+jML+o0.2c\", box=\"+gwhite\")\n",
    "fig.savefig(\"figures/cascade_whillans_ice_stream.png\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "jupytext": {
   "encoding": "# -*- coding: utf-8 -*-",
   "formats": "ipynb,py:hydrogen"
  },
  "kernelspec": {
   "display_name": "deepicedrain",
   "language": "python",
   "name": "deepicedrain"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}