Skip to content

Commit 7683721

Browse files
committed
🔀 Merge branch 'ice_volume_displacement' (#216)
Closes #216 Calculate rolling time-series of ice volume displacement over subglacial lakes.
2 parents ab2ba20 + 1872ad0 commit 7683721

File tree

12 files changed

+608
-110
lines changed

12 files changed

+608
-110
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,4 @@ jobs:
6969
run: poetry run black . --check
7070

7171
- name: Test with pytest
72-
run: poetry run pytest --verbose deepicedrain/
72+
run: poetry run pytest --verbose --doctest-modules deepicedrain/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ in Antarctica using remote sensing and machine learning.
1818

1919
| Along track view of an ATL11 Ground Track | Elevation time-series at Crossover Points |
2020
|---|---|
21-
| ![alongtrack_whillans_ix_1080_pt3](https://user-images.githubusercontent.com/23487320/102156291-2210f600-3ee2-11eb-8175-e854b70444df.png) | ![crossover_many_normalized_whillans_ix_2018-10-14_2020-09-30](https://user-images.githubusercontent.com/23487320/102156396-5dabc000-3ee2-11eb-9e42-37463f81058d.png) |
21+
| ![alongtrack_whillans_ix_1080_pt3](https://user-images.githubusercontent.com/23487320/102156291-2210f600-3ee2-11eb-8175-e854b70444df.png) | ![crossover_anomaly_whillans_ix_2018-10-14_2020-09-30](https://user-images.githubusercontent.com/23487320/102610765-9bcf0b00-4192-11eb-803b-247ed960f9bb.png) |
2222

2323

2424

atlxi_xover.ipynb

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@
3636
"import geopandas as gpd\n",
3737
"import numpy as np\n",
3838
"import pandas as pd\n",
39+
"import pint\n",
40+
"import pint_pandas\n",
3941
"import pygmt\n",
4042
"import shapely.geometry\n",
41-
"import tqdm"
43+
"import tqdm\n",
44+
"import uncertainties"
4245
]
4346
},
4447
{
@@ -49,6 +52,8 @@
4952
},
5053
"outputs": [],
5154
"source": [
55+
"ureg = pint.UnitRegistry()\n",
56+
"pint_pandas.PintType.ureg = ureg\n",
5257
"tag: str = \"X2SYS\"\n",
5358
"os.environ[\"X2SYS_HOME\"] = os.path.abspath(tag)\n",
5459
"client = dask.distributed.Client(\n",
@@ -111,7 +116,7 @@
111116
"outputs": [],
112117
"source": [
113118
"# Choose one Antarctic active subglacial lake polygon with EPSG:3031 coordinates\n",
114-
"lake_name: str = \"Subglacial Lake Conway\"\n",
119+
"lake_name: str = \"Whillans 12\"\n",
115120
"lake_catalog = deepicedrain.catalog.subglacial_lakes()\n",
116121
"lake_ids: list = (\n",
117122
" pd.json_normalize(lake_catalog.metadata[\"lakedict\"])\n",
@@ -142,7 +147,11 @@
142147
"source": [
143148
"# Subset data to lake of interest\n",
144149
"placename: str = region.name.lower().replace(\" \", \"_\")\n",
145-
"df_lake: pd.DataFrame = region.subset(data=df_dhdt)"
150+
"df_lake: cudf.DataFrame = region.subset(data=df_dhdt) # bbox subset\n",
151+
"gdf_lake = gpd.GeoDataFrame(\n",
152+
" df_lake, geometry=gpd.points_from_xy(x=df_lake.x, y=df_lake.y, crs=3031)\n",
153+
")\n",
154+
"df_lake: pd.DataFrame = df_lake.loc[gdf_lake.within(lake.geometry)] # polygon subset"
146155
]
147156
},
148157
{
@@ -251,9 +260,7 @@
251260
{
252261
"cell_type": "code",
253262
"execution_count": null,
254-
"metadata": {
255-
"lines_to_next_cell": 2
256-
},
263+
"metadata": {},
257264
"outputs": [],
258265
"source": [
259266
"# 2D plot of crossover locations\n",
@@ -318,7 +325,8 @@
318325
" stubnames=[\"t\", \"h\"],\n",
319326
" j=\"track\",\n",
320327
")\n",
321-
"df_th = df_th.drop_duplicates(ignore_index=True)"
328+
"df_th: pd.DataFrame = df_th.drop_duplicates(ignore_index=True)\n",
329+
"df_th: pd.DataFrame = df_th.sort_values(by=\"t\").reset_index(drop=True)"
322330
]
323331
},
324332
{
@@ -328,9 +336,9 @@
328336
"outputs": [],
329337
"source": [
330338
"# Plot at single location with **maximum** absolute crossover height error (max_h_X)\n",
331-
"df_max = df_th.query(expr=\"x == @max_h_X.x & y == @max_h_X.y\").sort_values(by=\"t\")\n",
339+
"df_max = df_th.query(expr=\"x == @max_h_X.x & y == @max_h_X.y\")\n",
332340
"track1, track2 = df_max.track1_track2.iloc[0].split(\"x\")\n",
333-
"print(f\"{round(max_h_X.h_X, 2)} metres height change at {max_h_X.x}, {max_h_X.y}\")\n",
341+
"print(f\"{max_h_X.h_X:.2f} metres height change at {max_h_X.x}, {max_h_X.y}\")\n",
334342
"plotregion = np.array(\n",
335343
" [df_max.t.min(), df_max.t.max(), *pygmt.info(table=df_max[[\"h\"]], spacing=2.5)[:2]]\n",
336344
")\n",
@@ -384,20 +392,95 @@
384392
"metadata": {},
385393
"outputs": [],
386394
"source": [
387-
"# Plot all crossover height points over time over the lake area\n",
388-
"# with height values normalized to 0 from the first observation date\n",
389-
"normfunc = lambda h: h - h.iloc[0] # lambda h: h - h.mean()\n",
390-
"df_th[\"h_norm\"] = df_th.groupby(by=\"track1_track2\").h.transform(func=normfunc)\n",
391-
"\n",
395+
"# Calculate height anomaly at crossover point as\n",
396+
"# height at t=n minus height at t=0 (first observation date at crossover point)\n",
397+
"anomfunc = lambda h: h - h.iloc[0] # lambda h: h - h.mean()\n",
398+
"df_th[\"h_anom\"] = df_th.groupby(by=\"track1_track2\").h.transform(func=anomfunc)\n",
399+
"# Calculate ice volume displacement (dvol) in unit metres^3\n",
400+
"# and rolling mean height anomaly (h_roll) in unit metres\n",
401+
"surface_area: pint.Quantity = lake.geometry.area * ureg.metre ** 2\n",
402+
"ice_dvol: pd.Series = deepicedrain.ice_volume_over_time(\n",
403+
" df_elev=df_th.astype(dtype={\"h_anom\": \"pint[metre]\"}),\n",
404+
" surface_area=surface_area,\n",
405+
" time_col=\"t\",\n",
406+
" outfile=f\"figures/{placename}/ice_dvol_dt_{placename}.txt\",\n",
407+
")\n",
408+
"df_th[\"h_roll\"]: pd.Series = uncertainties.unumpy.nominal_values(\n",
409+
" arr=ice_dvol.pint.magnitude / surface_area.magnitude\n",
410+
")"
411+
]
412+
},
413+
{
414+
"cell_type": "code",
415+
"execution_count": null,
416+
"metadata": {},
417+
"outputs": [],
418+
"source": [
419+
"# Plot all crossover height point anomalies over time over the lake area\n",
392420
"fig = deepicedrain.plot_crossovers(\n",
393421
" df=df_th,\n",
394422
" regionname=region.name,\n",
395-
" elev_var=\"h_norm\",\n",
423+
" elev_var=\"h_anom\",\n",
396424
" outline_points=f\"figures/{placename}/{placename}.gmt\",\n",
397425
")\n",
426+
"fig.plot(x=df_th.t, y=df_th.h_roll, pen=\"thick,-\") # plot rolling mean height anomaly\n",
398427
"fig.savefig(\n",
399-
" f\"figures/{placename}/crossover_many_normalized_{placename}_{min_date}_{max_date}.png\"\n",
428+
" f\"figures/{placename}/crossover_anomaly_{placename}_{min_date}_{max_date}.png\"\n",
429+
")\n",
430+
"fig.show()"
431+
]
432+
},
433+
{
434+
"cell_type": "code",
435+
"execution_count": null,
436+
"metadata": {},
437+
"outputs": [],
438+
"source": []
439+
},
440+
{
441+
"cell_type": "markdown",
442+
"metadata": {},
443+
"source": [
444+
"## Combined ice volume displacement plot\n",
445+
"\n",
446+
"Showing how subglacial water cascades down a drainage basin!"
447+
]
448+
},
449+
{
450+
"cell_type": "code",
451+
"execution_count": null,
452+
"metadata": {
453+
"lines_to_next_cell": 0
454+
},
455+
"outputs": [],
456+
"source": [
457+
"fig = pygmt.Figure()\n",
458+
"fig.basemap(\n",
459+
" region=f\"2019-02-28/2020-09-30/-0.3/0.5\",\n",
460+
" frame=[\"wSnE\", \"xaf\", 'yaf+l\"Ice Volume Displacement (km@+3@+)\"'],\n",
461+
")\n",
462+
"pygmt.makecpt(cmap=\"davosS\", color_model=\"+c\", series=(-2, 4, 0.5))\n",
463+
"for i, (_placename, linestyle) in enumerate(\n",
464+
" iterable=zip(\n",
465+
" [\"whillans_ix\", \"subglacial_lake_whillans\", \"whillans_12\", \"whillans_7\"],\n",
466+
" [\"\", \".-\", \"-\", \"..-\"],\n",
467+
" )\n",
468+
"):\n",
469+
" fig.plot(\n",
470+
" data=f\"figures/{_placename}/ice_dvol_dt_{_placename}.txt\",\n",
471+
" cmap=True,\n",
472+
" pen=f\"thick,{linestyle}\",\n",
473+
" zvalue=i,\n",
474+
" label=_placename,\n",
475+
" columns=\"0,3\", # time column (0), ice_dvol column (3)\n",
476+
" )\n",
477+
"fig.text(\n",
478+
" position=\"TL\",\n",
479+
" offset=\"j0.2c\",\n",
480+
" text=\"Whillans Ice Stream Central Catchment active subglacial lakes\",\n",
400481
")\n",
482+
"fig.legend(position=\"jML+jML+o0.2c\", box=\"+gwhite\")\n",
483+
"fig.savefig(\"figures/cascade_whillans_ice_stream.png\")\n",
401484
"fig.show()"
402485
]
403486
},

atlxi_xover.py

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,16 @@
4040
import geopandas as gpd
4141
import numpy as np
4242
import pandas as pd
43+
import pint
44+
import pint_pandas
4345
import pygmt
4446
import shapely.geometry
4547
import tqdm
48+
import uncertainties
4649

4750
# %%
51+
ureg = pint.UnitRegistry()
52+
pint_pandas.PintType.ureg = ureg
4853
tag: str = "X2SYS"
4954
os.environ["X2SYS_HOME"] = os.path.abspath(tag)
5055
client = dask.distributed.Client(
@@ -79,7 +84,7 @@
7984

8085
# %%
8186
# Choose one Antarctic active subglacial lake polygon with EPSG:3031 coordinates
82-
lake_name: str = "Subglacial Lake Conway"
87+
lake_name: str = "Whillans 12"
8388
lake_catalog = deepicedrain.catalog.subglacial_lakes()
8489
lake_ids: list = (
8590
pd.json_normalize(lake_catalog.metadata["lakedict"])
@@ -102,7 +107,11 @@
102107
# %%
103108
# Subset data to lake of interest
104109
placename: str = region.name.lower().replace(" ", "_")
105-
df_lake: pd.DataFrame = region.subset(data=df_dhdt)
110+
df_lake: cudf.DataFrame = region.subset(data=df_dhdt) # bbox subset
111+
gdf_lake = gpd.GeoDataFrame(
112+
df_lake, geometry=gpd.points_from_xy(x=df_lake.x, y=df_lake.y, crs=3031)
113+
)
114+
df_lake: pd.DataFrame = df_lake.loc[gdf_lake.within(lake.geometry)] # polygon subset
106115

107116

108117
# %%
@@ -214,7 +223,6 @@
214223
fig.savefig(f"figures/{placename}/crossover_area_{placename}_{min_date}_{max_date}.png")
215224
fig.show()
216225

217-
218226
# %% [markdown]
219227
# ### Plot Crossover Elevation time-series
220228
#
@@ -231,13 +239,14 @@
231239
stubnames=["t", "h"],
232240
j="track",
233241
)
234-
df_th = df_th.drop_duplicates(ignore_index=True)
242+
df_th: pd.DataFrame = df_th.drop_duplicates(ignore_index=True)
243+
df_th: pd.DataFrame = df_th.sort_values(by="t").reset_index(drop=True)
235244

236245
# %%
237246
# Plot at single location with **maximum** absolute crossover height error (max_h_X)
238-
df_max = df_th.query(expr="x == @max_h_X.x & y == @max_h_X.y").sort_values(by="t")
247+
df_max = df_th.query(expr="x == @max_h_X.x & y == @max_h_X.y")
239248
track1, track2 = df_max.track1_track2.iloc[0].split("x")
240-
print(f"{round(max_h_X.h_X, 2)} metres height change at {max_h_X.x}, {max_h_X.y}")
249+
print(f"{max_h_X.h_X:.2f} metres height change at {max_h_X.x}, {max_h_X.y}")
241250
plotregion = np.array(
242251
[df_max.t.min(), df_max.t.max(), *pygmt.info(table=df_max[["h"]], spacing=2.5)[:2]]
243252
)
@@ -279,20 +288,72 @@
279288
fig.show()
280289

281290
# %%
282-
# Plot all crossover height points over time over the lake area
283-
# with height values normalized to 0 from the first observation date
284-
normfunc = lambda h: h - h.iloc[0] # lambda h: h - h.mean()
285-
df_th["h_norm"] = df_th.groupby(by="track1_track2").h.transform(func=normfunc)
291+
# Calculate height anomaly at crossover point as
292+
# height at t=n minus height at t=0 (first observation date at crossover point)
293+
anomfunc = lambda h: h - h.iloc[0] # lambda h: h - h.mean()
294+
df_th["h_anom"] = df_th.groupby(by="track1_track2").h.transform(func=anomfunc)
295+
# Calculate ice volume displacement (dvol) in unit metres^3
296+
# and rolling mean height anomaly (h_roll) in unit metres
297+
surface_area: pint.Quantity = lake.geometry.area * ureg.metre ** 2
298+
ice_dvol: pd.Series = deepicedrain.ice_volume_over_time(
299+
df_elev=df_th.astype(dtype={"h_anom": "pint[metre]"}),
300+
surface_area=surface_area,
301+
time_col="t",
302+
outfile=f"figures/{placename}/ice_dvol_dt_{placename}.txt",
303+
)
304+
df_th["h_roll"]: pd.Series = uncertainties.unumpy.nominal_values(
305+
arr=ice_dvol.pint.magnitude / surface_area.magnitude
306+
)
286307

308+
# %%
309+
# Plot all crossover height point anomalies over time over the lake area
287310
fig = deepicedrain.plot_crossovers(
288311
df=df_th,
289312
regionname=region.name,
290-
elev_var="h_norm",
313+
elev_var="h_anom",
291314
outline_points=f"figures/{placename}/{placename}.gmt",
292315
)
316+
fig.plot(x=df_th.t, y=df_th.h_roll, pen="thick,-") # plot rolling mean height anomaly
293317
fig.savefig(
294-
f"figures/{placename}/crossover_many_normalized_{placename}_{min_date}_{max_date}.png"
318+
f"figures/{placename}/crossover_anomaly_{placename}_{min_date}_{max_date}.png"
319+
)
320+
fig.show()
321+
322+
# %%
323+
324+
# %% [markdown]
325+
# ## Combined ice volume displacement plot
326+
#
327+
# Showing how subglacial water cascades down a drainage basin!
328+
329+
# %%
330+
fig = pygmt.Figure()
331+
fig.basemap(
332+
region=f"2019-02-28/2020-09-30/-0.3/0.5",
333+
frame=["wSnE", "xaf", 'yaf+l"Ice Volume Displacement (km@+3@+)"'],
334+
)
335+
pygmt.makecpt(cmap="davosS", color_model="+c", series=(-2, 4, 0.5))
336+
for i, (_placename, linestyle) in enumerate(
337+
iterable=zip(
338+
["whillans_ix", "subglacial_lake_whillans", "whillans_12", "whillans_7"],
339+
["", ".-", "-", "..-"],
340+
)
341+
):
342+
fig.plot(
343+
data=f"figures/{_placename}/ice_dvol_dt_{_placename}.txt",
344+
cmap=True,
345+
pen=f"thick,{linestyle}",
346+
zvalue=i,
347+
label=_placename,
348+
columns="0,3", # time column (0), ice_dvol column (3)
349+
)
350+
fig.text(
351+
position="TL",
352+
offset="j0.2c",
353+
text="Whillans Ice Stream Central Catchment active subglacial lakes",
295354
)
355+
fig.legend(position="jML+jML+o0.2c", box="+gwhite")
356+
fig.savefig("figures/cascade_whillans_ice_stream.png")
296357
fig.show()
297358

298359
# %%

deepicedrain/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Contents:
2929

3030
- :droplet: lakealgorithms.py - Custom algorithms for detecting and filtering active subglacial lakes
3131
- find_clusters - Density based clustering algorithm (DBSCAN) to group points into lakes
32+
- ice_volume_over_time - Calculate a rolling mean time-series of ice volume displacement over a lake area
3233

3334
- :world_map: vizplots.py - Makes interactive dashboard plots and publication quality figures
3435
- IceSat2Explorer - Dashboard for interacting with ICESat-2 point clouds on a 2D map

deepicedrain/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
split_tracks,
1212
wide_to_long,
1313
)
14-
from deepicedrain.lake_algorithms import find_clusters
14+
from deepicedrain.lake_algorithms import find_clusters, ice_volume_over_time
1515
from deepicedrain.spatiotemporal import (
1616
Region,
1717
deltatime_to_utctime,

0 commit comments

Comments
 (0)