Skip to content

Commit 3d1534e

Browse files
committed
🔀 Merge branch 'spatiotemporal_cube' (#180)
Closes #180 Spatiotemporal visualization of active subglacial lake surfaces over ICESat-2 cycles
2 parents a2ef431 + 38237d1 commit 3d1534e

File tree

7 files changed

+656
-62
lines changed

7 files changed

+656
-62
lines changed

atlxi_lake.ipynb

Lines changed: 265 additions & 32 deletions
Large diffs are not rendered by default.

atlxi_lake.py

Lines changed: 174 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@
4444
import dask
4545
import dask.array
4646
import geopandas as gpd
47+
import hvplot.xarray
4748
import numpy as np
4849
import pandas as pd
4950
import panel as pn
5051
import pygmt
5152
import scipy.spatial
5253
import shapely.geometry
5354
import tqdm
55+
import xarray as xr
5456
import zarr
5557

5658
import deepicedrain
@@ -339,15 +341,18 @@ def find_clusters(X: cudf.core.dataframe.DataFrame) -> cudf.core.series.Series:
339341

340342

341343
# %% [markdown]
342-
# # Select a lake to examine
344+
# # Select a subglacial lake to examine
343345

344346
# %%
345347
# Save or load dhdt data from Parquet file
346-
placename: str = "Recovery" # "Whillans"
347-
drainage_basins: gpd.GeoDataFrame = drainage_basins.set_index(keys="NAME")
348-
region: deepicedrain.Region = deepicedrain.Region.from_gdf(
349-
gdf=drainage_basins.loc[placename], name="Recovery Basin"
350-
)
348+
placename: str = "siple_coast" # "Recovery" # "Whillans"
349+
try:
350+
drainage_basins: gpd.GeoDataFrame = drainage_basins.set_index(keys="NAME")
351+
region: deepicedrain.Region = deepicedrain.Region.from_gdf(
352+
gdf=drainage_basins.loc[placename], name="Recovery Basin"
353+
)
354+
except KeyError:
355+
pass
351356
df_dhdt: cudf.DataFrame = cudf.read_parquet(
352357
f"ATLXI/df_dhdt_{placename.lower()}.parquet"
353358
)
@@ -356,29 +361,158 @@ def find_clusters(X: cudf.core.dataframe.DataFrame) -> cudf.core.series.Series:
356361
# %%
357362
# Antarctic subglacial lake polygons with EPSG:3031 coordinates
358363
antarctic_lakes: gpd.GeoDataFrame = gpd.read_file(
359-
filename="antarctic_subglacial_lakes.geojson"
364+
filename="antarctic_subglacial_lakes_3031.geojson"
360365
)
361366

362367
# %%
363368
# Choose one draining/filling lake
364-
draining: bool = False # False
365-
placename: str = "Slessor" # "Whillans"
369+
draining: bool = False
370+
placename: str = "Whillans" # "Slessor" # "Kamb" # "Mercer" #
366371
lakes: gpd.GeoDataFrame = antarctic_lakes.query(expr="basin_name == @placename")
367-
lake = lakes.loc[lakes.maxabsdhdt.idxmin() if draining else lakes.maxabsdhdt.idxmax()]
372+
lake = lakes.loc[lakes.inner_dhdt.idxmin() if draining else lakes.inner_dhdt.idxmax()]
373+
# lake = lakes.query(expr="inner_dhdt < 0" if draining else "inner_dhdt > 0").loc[63]
368374
lakedict = {
369-
76: "Subglacial Lake Conway", # draining lake
370-
78: "Whillans IX", # filling lake
371-
103: "Slessor 45", # draining lake
372-
108: "Slessor 23", # filling lake
375+
21: "Mercer 2b", # filling lake
376+
40: "Subglacial Lake Conway", # draining lake
377+
48: "Subglacial Lake Whillans", # filling lake
378+
50: "Whillans IX", # filling lake
379+
63: "Kamb 1", # filling lake
380+
65: "Kamb 12", # filling lake
381+
97: "MacAyeal 1", # draining lake
382+
109: "Slessor 45", # draining lake
383+
116: "Slessor 23", # filling lake
384+
153: "Recovery IX", # draining lake
385+
157: "Recovery 3", # filling lake
373386
}
374387
region = deepicedrain.Region.from_gdf(gdf=lake, name=lakedict[lake.name])
375388

389+
print(lake)
390+
lake.geometry
391+
376392
# %%
377393
# Subset data to lake of interest
378394
placename: str = region.name.lower().replace(" ", "_")
379395
df_lake: cudf.DataFrame = region.subset(data=df_dhdt)
380396

381397

398+
# %% [markdown]
399+
# ## Create an interpolated ice surface elevation grid for each ICESat-2 cycle
400+
401+
# %%
402+
# Generate gridded time-series of ice elevation over lake
403+
cycles: tuple = (3, 4, 5, 6, 7, 8)
404+
os.makedirs(name=f"figures/{placename}", exist_ok=True)
405+
ds_lake: xr.Dataset = deepicedrain.spatiotemporal_cube(
406+
table=df_lake.to_pandas(),
407+
placename=placename,
408+
cycles=cycles,
409+
folder=f"figures/{placename}",
410+
)
411+
ds_lake.to_netcdf(path=f"figures/{placename}/xyht_{placename}.nc", mode="w")
412+
413+
# %%
414+
# Get 3D grid_region (xmin/xmax/ymin/ymax/zmin/zmax),
415+
# and calculate normalized z-values as Elevation delta relative to Cycle 3
416+
grid_region = pygmt.info(table=df_lake[["x", "y"]].to_pandas(), spacing="s250")
417+
z_limits: tuple = (float(ds_lake.z.min()), float(ds_lake.z.max())) # original z limits
418+
grid_region: np.ndarray = np.append(arr=grid_region, values=z_limits)
419+
420+
ds_lake_norm: xr.Dataset = ds_lake - ds_lake.sel(cycle_number=3).z
421+
z_norm_limits: tuple = (float(ds_lake_norm.z.min()), float(ds_lake_norm.z.max()))
422+
grid_region_norm: np.ndarray = np.append(arr=grid_region[:4], values=z_norm_limits)
423+
424+
print(f"Elevation limits are: {z_limits}")
425+
426+
# %%
427+
# 3D plots of gridded ice surface elevation over time
428+
azimuth: float = 157.5 # 202.5 # 270
429+
elevation: float = 45 # 60
430+
for cycle in tqdm.tqdm(iterable=cycles):
431+
time_nsec: pd.Timestamp = df_lake[f"utc_time_{cycle}"].to_pandas().mean()
432+
time_sec: str = np.datetime_as_string(arr=time_nsec.to_datetime64(), unit="s")
433+
434+
grdview_kwargs = dict(
435+
cmap=True,
436+
zscale=0.1, # zscaling factor, default to 10x vertical exaggeration
437+
surftype="sim", # surface, image and mesh plot
438+
perspective=[azimuth, elevation], # perspective using azimuth/elevation
439+
# W="c0.05p,black,solid", # draw contours
440+
)
441+
442+
fig = pygmt.Figure()
443+
# grid = ds_lake.sel(cycle_number=cycle).z
444+
grid = f"figures/{placename}/h_corr_{placename}_cycle_{cycle}.nc"
445+
pygmt.makecpt(cmap="lapaz", series=z_limits)
446+
fig.grdview(
447+
grid=grid,
448+
projection="X10c",
449+
region=grid_region,
450+
shading=True,
451+
frame=[
452+
f'SWZ+t"{region.name}"', # plot South, West axes, and Z-axis
453+
'xaf+l"Polar Stereographic X (m)"', # add x-axis annotations and minor ticks
454+
'yaf+l"Polar Stereographic Y (m)"', # add y-axis annotations and minor ticks
455+
f'zaf+l"Elevation (m)"', # add z-axis annotations, minor ticks and axis label
456+
],
457+
**grdview_kwargs,
458+
)
459+
460+
# Plot lake boundary outline
461+
# TODO wait for plot3d to plot lake boundary points at correct height
462+
df = pd.DataFrame([region.bounds()]).values
463+
points = pd.DataFrame(
464+
data=[point for point in lake.geometry.exterior.coords], columns=("x", "y")
465+
)
466+
df_xyz = pygmt.grdtrack(points=points, grid=grid, newcolname="z")
467+
fig.plot(
468+
data=df_xyz.values,
469+
region=grid_region,
470+
pen="1.5p,yellow2",
471+
Jz=True, # zscale
472+
p=f"{azimuth}/{elevation}/{df_xyz.z.median()}", # perspective
473+
# label='"Subglacial Lake X"'
474+
)
475+
476+
# Plot normalized elevation change
477+
grid = ds_lake_norm.sel(cycle_number=cycle).z
478+
if cycle == 3:
479+
# add some tiny random noise to make plot work
480+
grid = grid + np.random.normal(scale=1e-8, size=grid.shape)
481+
pygmt.makecpt(cmap="vik", series=z_norm_limits)
482+
fig.grdview(
483+
grid=grid,
484+
region=grid_region_norm,
485+
frame=[
486+
f'SEZ2+t"Cycle {cycle} at {time_sec}"', # plot South, East axes, and Z-axis
487+
'xaf+l"Polar Stereographic X (m)"', # add x-axis annotations and minor ticks
488+
'yaf+l"Polar Stereographic Y (m)"', # add y-axis annotations and minor ticks
489+
f'zaf+l"Elev Change (m)"', # add z-axis annotations, minor ticks and axis label
490+
],
491+
X="10c", # xshift
492+
**grdview_kwargs,
493+
)
494+
495+
fig.savefig(f"figures/{placename}/dsm_{placename}_cycle_{cycle}.png")
496+
fig.show()
497+
498+
# %%
499+
# Make a animated GIF of changing ice surface from the PNG files
500+
gif_fname: str = (
501+
f"figures/{placename}/dsm_{placename}_cycles_{cycles[0]}-{cycles[-1]}.gif"
502+
)
503+
# !convert -delay 120 -loop 0 figures/{placename}/dsm_*.png {gif_fname}
504+
505+
# %%
506+
# HvPlot 2D interactive view of ice surface elevation grids over each ICESat-2 cycle
507+
dashboard: pn.layout.Column = pn.Column(
508+
ds_lake.hvplot.image(x="x", y="y", clim=z_limits, cmap="gist_earth", data_aspect=1)
509+
# * ds_lake.hvplot.contour(x="x", y="y", clim=z_limits, data_aspect=1)
510+
)
511+
dashboard.show(port=30227)
512+
513+
# %% [markdown]
514+
# ## Along track plots of ice surface elevation change over time
515+
382516
# %%
383517
# Select a few Reference Ground tracks to look at
384518
rgts: list = [int(rgt) for rgt in lake.refgtracks.split("|")]
@@ -445,6 +579,10 @@ def find_clusters(X: cudf.core.dataframe.DataFrame) -> cudf.core.series.Series:
445579
# Parallelized paired crossover analysis
446580
futures: list = []
447581
for rgt1, rgt2 in itertools.combinations(rgts, r=2):
582+
# skip if same referencegroundtrack but different laser pair
583+
# as they are parallel and won't cross
584+
if rgt1[:4] == rgt2[:4]:
585+
continue
448586
track1 = track_dict[rgt1][["x", "y", "h_corr", "utc_time"]]
449587
track2 = track_dict[rgt2][["x", "y", "h_corr", "utc_time"]]
450588
future = client.submit(
@@ -511,14 +649,20 @@ def find_clusters(X: cudf.core.dataframe.DataFrame) -> cudf.core.series.Series:
511649
pygmt.makecpt(cmap="batlow", series=[sumstats[var]["25%"], sumstats[var]["75%"]])
512650
# Map frame in metre units
513651
fig.basemap(frame="f", region=plotregion, projection="X8c")
514-
# Plot actual track points
652+
# Plot actual track points in green
515653
for track in tracks:
516-
fig.plot(x=track.x, y=track.y, color="green", style="c0.01c")
654+
tracklabel = f"{track.iloc[0].referencegroundtrack} {track.iloc[0].pairtrack}"
655+
fig.plot(
656+
x=track.x,
657+
y=track.y,
658+
pen="thinnest,green,.",
659+
style=f'qN+1:+l"{tracklabel}"+f3p,Helvetica,darkgreen',
660+
)
517661
# Plot crossover point locations
518662
fig.plot(x=df.x, y=df.y, color=df.h_X, cmap=True, style="c0.1c", pen="thinnest")
519-
# PLot lake boundary
663+
# Plot lake boundary in blue
520664
lakex, lakey = lake.geometry.exterior.coords.xy
521-
fig.plot(x=lakex, y=lakey, pen="thin")
665+
fig.plot(x=lakex, y=lakey, pen="thin,blue,-.")
522666
# Map frame in kilometre units
523667
fig.basemap(
524668
frame=[
@@ -530,7 +674,7 @@ def find_clusters(X: cudf.core.dataframe.DataFrame) -> cudf.core.series.Series:
530674
projection="X8c",
531675
)
532676
fig.colorbar(position="JMR", frame=['x+l"Crossover Error"', "y+lm"])
533-
fig.savefig(f"figures/crossover_area_{placename}.png")
677+
fig.savefig(f"figures/{placename}/crossover_area_{placename}_{min_date}_{max_date}.png")
534678
fig.show()
535679

536680

@@ -587,14 +731,14 @@ def find_clusters(X: cudf.core.dataframe.DataFrame) -> cudf.core.series.Series:
587731
# Plot dashed line connecting points
588732
fig.plot(x=df_max.t, y=df_max.h, pen=f"faint,blue,-")
589733
fig.savefig(
590-
f"figures/crossover_point_{placename}_{track1}_{track2}_{min_date}_{max_date}.png"
734+
f"figures/{placename}/crossover_point_{placename}_{track1}_{track2}_{min_date}_{max_date}.png"
591735
)
592736
fig.show()
593737

594738
# %%
595739
# Plot all crossover height points over time over the lake area
596-
fig = deepicedrain.vizplots.plot_crossovers(df=df_th, regionname=region.name)
597-
fig.savefig(f"figures/crossover_many_{placename}_{min_date}_{max_date}.png")
740+
fig = deepicedrain.plot_crossovers(df=df_th, regionname=region.name)
741+
fig.savefig(f"figures/{placename}/crossover_many_{placename}_{min_date}_{max_date}.png")
598742
fig.show()
599743

600744
# %%
@@ -603,10 +747,15 @@ def find_clusters(X: cudf.core.dataframe.DataFrame) -> cudf.core.series.Series:
603747
normfunc = lambda h: h - h.iloc[0] # lambda h: h - h.mean()
604748
df_th["h_norm"] = df_th.groupby(by="track1_track2").h.transform(func=normfunc)
605749

606-
fig = deepicedrain.vizplots.plot_crossovers(
607-
df=df_th, regionname=region.name, elev_var="h_norm"
750+
fig = deepicedrain.plot_crossovers(
751+
df=df_th,
752+
regionname=region.name,
753+
elev_var="h_norm",
754+
elev_filter=3 * abs(df.h_X).median(),
755+
)
756+
fig.savefig(
757+
f"figures/{placename}/crossover_many_normalized_{placename}_{min_date}_{max_date}.png"
608758
)
609-
fig.savefig(f"figures/crossover_many_normalized_{placename}_{min_date}_{max_date}.png")
610759
fig.show()
611760

612761
# %%

deepicedrain/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Contents:
1717
- Region - Bounding box data class structure that has xarray subsetting capabilities and more!
1818
- deltatime_to_utctime - Converts GPS time from an epoch (default is 2018 Jan 1st) to UTC time
1919
- lonlat_to_xy - Reprojects longitude/latitude EPSG:4326 coordinates to x/y EPSG:3031 coordinates
20+
- spatiotemporal_cube - Interpolates a time-series point cloud into an xarray.Dataset data cube
2021

2122
- :card_file_box: extraload.py - Convenience functions for extracting, transforming and loading data
2223
- array_to_dataframe - Turns a 1D/2D numpy/dask array into a tidy pandas/dask dataframe table

deepicedrain/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
deltatime_to_utctime,
1212
lonlat_to_xy,
1313
point_in_polygon_gpu,
14+
spatiotemporal_cube,
1415
)
1516
from deepicedrain.vizplots import IceSat2Explorer, plot_alongtrack, plot_crossovers
1617

0 commit comments

Comments
 (0)