diff --git a/README.md b/README.md index 29ade9e..b045e0f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ in Antarctica using remote sensing and machine learning. ![ICESat-2 ATL11 rate of height change over time in Antarctica 2018-10-14 to 2020-05-13](https://user-images.githubusercontent.com/23487320/90118294-2601ff80-ddac-11ea-8b93-7bc9b15f2be0.png) -![DeepIceDrain Pipeline](https://yuml.me/diagram/scruffy;dir:LR/class/[Land-Ice-Elevation|atl06_play.ipynb]->[Convert|atl06_to_atl11.ipynb],[Convert]->[Ice-Sheet-H(t)-Series|atl11_play.ipynb],[Ice-Sheet-H(t)-Series]->[Height-Change-over-Time-(dhdt)|atlxi_dhdt.ipynb]) +![DeepIceDrain Pipeline](https://yuml.me/diagram/scruffy;dir:LR/class/[Land-Ice-Elevation|atl06_play.ipynb]->[Convert|atl06_to_atl11.ipynb],[Convert]->[Ice-Sheet-H(t)-Series|atl11_play.ipynb],[Ice-Sheet-H(t)-Series]->[Height-Change-over-Time-(dhdt)|atlxi_dhdt.ipynb],[Height-Change-over-Time-(dhdt)]->[Subglacial-Lake-Finder|atlxi_lake.ipynb]) # Getting started diff --git a/atlxi_dhdt.ipynb b/atlxi_dhdt.ipynb index 6cceb72..b215faf 100644 --- a/atlxi_dhdt.ipynb +++ b/atlxi_dhdt.ipynb @@ -34,18 +34,22 @@ "source": [ "import itertools\n", "import os\n", + "import warnings\n", "\n", "import numpy as np\n", "import pandas as pd\n", "import xarray as xr\n", "\n", + "import cudf # comment out if no GPU\n", "import dask\n", "import datashader\n", "import deepicedrain\n", "import holoviews as hv\n", + "import hvplot.cudf # comment out if no GPU\n", "import hvplot.pandas\n", "import intake\n", "import panel as pn\n", + "import param\n", "import pygmt\n", "import scipy.stats\n", "import tqdm" @@ -902,63 +906,129 @@ "if not os.path.exists(f\"ATLXI/df_dhdt_{placename}.parquet\"):\n", " # Subset dataset to geographic region of interest\n", " ds_subset: xr.Dataset = region.subset(data=ds_dhdt)\n", - " # Add a UTC_time column to the dataframe\n", + " # Add a UTC_time column to the dataset\n", " ds_subset[\"utc_time\"] = deepicedrain.deltatime_to_utctime(\n", " dataarray=ds_subset.delta_time\n", " )\n", - " # Convert xarray.Dataset to pandas.DataFrame for easier analysis\n", - " df_many: pd.DataFrame = ds_subset.to_dataframe().dropna()\n", - " # Drop delta_time column since timedelta64 dtype cannot be saved to parquet\n", - " # https://github.com/pandas-dev/pandas/issues/31909\n", - " df_many: pd.DataFrame = df_many.drop(columns=\"delta_time\")\n", - " # Need to use_deprecated_int96_timestamps in order to save utc_time column\n", - " # https://issues.apache.org/jira/browse/ARROW-1957\n", - " df_many.to_parquet(\n", - " f\"ATLXI/df_dhdt_{placename}.parquet\", use_deprecated_int96_timestamps=True\n", + " # Save to parquet format\n", + " deepicedrain.ndarray_to_parquet(\n", + " ndarray=ds_subset,\n", + " parquetpath=f\"ATLXI/df_dhdt_{placename}.parquet\",\n", + " variables=[\n", + " \"x\",\n", + " \"y\",\n", + " \"dhdt_slope\",\n", + " \"referencegroundtrack\",\n", + " \"h_corr\",\n", + " \"utc_time\",\n", + " ],\n", + " dropnacols=[\"dhdt_slope\"],\n", + " use_deprecated_int96_timestamps=True,\n", " )\n", - "df_many = pd.read_parquet(f\"ATLXI/df_dhdt_{placename}.parquet\")" + "# df_many = pd.read_parquet(f\"ATLXI/df_dhdt_{placename}.parquet\")\n", + "df_dhdt = cudf.read_parquet(f\"ATLXI/df_dhdt_{placename}.parquet\")" ] }, { "cell_type": "code", "execution_count": 32, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "warnings.filterwarnings(\n", + " action=\"ignore\",\n", + " message=\"The global colormaps dictionary is no longer considered public API.\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "lines_to_next_cell": 1 + }, "outputs": [], "source": [ - "def dhdt_plot(\n", - " cycle: int = 7,\n", - " dhdt_variable: str = \"dhdt_slope\",\n", - " dhdt_range: tuple = (1, 10),\n", - " rasterize: bool = False,\n", - " datashade: bool = False,\n", - ") -> hv.element.chart.Scatter:\n", + "class IceSat2Explorer(param.Parameterized):\n", " \"\"\"\n", - " ICESat-2 rate of height change over time (dhdt) interactive scatter plot.\n", - " Uses HvPlot, and intended to be used inside a Panel dashboard.\n", + " ICESat-2 rate of height change over time (dhdt) interactive dashboard.\n", + " Built using HvPlot and Panel.\n", + "\n", + " Adapted from the \"Panel-based Datashader dashboard\" at\n", + " https://examples.pyviz.org/datashader_dashboard/dashboard.html.\n", + " See also https://github.com/holoviz/datashader/pull/676.\n", " \"\"\"\n", - " df_ = df_many.query(\n", - " expr=\"cycle_number == @cycle & \"\n", - " \"@dhdt_range[0] < abs(dhdt_slope) & abs(dhdt_slope) < @dhdt_range[1]\"\n", - " )\n", - " return df_.hvplot.scatter(\n", - " title=f\"ICESat-2 Cycle {cycle} {dhdt_variable}\",\n", - " x=\"x\",\n", - " y=\"y\",\n", - " c=dhdt_variable,\n", - " cmap=\"gist_earth\" if dhdt_variable == \"h_corr\" else \"BrBG\",\n", - " clim=None,\n", - " # by=\"cycle_number\",\n", - " rasterize=rasterize,\n", - " datashade=datashade,\n", - " dynspread=datashade,\n", - " hover=True,\n", - " hover_cols=[\"referencegroundtrack\", \"dhdt_slope\", \"h_corr\"],\n", - " colorbar=True,\n", - " grid=True,\n", - " frame_width=1000,\n", - " frame_height=600,\n", - " data_aspect=1,\n", - " )" + "\n", + " variable_cmap: dict = {\n", + " \"referencegroundtrack\": \"glasbey\",\n", + " \"dhdt_slope\": \"BrBG\",\n", + " \"h_corr\": \"gist_earth\",\n", + " }\n", + " dhdt_variable = param.Selector(default=\"dhdt_slope\", objects=variable_cmap.keys())\n", + " cycle = param.Integer(default=7, bounds=(2, 7))\n", + " dhdt_range = param.Range(default=(1.0, 10.0), bounds=(0.0, 20.0))\n", + " rasterize = param.Boolean(default=False)\n", + " datashade = param.Boolean(default=False)\n", + "\n", + " df_ = df_dhdt\n", + " plot = df_.hvplot.points(x=\"x\", y=\"y\", c=\"dhdt_slope\", cmap=\"BrBG\")\n", + " startX, endX = plot.range(\"x\")\n", + " startY, endY = plot.range(\"y\")\n", + "\n", + " def keep_zoom(self, x_range, y_range):\n", + " self.startX, self.endX = x_range\n", + " self.startY, self.endY = y_range\n", + "\n", + " @param.depends(\"cycle\", \"dhdt_variable\", \"dhdt_range\", \"rasterize\", \"datashade\")\n", + " def view(self):\n", + " cond = np.logical_and(\n", + " float(self.dhdt_range[0]) < abs(self.df_.dhdt_slope),\n", + " abs(self.df_.dhdt_slope) < float(self.dhdt_range[1]),\n", + " )\n", + " column: str = (\n", + " self.dhdt_variable\n", + " if self.dhdt_variable != \"h_corr\"\n", + " else f\"h_corr_{self.cycle}\"\n", + " )\n", + " if self.dhdt_variable == \"h_corr\":\n", + " df_subset = self.df_.loc[cond].dropna(subset=f\"h_corr_{self.cycle}\")\n", + " else:\n", + " df_subset = self.df_.loc[cond]\n", + " self.plot = df_subset.hvplot.points(\n", + " title=f\"ICESat-2 Cycle {self.cycle} {self.dhdt_variable}\",\n", + " x=\"x\",\n", + " y=\"y\",\n", + " c=column,\n", + " cmap=self.variable_cmap[self.dhdt_variable],\n", + " rasterize=self.rasterize,\n", + " datashade=self.datashade,\n", + " dynspread=self.datashade,\n", + " hover=True,\n", + " hover_cols=[\n", + " \"referencegroundtrack\",\n", + " \"dhdt_slope\",\n", + " f\"h_corr_{self.cycle}\",\n", + " f\"utc_time_{self.cycle}\",\n", + " ],\n", + " colorbar=True,\n", + " grid=True,\n", + " frame_width=1000,\n", + " frame_height=600,\n", + " data_aspect=1,\n", + " )\n", + " self.plot = self.plot.redim.range(\n", + " x=(self.startX, self.endX), y=(self.startY, self.endY)\n", + " )\n", + " self.plot = self.plot.opts(active_tools=[\"pan\", \"wheel_zoom\"])\n", + " rangexy = hv.streams.RangeXY(\n", + " source=self.plot,\n", + " x_range=(self.startX, self.endX),\n", + " y_range=(self.startY, self.endY),\n", + " )\n", + " rangexy.add_subscriber(self.keep_zoom)\n", + " return self.plot" ] }, { @@ -969,27 +1039,24 @@ "source": [ "# Interactive holoviews scatter plot to find referencegroundtrack needed\n", "# Tip: Hover over the points, and find those with high 'dhdt_slope' values\n", - "layout: pn.layout.Column = pn.interact(\n", - " dhdt_plot,\n", - " cycle=pn.widgets.IntSlider(name=\"Cycle Number\", start=2, end=7, step=1, value=7),\n", - " dhdt_variable=pn.widgets.RadioButtonGroup(\n", - " name=\"dhdt_variables\",\n", - " value=\"dhdt_slope\",\n", - " options=[\"referencegroundtrack\", \"dhdt_slope\", \"h_corr\"],\n", - " ),\n", - " dhdt_range=pn.widgets.RangeSlider(\n", - " name=\"dhdt range ±\", start=0, end=20, value=(1, 10), step=0.25\n", - " ),\n", - " rasterize=pn.widgets.Checkbox(name=\"Rasterize\"),\n", - " datashade=pn.widgets.Checkbox(name=\"Datashade\"),\n", + "viewer = IceSat2Explorer()\n", + "widgets: pn.param.Param = pn.Param(\n", + " viewer.param,\n", + " widgets={\n", + " \"dhdt_variable\": pn.widgets.RadioButtonGroup,\n", + " \"cycle\": pn.widgets.IntSlider,\n", + " \"dhdt_range\": pn.widgets.RangeSlider,\n", + " \"rasterize\": pn.widgets.Checkbox,\n", + " \"datashade\": pn.widgets.Checkbox,\n", + " },\n", ")\n", "dashboard: pn.layout.Column = pn.Column(\n", " pn.Row(\n", - " pn.Column(layout[0][1], align=\"center\"),\n", - " pn.Column(layout[0][0], layout[0][2], align=\"center\"),\n", - " pn.Column(layout[0][3], layout[0][4], align=\"center\"),\n", + " pn.Column(widgets[0], widgets[1], align=\"center\"),\n", + " pn.Column(widgets[2], widgets[3], align=\"center\"),\n", + " pn.Column(widgets[4], widgets[5], align=\"center\"),\n", " ),\n", - " layout[1],\n", + " viewer.view,\n", ")\n", "# dashboard" ] @@ -1116,347 +1183,6 @@ "fig.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "lines_to_next_cell": 2 - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": { - "lines_to_next_cell": 2 - }, - "source": [ - "# 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 [x2sys_cross](https://docs.generic-mapping-tools.org/6.1/supplements/x2sys/x2sys_cross).\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": 40, - "metadata": {}, - "outputs": [], - "source": [ - "# Initialize X2SYS database in the X2SYS/ICESAT2 folder\n", - "tag = \"X2SYS\"\n", - "os.environ[\"X2SYS_HOME\"] = os.path.abspath(tag)\n", - "os.getcwd()\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": "code", - "execution_count": 44, - "metadata": { - "lines_to_next_cell": 2 - }, - "outputs": [], - "source": [ - "# Run crossover analysis on all tracks\n", - "rgts: list = [135, 327, 388, 577, 1080, 1272] # Whillans upstream\n", - "# rgts: list = [236, 501, 562, 1181] # Whillans_downstream\n", - "tracks = [f\"{tag}/track_{i}.tsv\" for i in rgts]\n", - "assert all(os.path.exists(k) for k in tracks)\n", - "\n", - "# Parallelized paired crossover analysis\n", - "futures: list = []\n", - "for track1, track2 in itertools.combinations(rgts, r=2):\n", - " future = client.submit(\n", - " key=f\"{track1}_{track2}\",\n", - " func=pygmt.x2sys_cross,\n", - " tracks=[f\"{tag}/track_{track1}.tsv\", f\"{tag}/track_{track2}.tsv\"],\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": 42, - "metadata": { - "lines_to_next_cell": 2 - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 15/15 [00:00<00:00, 244.92it/s]" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "26 crossover intersection point locations found with 332 crossover height-time pairs over 6 tracks\n" - ] - } - ], - "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": 43, - "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": 44, - "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": { - "lines_to_next_cell": 2 - }, - "outputs": [], - "source": [ - "# 2D plot of crossover locations\n", - "var: str = \"h_X\"\n", - "fig = pygmt.Figure()\n", - "# Setup basemap\n", - "region = np.array([df.x.min(), df.x.max(), df.y.min(), df.y.max()])\n", - "buffer = np.array([-2000, +2000, -2000, +2000])\n", - "pygmt.makecpt(cmap=\"batlow\", series=[sumstats[var][\"25%\"], sumstats[var][\"75%\"]])\n", - "# Map frame in metre units\n", - "fig.basemap(frame=\"f\", region=region + buffer, projection=\"X8c\")\n", - "# Plot actual track points\n", - "for track in tracks:\n", - " fig.plot(data=track, color=\"green\", style=\"c0.01c\")\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", - "# Map frame in kilometre units\n", - "fig.basemap(\n", - " frame=[\n", - " \"WSne\",\n", - " 'xaf+l\"Polar Stereographic X (km)\"',\n", - " 'yaf+l\"Polar Stereographic Y (km)\"',\n", - " ],\n", - " region=(region + buffer) / 1000,\n", - " projection=\"X8c\",\n", - ")\n", - "fig.colorbar(position=\"JMR\", frame=['x+l\"Crossover Error\"', \"y+lm\"])\n", - "fig.savefig(\"figures/crossover_area.png\")\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1D plots of height changing over time\n", - "\n", - "Plot height 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": 46, - "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[\"id\"] = df.index\n", - "df_th: pd.DataFrame = pd.wide_to_long(\n", - " df=df[[\"id\", \"track1_track2\", \"x\", \"y\", \"t_1\", \"t_2\", \"h_1\", \"h_2\"]],\n", - " stubnames=[\"t\", \"h\"],\n", - " i=\"id\",\n", - " j=\"track\",\n", - " sep=\"_\",\n", - ")\n", - "df_th = df_th.reset_index(level=\"track\").drop_duplicates(ignore_index=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "6.02 metres height change at -445283.940476, -538147.2479729999\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "execution_count": 49, - "metadata": { - "image/png": { - "width": 500 - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "# 1D Plot at 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\").sort_values(by=\"t\")\n", - "track1, track2 = df_max.track1_track2.iloc[0].split(\"_\")\n", - "print(f\"{round(max_h_X.h_X, 2)} metres height change at {max_h_X.x}, {max_h_X.y}\")\n", - "t_min = (df_max.t.min() - pd.Timedelta(2, unit=\"W\")).isoformat()\n", - "t_max = (df_max.t.max() + pd.Timedelta(2, unit=\"W\")).isoformat()\n", - "h_min = df_max.h.min() - 0.2\n", - "h_max = df_max.h.max() + 0.4\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=[t_min, t_max, h_min, h_max],\n", - " frame=[\n", - " \"WSne\",\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", - " V=\"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(f\"figures/crossover_{track1}_{track2}_{min_date}_{max_date}.png\")\n", - "fig.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# 1D plots of a crossover area, all the height points over time\n", - "t_min = (df_th.t.min() - pd.Timedelta(1, unit=\"W\")).isoformat()\n", - "t_max = (df_th.t.max() + pd.Timedelta(1, unit=\"W\")).isoformat()\n", - "h_min = df_th.h.min() - 0.2\n", - "h_max = df_th.h.max() + 0.2\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/12c\",\n", - " region=[t_min, t_max, h_min, h_max],\n", - " frame=[\n", - " \"WSne\",\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", - "\n", - "crossovers = df_th.groupby(by=[\"x\", \"y\"])\n", - "pygmt.makecpt(cmap=\"categorical\", series=[1, len(crossovers) + 1, 1])\n", - "for i, ((x_coord, y_coord), indexes) in enumerate(crossovers.indices.items()):\n", - " df_ = df_th.loc[indexes].sort_values(by=\"t\")\n", - " # if df_.h.max() - df_.h.min() > 1.0: # plot only > 1 metre height change\n", - " track1, track2 = df_.track1_track2.iloc[0].split(\"_\")\n", - " label = f'\"Track {track1} {track2}\"'\n", - " fig.plot(x=df_.t, y=df_.h, Z=i, style=\"c0.1c\", cmap=True, pen=\"thin+z\", label=label)\n", - " # Plot line connecting points\n", - " fig.plot(\n", - " x=df_.t, y=df_.h, Z=i, pen=f\"faint,+z,-\", cmap=True\n", - " ) # , label=f'\"+g-1l+s0.15c\"')\n", - "fig.legend(position=\"JMR+JMR+o0.2c\", box=\"+gwhite+p1p\")\n", - "fig.savefig(f\"figures/crossover_many_{min_date}_{max_date}.png\")\n", - "fig.show()" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/atlxi_dhdt.py b/atlxi_dhdt.py index cdbea52..9b1becc 100644 --- a/atlxi_dhdt.py +++ b/atlxi_dhdt.py @@ -37,18 +37,22 @@ # %% import itertools import os +import warnings import numpy as np import pandas as pd import xarray as xr +import cudf # comment out if no GPU import dask import datashader import deepicedrain import holoviews as hv +import hvplot.cudf # comment out if no GPU import hvplot.pandas import intake import panel as pn +import param import pygmt import scipy.stats import tqdm @@ -399,84 +403,136 @@ if not os.path.exists(f"ATLXI/df_dhdt_{placename}.parquet"): # Subset dataset to geographic region of interest ds_subset: xr.Dataset = region.subset(data=ds_dhdt) - # Add a UTC_time column to the dataframe + # Add a UTC_time column to the dataset ds_subset["utc_time"] = deepicedrain.deltatime_to_utctime( dataarray=ds_subset.delta_time ) - # Convert xarray.Dataset to pandas.DataFrame for easier analysis - df_many: pd.DataFrame = ds_subset.to_dataframe().dropna() - # Drop delta_time column since timedelta64 dtype cannot be saved to parquet - # https://github.com/pandas-dev/pandas/issues/31909 - df_many: pd.DataFrame = df_many.drop(columns="delta_time") - # Need to use_deprecated_int96_timestamps in order to save utc_time column - # https://issues.apache.org/jira/browse/ARROW-1957 - df_many.to_parquet( - f"ATLXI/df_dhdt_{placename}.parquet", use_deprecated_int96_timestamps=True + # Save to parquet format + deepicedrain.ndarray_to_parquet( + ndarray=ds_subset, + parquetpath=f"ATLXI/df_dhdt_{placename}.parquet", + variables=[ + "x", + "y", + "dhdt_slope", + "referencegroundtrack", + "h_corr", + "utc_time", + ], + dropnacols=["dhdt_slope"], + use_deprecated_int96_timestamps=True, ) -df_many = pd.read_parquet(f"ATLXI/df_dhdt_{placename}.parquet") +# df_many = pd.read_parquet(f"ATLXI/df_dhdt_{placename}.parquet") +df_dhdt = cudf.read_parquet(f"ATLXI/df_dhdt_{placename}.parquet") +# %% +warnings.filterwarnings( + action="ignore", + message="The global colormaps dictionary is no longer considered public API.", +) # %% -def dhdt_plot( - cycle: int = 7, - dhdt_variable: str = "dhdt_slope", - dhdt_range: tuple = (1, 10), - rasterize: bool = False, - datashade: bool = False, -) -> hv.element.chart.Scatter: +class IceSat2Explorer(param.Parameterized): """ - ICESat-2 rate of height change over time (dhdt) interactive scatter plot. - Uses HvPlot, and intended to be used inside a Panel dashboard. + ICESat-2 rate of height change over time (dhdt) interactive dashboard. + Built using HvPlot and Panel. + + Adapted from the "Panel-based Datashader dashboard" at + https://examples.pyviz.org/datashader_dashboard/dashboard.html. + See also https://github.com/holoviz/datashader/pull/676. """ - df_ = df_many.query( - expr="cycle_number == @cycle & " - "@dhdt_range[0] < abs(dhdt_slope) & abs(dhdt_slope) < @dhdt_range[1]" - ) - return df_.hvplot.scatter( - title=f"ICESat-2 Cycle {cycle} {dhdt_variable}", - x="x", - y="y", - c=dhdt_variable, - cmap="gist_earth" if dhdt_variable == "h_corr" else "BrBG", - clim=None, - # by="cycle_number", - rasterize=rasterize, - datashade=datashade, - dynspread=datashade, - hover=True, - hover_cols=["referencegroundtrack", "dhdt_slope", "h_corr"], - colorbar=True, - grid=True, - frame_width=1000, - frame_height=600, - data_aspect=1, - ) + + variable_cmap: dict = { + "referencegroundtrack": "glasbey", + "dhdt_slope": "BrBG", + "h_corr": "gist_earth", + } + dhdt_variable = param.Selector(default="dhdt_slope", objects=variable_cmap.keys()) + cycle = param.Integer(default=7, bounds=(2, 7)) + dhdt_range = param.Range(default=(1.0, 10.0), bounds=(0.0, 20.0)) + rasterize = param.Boolean(default=False) + datashade = param.Boolean(default=False) + + df_ = df_dhdt + plot = df_.hvplot.points(x="x", y="y", c="dhdt_slope", cmap="BrBG") + startX, endX = plot.range("x") + startY, endY = plot.range("y") + + def keep_zoom(self, x_range, y_range): + self.startX, self.endX = x_range + self.startY, self.endY = y_range + + @param.depends("cycle", "dhdt_variable", "dhdt_range", "rasterize", "datashade") + def view(self): + cond = np.logical_and( + float(self.dhdt_range[0]) < abs(self.df_.dhdt_slope), + abs(self.df_.dhdt_slope) < float(self.dhdt_range[1]), + ) + column: str = ( + self.dhdt_variable + if self.dhdt_variable != "h_corr" + else f"h_corr_{self.cycle}" + ) + if self.dhdt_variable == "h_corr": + df_subset = self.df_.loc[cond].dropna(subset=f"h_corr_{self.cycle}") + else: + df_subset = self.df_.loc[cond] + self.plot = df_subset.hvplot.points( + title=f"ICESat-2 Cycle {self.cycle} {self.dhdt_variable}", + x="x", + y="y", + c=column, + cmap=self.variable_cmap[self.dhdt_variable], + rasterize=self.rasterize, + datashade=self.datashade, + dynspread=self.datashade, + hover=True, + hover_cols=[ + "referencegroundtrack", + "dhdt_slope", + f"h_corr_{self.cycle}", + f"utc_time_{self.cycle}", + ], + colorbar=True, + grid=True, + frame_width=1000, + frame_height=600, + data_aspect=1, + ) + self.plot = self.plot.redim.range( + x=(self.startX, self.endX), y=(self.startY, self.endY) + ) + self.plot = self.plot.opts(active_tools=["pan", "wheel_zoom"]) + rangexy = hv.streams.RangeXY( + source=self.plot, + x_range=(self.startX, self.endX), + y_range=(self.startY, self.endY), + ) + rangexy.add_subscriber(self.keep_zoom) + return self.plot # %% # Interactive holoviews scatter plot to find referencegroundtrack needed # Tip: Hover over the points, and find those with high 'dhdt_slope' values -layout: pn.layout.Column = pn.interact( - dhdt_plot, - cycle=pn.widgets.IntSlider(name="Cycle Number", start=2, end=7, step=1, value=7), - dhdt_variable=pn.widgets.RadioButtonGroup( - name="dhdt_variables", - value="dhdt_slope", - options=["referencegroundtrack", "dhdt_slope", "h_corr"], - ), - dhdt_range=pn.widgets.RangeSlider( - name="dhdt range ±", start=0, end=20, value=(1, 10), step=0.25 - ), - rasterize=pn.widgets.Checkbox(name="Rasterize"), - datashade=pn.widgets.Checkbox(name="Datashade"), +viewer = IceSat2Explorer() +widgets: pn.param.Param = pn.Param( + viewer.param, + widgets={ + "dhdt_variable": pn.widgets.RadioButtonGroup, + "cycle": pn.widgets.IntSlider, + "dhdt_range": pn.widgets.RangeSlider, + "rasterize": pn.widgets.Checkbox, + "datashade": pn.widgets.Checkbox, + }, ) dashboard: pn.layout.Column = pn.Column( pn.Row( - pn.Column(layout[0][1], align="center"), - pn.Column(layout[0][0], layout[0][2], align="center"), - pn.Column(layout[0][3], layout[0][4], align="center"), + pn.Column(widgets[0], widgets[1], align="center"), + pn.Column(widgets[2], widgets[3], align="center"), + pn.Column(widgets[4], widgets[5], align="center"), ), - layout[1], + viewer.view, ) # dashboard @@ -559,229 +615,3 @@ def dhdt_plot( fig.show() # %% - - -# %% [markdown] -# # Crossover Track Analysis -# -# To increase the temporal resolution of -# our ice elevation change analysis -# (i.e. at time periods less than -# the 91 day repeat cycle of ICESat-2), -# we can look at the locations where the -# ICESat-2 tracks intersect and get the -# height values there! -# Uses [x2sys_cross](https://docs.generic-mapping-tools.org/6.1/supplements/x2sys/x2sys_cross). -# -# References: -# - Wessel, P. (2010). Tools for analyzing intersecting tracks: The x2sys package. -# Computers & Geosciences, 36(3), 348–354. https://doi.org/10.1016/j.cageo.2009.05.009 - - -# %% -# Initialize X2SYS database in the X2SYS/ICESAT2 folder -tag = "X2SYS" -os.environ["X2SYS_HOME"] = os.path.abspath(tag) -os.getcwd() -pygmt.x2sys_init( - tag="ICESAT2", - fmtfile=f"{tag}/ICESAT2/xyht", - suffix="tsv", - units=["de", "se"], # distance in metres, speed in metres per second - gap="d250e", # distance gap up to 250 metres allowed - force=True, - verbose="q", -) - -# %% -# Run crossover analysis on all tracks -rgts: list = [135, 327, 388, 577, 1080, 1272] # Whillans upstream -# rgts: list = [236, 501, 562, 1181] # Whillans_downstream -tracks = [f"{tag}/track_{i}.tsv" for i in rgts] -assert all(os.path.exists(k) for k in tracks) - -# Parallelized paired crossover analysis -futures: list = [] -for track1, track2 in itertools.combinations(rgts, r=2): - future = client.submit( - key=f"{track1}_{track2}", - func=pygmt.x2sys_cross, - tracks=[f"{tag}/track_{track1}.tsv", f"{tag}/track_{track2}.tsv"], - tag="ICESAT2", - region=[-460000, -400000, -560000, -500000], - interpolation="l", # linear interpolation - coe="e", # external crossovers - trackvalues=True, # Get track 1 height (h_1) and track 2 height (h_2) - # trackvalues=False, # Get crossover error (h_X) and mean height value (h_M) - # outfile="xover_236_562.tsv" - ) - futures.append(future) - - -# %% -crossovers: dict = {} -for f in tqdm.tqdm( - iterable=dask.distributed.as_completed(futures=futures), total=len(futures) -): - if f.status != "error": # skip those track pairs which don't intersect - crossovers[f.key] = f.result().dropna().reset_index(drop=True) - -df_cross: pd.DataFrame = pd.concat(objs=crossovers, names=["track1_track2", "id"]) -df: pd.DataFrame = df_cross.reset_index(level="track1_track2").reset_index(drop=True) -# Report on how many unique crossover intersections there were -# df.plot.scatter(x="x", y="y") # quick plot of our crossover points -print( - f"{len(df.groupby(by=['x', 'y']))} crossover intersection point locations found " - f"with {len(df)} crossover height-time pairs " - f"over {len(tracks)} tracks" -) - - -# %% -# Calculate crossover error -df["h_X"]: pd.Series = df.h_2 - df.h_1 # crossover error (i.e. height difference) -df["t_D"]: pd.Series = df.t_2 - df.t_1 # elapsed time in ns (i.e. time difference) -ns_in_yr: int = (365.25 * 24 * 60 * 60 * 1_000_000_000) # nanoseconds in a year -df["dhdt"]: pd.Series = df.h_X / (df.t_D.astype(np.int64) / ns_in_yr) - -# %% -# Get some summary statistics of our crossover errors -sumstats: pd.DataFrame = df[["h_X", "t_D", "dhdt"]].describe() -# Find location with highest absolute crossover error, and most sudden height change -max_h_X: pd.Series = df.iloc[np.nanargmax(df.h_X.abs())] # highest crossover error -max_dhdt: pd.Series = df.iloc[df.dhdt.argmax()] # most sudden change in height - - -# %% [markdown] -# ### 2D Map view of crossover points -# -# Bird's eye view of the crossover points -# overlaid on top of the ICESat-2 tracks. - -# %% -# 2D plot of crossover locations -var: str = "h_X" -fig = pygmt.Figure() -# Setup basemap -region = np.array([df.x.min(), df.x.max(), df.y.min(), df.y.max()]) -buffer = np.array([-2000, +2000, -2000, +2000]) -pygmt.makecpt(cmap="batlow", series=[sumstats[var]["25%"], sumstats[var]["75%"]]) -# Map frame in metre units -fig.basemap(frame="f", region=region + buffer, projection="X8c") -# Plot actual track points -for track in tracks: - fig.plot(data=track, color="green", style="c0.01c") -# Plot crossover point locations -fig.plot(x=df.x, y=df.y, color=df.h_X, cmap=True, style="c0.1c", pen="thinnest") -# Map frame in kilometre units -fig.basemap( - frame=[ - "WSne", - 'xaf+l"Polar Stereographic X (km)"', - 'yaf+l"Polar Stereographic Y (km)"', - ], - region=(region + buffer) / 1000, - projection="X8c", -) -fig.colorbar(position="JMR", frame=['x+l"Crossover Error"', "y+lm"]) -fig.savefig("figures/crossover_area.png") -fig.show() - - -# %% [markdown] -# ### 1D plots of height changing over time -# -# Plot height change over time at: -# -# 1. One single crossover point location -# 2. Many crossover locations over an area - -# %% -# Tidy up dataframe first using pd.wide_to_long -# I.e. convert 't_1', 't_2', 'h_1', 'h_2' columns into just 't' and 'h'. -df["id"] = df.index -df_th: pd.DataFrame = pd.wide_to_long( - df=df[["id", "track1_track2", "x", "y", "t_1", "t_2", "h_1", "h_2"]], - stubnames=["t", "h"], - i="id", - j="track", - sep="_", -) -df_th = df_th.reset_index(level="track").drop_duplicates(ignore_index=True) - -# %% -# 1D Plot at location with **maximum** absolute crossover height error (max_h_X) -df_max = df_th.query(expr="x == @max_h_X.x & y == @max_h_X.y").sort_values(by="t") -track1, track2 = df_max.track1_track2.iloc[0].split("_") -print(f"{round(max_h_X.h_X, 2)} metres height change at {max_h_X.x}, {max_h_X.y}") -t_min = (df_max.t.min() - pd.Timedelta(2, unit="W")).isoformat() -t_max = (df_max.t.max() + pd.Timedelta(2, unit="W")).isoformat() -h_min = df_max.h.min() - 0.2 -h_max = df_max.h.max() + 0.4 - -fig = pygmt.Figure() -with pygmt.config( - FONT_ANNOT_PRIMARY="9p", FORMAT_TIME_PRIMARY_MAP="abbreviated", FORMAT_DATE_MAP="o" -): - fig.basemap( - projection="X12c/8c", - region=[t_min, t_max, h_min, h_max], - frame=[ - "WSne", - "pxa1Of1o+lDate", # primary time axis, 1 mOnth annotation and minor axis - "sx1Y", # secondary time axis, 1 Year intervals - 'yaf+l"Elevation at crossover (m)"', - ], - ) -fig.text( - text=f"Track {track1} and {track2} crossover", - position="TC", - offset="jTC0c/0.2c", - V="q", -) -# Plot data points -fig.plot(x=df_max.t, y=df_max.h, style="c0.15c", color="darkblue", pen="thin") -# Plot dashed line connecting points -fig.plot(x=df_max.t, y=df_max.h, pen=f"faint,blue,-") -fig.savefig(f"figures/crossover_{track1}_{track2}_{min_date}_{max_date}.png") -fig.show() - -# %% -# 1D plots of a crossover area, all the height points over time -t_min = (df_th.t.min() - pd.Timedelta(1, unit="W")).isoformat() -t_max = (df_th.t.max() + pd.Timedelta(1, unit="W")).isoformat() -h_min = df_th.h.min() - 0.2 -h_max = df_th.h.max() + 0.2 - -fig = pygmt.Figure() -with pygmt.config( - FONT_ANNOT_PRIMARY="9p", FORMAT_TIME_PRIMARY_MAP="abbreviated", FORMAT_DATE_MAP="o" -): - fig.basemap( - projection="X12c/12c", - region=[t_min, t_max, h_min, h_max], - frame=[ - "WSne", - "pxa1Of1o+lDate", # primary time axis, 1 mOnth annotation and minor axis - "sx1Y", # secondary time axis, 1 Year intervals - 'yaf+l"Elevation at crossover (m)"', - ], - ) - -crossovers = df_th.groupby(by=["x", "y"]) -pygmt.makecpt(cmap="categorical", series=[1, len(crossovers) + 1, 1]) -for i, ((x_coord, y_coord), indexes) in enumerate(crossovers.indices.items()): - df_ = df_th.loc[indexes].sort_values(by="t") - # if df_.h.max() - df_.h.min() > 1.0: # plot only > 1 metre height change - track1, track2 = df_.track1_track2.iloc[0].split("_") - label = f'"Track {track1} {track2}"' - fig.plot(x=df_.t, y=df_.h, Z=i, style="c0.1c", cmap=True, pen="thin+z", label=label) - # Plot line connecting points - fig.plot( - x=df_.t, y=df_.h, Z=i, pen=f"faint,+z,-", cmap=True - ) # , label=f'"+g-1l+s0.15c"') -fig.legend(position="JMR+JMR+o0.2c", box="+gwhite+p1p") -fig.savefig(f"figures/crossover_many_{min_date}_{max_date}.png") -fig.show() - -# %% diff --git a/atlxi_lake.ipynb b/atlxi_lake.ipynb new file mode 100644 index 0000000..78c663b --- /dev/null +++ b/atlxi_lake.ipynb @@ -0,0 +1,330 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "# 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 [x2sys_cross](https://docs.generic-mapping-tools.org/6.1/supplements/x2sys/x2sys_cross).\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": [ + "# Initialize X2SYS database in the X2SYS/ICESAT2 folder\n", + "tag = \"X2SYS\"\n", + "os.environ[\"X2SYS_HOME\"] = os.path.abspath(tag)\n", + "os.getcwd()\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": "code", + "execution_count": null, + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# Run crossover analysis on all tracks\n", + "rgts: list = [135, 327, 388, 577, 1080, 1272] # Whillans upstream\n", + "# rgts: list = [236, 501, 562, 1181] # Whillans_downstream\n", + "tracks = [f\"{tag}/track_{i}.tsv\" for i in rgts]\n", + "assert all(os.path.exists(k) for k in tracks)\n", + "\n", + "# Parallelized paired crossover analysis\n", + "futures: list = []\n", + "for track1, track2 in itertools.combinations(rgts, r=2):\n", + " future = client.submit(\n", + " key=f\"{track1}_{track2}\",\n", + " func=pygmt.x2sys_cross,\n", + " tracks=[f\"{tag}/track_{track1}.tsv\", f\"{tag}/track_{track2}.tsv\"],\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": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# 2D plot of crossover locations\n", + "var: str = \"h_X\"\n", + "fig = pygmt.Figure()\n", + "# Setup basemap\n", + "region = np.array([df.x.min(), df.x.max(), df.y.min(), df.y.max()])\n", + "buffer = np.array([-2000, +2000, -2000, +2000])\n", + "pygmt.makecpt(cmap=\"batlow\", series=[sumstats[var][\"25%\"], sumstats[var][\"75%\"]])\n", + "# Map frame in metre units\n", + "fig.basemap(frame=\"f\", region=region + buffer, projection=\"X8c\")\n", + "# Plot actual track points\n", + "for track in tracks:\n", + " fig.plot(data=track, color=\"green\", style=\"c0.01c\")\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", + "# Map frame in kilometre units\n", + "fig.basemap(\n", + " frame=[\n", + " \"WSne\",\n", + " 'xaf+l\"Polar Stereographic X (km)\"',\n", + " 'yaf+l\"Polar Stereographic Y (km)\"',\n", + " ],\n", + " region=(region + buffer) / 1000,\n", + " projection=\"X8c\",\n", + ")\n", + "fig.colorbar(position=\"JMR\", frame=['x+l\"Crossover Error\"', \"y+lm\"])\n", + "fig.savefig(\"figures/crossover_area.png\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1D plots of height changing over time\n", + "\n", + "Plot height 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[\"id\"] = df.index\n", + "df_th: pd.DataFrame = pd.wide_to_long(\n", + " df=df[[\"id\", \"track1_track2\", \"x\", \"y\", \"t_1\", \"t_2\", \"h_1\", \"h_2\"]],\n", + " stubnames=[\"t\", \"h\"],\n", + " i=\"id\",\n", + " j=\"track\",\n", + " sep=\"_\",\n", + ")\n", + "df_th = df_th.reset_index(level=\"track\").drop_duplicates(ignore_index=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 1D Plot at 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\").sort_values(by=\"t\")\n", + "track1, track2 = df_max.track1_track2.iloc[0].split(\"_\")\n", + "print(f\"{round(max_h_X.h_X, 2)} metres height change at {max_h_X.x}, {max_h_X.y}\")\n", + "t_min = (df_max.t.min() - pd.Timedelta(2, unit=\"W\")).isoformat()\n", + "t_max = (df_max.t.max() + pd.Timedelta(2, unit=\"W\")).isoformat()\n", + "h_min = df_max.h.min() - 0.2\n", + "h_max = df_max.h.max() + 0.4\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=[t_min, t_max, h_min, h_max],\n", + " frame=[\n", + " \"WSne\",\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", + " V=\"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(f\"figures/crossover_{track1}_{track2}_{min_date}_{max_date}.png\")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 1D plots of a crossover area, all the height points over time\n", + "t_min = (df_th.t.min() - pd.Timedelta(1, unit=\"W\")).isoformat()\n", + "t_max = (df_th.t.max() + pd.Timedelta(1, unit=\"W\")).isoformat()\n", + "h_min = df_th.h.min() - 0.2\n", + "h_max = df_th.h.max() + 0.2\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/12c\",\n", + " region=[t_min, t_max, h_min, h_max],\n", + " frame=[\n", + " \"WSne\",\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", + "\n", + "crossovers = df_th.groupby(by=[\"x\", \"y\"])\n", + "pygmt.makecpt(cmap=\"categorical\", series=[1, len(crossovers) + 1, 1])\n", + "for i, ((x_coord, y_coord), indexes) in enumerate(crossovers.indices.items()):\n", + " df_ = df_th.loc[indexes].sort_values(by=\"t\")\n", + " # if df_.h.max() - df_.h.min() > 1.0: # plot only > 1 metre height change\n", + " track1, track2 = df_.track1_track2.iloc[0].split(\"_\")\n", + " label = f'\"Track {track1} {track2}\"'\n", + " fig.plot(x=df_.t, y=df_.h, Z=i, style=\"c0.1c\", cmap=True, pen=\"thin+z\", label=label)\n", + " # Plot line connecting points\n", + " fig.plot(\n", + " x=df_.t, y=df_.h, Z=i, pen=f\"faint,+z,-\", cmap=True\n", + " ) # , label=f'\"+g-1l+s0.15c\"')\n", + "fig.legend(position=\"JMR+JMR+o0.2c\", box=\"+gwhite+p1p\")\n", + "fig.savefig(f\"figures/crossover_many_{min_date}_{max_date}.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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/atlxi_lake.py b/atlxi_lake.py new file mode 100644 index 0000000..18b4e79 --- /dev/null +++ b/atlxi_lake.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:hydrogen +# text_representation: +# extension: .py +# format_name: hydrogen +# format_version: '1.3' +# jupytext_version: 1.5.2 +# kernelspec: +# display_name: deepicedrain +# language: python +# name: deepicedrain +# --- + +# %% [markdown] +# # Crossover Track Analysis +# +# To increase the temporal resolution of +# our ice elevation change analysis +# (i.e. at time periods less than +# the 91 day repeat cycle of ICESat-2), +# we can look at the locations where the +# ICESat-2 tracks intersect and get the +# height values there! +# Uses [x2sys_cross](https://docs.generic-mapping-tools.org/6.1/supplements/x2sys/x2sys_cross). +# +# References: +# - Wessel, P. (2010). Tools for analyzing intersecting tracks: The x2sys package. +# Computers & Geosciences, 36(3), 348–354. https://doi.org/10.1016/j.cageo.2009.05.009 + + +# %% +# Initialize X2SYS database in the X2SYS/ICESAT2 folder +tag = "X2SYS" +os.environ["X2SYS_HOME"] = os.path.abspath(tag) +os.getcwd() +pygmt.x2sys_init( + tag="ICESAT2", + fmtfile=f"{tag}/ICESAT2/xyht", + suffix="tsv", + units=["de", "se"], # distance in metres, speed in metres per second + gap="d250e", # distance gap up to 250 metres allowed + force=True, + verbose="q", +) + +# %% +# Run crossover analysis on all tracks +rgts: list = [135, 327, 388, 577, 1080, 1272] # Whillans upstream +# rgts: list = [236, 501, 562, 1181] # Whillans_downstream +tracks = [f"{tag}/track_{i}.tsv" for i in rgts] +assert all(os.path.exists(k) for k in tracks) + +# Parallelized paired crossover analysis +futures: list = [] +for track1, track2 in itertools.combinations(rgts, r=2): + future = client.submit( + key=f"{track1}_{track2}", + func=pygmt.x2sys_cross, + tracks=[f"{tag}/track_{track1}.tsv", f"{tag}/track_{track2}.tsv"], + tag="ICESAT2", + region=[-460000, -400000, -560000, -500000], + interpolation="l", # linear interpolation + coe="e", # external crossovers + trackvalues=True, # Get track 1 height (h_1) and track 2 height (h_2) + # trackvalues=False, # Get crossover error (h_X) and mean height value (h_M) + # outfile="xover_236_562.tsv" + ) + futures.append(future) + + +# %% +crossovers: dict = {} +for f in tqdm.tqdm( + iterable=dask.distributed.as_completed(futures=futures), total=len(futures) +): + if f.status != "error": # skip those track pairs which don't intersect + crossovers[f.key] = f.result().dropna().reset_index(drop=True) + +df_cross: pd.DataFrame = pd.concat(objs=crossovers, names=["track1_track2", "id"]) +df: pd.DataFrame = df_cross.reset_index(level="track1_track2").reset_index(drop=True) +# Report on how many unique crossover intersections there were +# df.plot.scatter(x="x", y="y") # quick plot of our crossover points +print( + f"{len(df.groupby(by=['x', 'y']))} crossover intersection point locations found " + f"with {len(df)} crossover height-time pairs " + f"over {len(tracks)} tracks" +) + + +# %% +# Calculate crossover error +df["h_X"]: pd.Series = df.h_2 - df.h_1 # crossover error (i.e. height difference) +df["t_D"]: pd.Series = df.t_2 - df.t_1 # elapsed time in ns (i.e. time difference) +ns_in_yr: int = (365.25 * 24 * 60 * 60 * 1_000_000_000) # nanoseconds in a year +df["dhdt"]: pd.Series = df.h_X / (df.t_D.astype(np.int64) / ns_in_yr) + +# %% +# Get some summary statistics of our crossover errors +sumstats: pd.DataFrame = df[["h_X", "t_D", "dhdt"]].describe() +# Find location with highest absolute crossover error, and most sudden height change +max_h_X: pd.Series = df.iloc[np.nanargmax(df.h_X.abs())] # highest crossover error +max_dhdt: pd.Series = df.iloc[df.dhdt.argmax()] # most sudden change in height + + +# %% [markdown] +# ### 2D Map view of crossover points +# +# Bird's eye view of the crossover points +# overlaid on top of the ICESat-2 tracks. + +# %% +# 2D plot of crossover locations +var: str = "h_X" +fig = pygmt.Figure() +# Setup basemap +region = np.array([df.x.min(), df.x.max(), df.y.min(), df.y.max()]) +buffer = np.array([-2000, +2000, -2000, +2000]) +pygmt.makecpt(cmap="batlow", series=[sumstats[var]["25%"], sumstats[var]["75%"]]) +# Map frame in metre units +fig.basemap(frame="f", region=region + buffer, projection="X8c") +# Plot actual track points +for track in tracks: + fig.plot(data=track, color="green", style="c0.01c") +# Plot crossover point locations +fig.plot(x=df.x, y=df.y, color=df.h_X, cmap=True, style="c0.1c", pen="thinnest") +# Map frame in kilometre units +fig.basemap( + frame=[ + "WSne", + 'xaf+l"Polar Stereographic X (km)"', + 'yaf+l"Polar Stereographic Y (km)"', + ], + region=(region + buffer) / 1000, + projection="X8c", +) +fig.colorbar(position="JMR", frame=['x+l"Crossover Error"', "y+lm"]) +fig.savefig("figures/crossover_area.png") +fig.show() + + +# %% [markdown] +# ### 1D plots of height changing over time +# +# Plot height change over time at: +# +# 1. One single crossover point location +# 2. Many crossover locations over an area + +# %% +# Tidy up dataframe first using pd.wide_to_long +# I.e. convert 't_1', 't_2', 'h_1', 'h_2' columns into just 't' and 'h'. +df["id"] = df.index +df_th: pd.DataFrame = pd.wide_to_long( + df=df[["id", "track1_track2", "x", "y", "t_1", "t_2", "h_1", "h_2"]], + stubnames=["t", "h"], + i="id", + j="track", + sep="_", +) +df_th = df_th.reset_index(level="track").drop_duplicates(ignore_index=True) + +# %% +# 1D Plot at location with **maximum** absolute crossover height error (max_h_X) +df_max = df_th.query(expr="x == @max_h_X.x & y == @max_h_X.y").sort_values(by="t") +track1, track2 = df_max.track1_track2.iloc[0].split("_") +print(f"{round(max_h_X.h_X, 2)} metres height change at {max_h_X.x}, {max_h_X.y}") +t_min = (df_max.t.min() - pd.Timedelta(2, unit="W")).isoformat() +t_max = (df_max.t.max() + pd.Timedelta(2, unit="W")).isoformat() +h_min = df_max.h.min() - 0.2 +h_max = df_max.h.max() + 0.4 + +fig = pygmt.Figure() +with pygmt.config( + FONT_ANNOT_PRIMARY="9p", FORMAT_TIME_PRIMARY_MAP="abbreviated", FORMAT_DATE_MAP="o" +): + fig.basemap( + projection="X12c/8c", + region=[t_min, t_max, h_min, h_max], + frame=[ + "WSne", + "pxa1Of1o+lDate", # primary time axis, 1 mOnth annotation and minor axis + "sx1Y", # secondary time axis, 1 Year intervals + 'yaf+l"Elevation at crossover (m)"', + ], + ) +fig.text( + text=f"Track {track1} and {track2} crossover", + position="TC", + offset="jTC0c/0.2c", + V="q", +) +# Plot data points +fig.plot(x=df_max.t, y=df_max.h, style="c0.15c", color="darkblue", pen="thin") +# Plot dashed line connecting points +fig.plot(x=df_max.t, y=df_max.h, pen=f"faint,blue,-") +fig.savefig(f"figures/crossover_{track1}_{track2}_{min_date}_{max_date}.png") +fig.show() + +# %% +# 1D plots of a crossover area, all the height points over time +t_min = (df_th.t.min() - pd.Timedelta(1, unit="W")).isoformat() +t_max = (df_th.t.max() + pd.Timedelta(1, unit="W")).isoformat() +h_min = df_th.h.min() - 0.2 +h_max = df_th.h.max() + 0.2 + +fig = pygmt.Figure() +with pygmt.config( + FONT_ANNOT_PRIMARY="9p", FORMAT_TIME_PRIMARY_MAP="abbreviated", FORMAT_DATE_MAP="o" +): + fig.basemap( + projection="X12c/12c", + region=[t_min, t_max, h_min, h_max], + frame=[ + "WSne", + "pxa1Of1o+lDate", # primary time axis, 1 mOnth annotation and minor axis + "sx1Y", # secondary time axis, 1 Year intervals + 'yaf+l"Elevation at crossover (m)"', + ], + ) + +crossovers = df_th.groupby(by=["x", "y"]) +pygmt.makecpt(cmap="categorical", series=[1, len(crossovers) + 1, 1]) +for i, ((x_coord, y_coord), indexes) in enumerate(crossovers.indices.items()): + df_ = df_th.loc[indexes].sort_values(by="t") + # if df_.h.max() - df_.h.min() > 1.0: # plot only > 1 metre height change + track1, track2 = df_.track1_track2.iloc[0].split("_") + label = f'"Track {track1} {track2}"' + fig.plot(x=df_.t, y=df_.h, Z=i, style="c0.1c", cmap=True, pen="thin+z", label=label) + # Plot line connecting points + fig.plot( + x=df_.t, y=df_.h, Z=i, pen=f"faint,+z,-", cmap=True + ) # , label=f'"+g-1l+s0.15c"') +fig.legend(position="JMR+JMR+o0.2c", box="+gwhite+p1p") +fig.savefig(f"figures/crossover_many_{min_date}_{max_date}.png") +fig.show() + +# %%