diff --git a/.gitignore b/.gitignore index e19cfc4..789d24f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ ATL11.001 ATL11.001z123 ATLXI Quantarctica3 + +# Subglacial Lake grid files and figures +figures/**/*.gif +figures/**/*.nc +figures/**/*.png diff --git a/atlxi_lake.ipynb b/atlxi_lake.ipynb index d990b28..0391236 100644 --- a/atlxi_lake.ipynb +++ b/atlxi_lake.ipynb @@ -922,7 +922,7 @@ "outputs": [], "source": [ "# Save or load dhdt data from Parquet file\n", - "placename: str = \"siple_coast\" # \"Recovery\" # \"Whillans\"\n", + "placename: str = \"siple_coast\" # \"slessor_downstream\" # \"Recovery\" # \"Whillans\"\n", "try:\n", " drainage_basins: gpd.GeoDataFrame = drainage_basins.set_index(keys=\"NAME\")\n", " region: deepicedrain.Region = deepicedrain.Region.from_gdf(\n", @@ -942,7 +942,8 @@ "outputs": [], "source": [ "# Antarctic subglacial lake polygons with EPSG:3031 coordinates\n", - "antarctic_lakes: gpd.GeoDataFrame = deepicedrain.catalog.subglacial_lakes.read()" + "antarctic_lakes: gpd.GeoDataFrame = deepicedrain.catalog.subglacial_lakes.read()\n", + "antarctic_lakes = antarctic_lakes.set_crs(epsg=3031, allow_override=True)" ] }, { @@ -986,24 +987,30 @@ "# Choose one draining/filling lake\n", "draining: bool = False\n", "placename: str = \"Whillans\" # \"Slessor\" # \"Kamb\" # \"Mercer\" #\n", - "lakes: gpd.GeoDataFrame = antarctic_lakes.query(expr=\"basin_name == @placename\")\n", - "lake = lakes.loc[lakes.inner_dhdt.idxmin() if draining else lakes.inner_dhdt.idxmax()]\n", - "lake = lakes.query(expr=\"inner_dhdt < 0\" if draining else \"inner_dhdt > 0\").loc[48]\n", + "lakes: gpd.GeoDataFrame = antarctic_lakes # .query(expr=\"basin_name == @placename\")\n", + "\n", + "lake_ids: int = (44,) # single lake\n", + "lake_ids: tuple = (41, 43, 45) # lake mega-cluster\n", + "# TODO handle Lake 78 cross-basin by using dissolve(by=None) available\n", + "# in geopandas v0.9.0 https://github.com/geopandas/geopandas/pull/1568\n", + "lake = lakes.loc[list(lake_ids)].dissolve(by=\"basin_name\", as_index=False).squeeze()\n", "lakedict = {\n", - " 21: \"Mercer 2b\", # filling lake\n", - " 40: \"Lower Subglacial Lake Conway\", # draining lake\n", - " 41: \"Subglacial Lake Conway\", # draining lake\n", - " 48: \"Subglacial Lake Whillans\", # filling lake\n", - " 50: \"Whillans IX\", # filling lake\n", - " 63: \"Kamb 1\", # filling lake\n", - " 65: \"Kamb 12\", # filling lake\n", - " 97: \"MacAyeal 1\", # draining lake\n", - " 109: \"Slessor 45\", # draining lake\n", - " 116: \"Slessor 23\", # filling lake\n", - " 151: \"Recovery IV\", # draining lake\n", - " 156: \"Recovery 2\", # filling lake\n", + " (15, 19): \"Subglacial Lake Mercer\", # filling lake\n", + " (32,): \"Whillans 7\", # draining lake\n", + " (34, 35): \"Subglacial Lake Conway\", # draining lake\n", + " (41, 43, 45): \"Subglacial Lake Whillans\", # filling lake\n", + " (16, 46, 48): \"Lake 78\", # filling lake\n", + " (44,): \"Whillans IX\", # filling lake\n", + " (62,): \"Kamb 1\", # filling lake\n", + " # (65): \"Kamb 12\", # filling lake\n", + " (84,): \"MacAyeal 1\", # draining lake\n", + " (95,): \"Slessor 45\", # draining lake\n", + " (101,): \"Slessor 23\", # filling lake\n", + " (141,): \"Recovery IV\", # draining lake\n", + " (143, 144): \"Recovery 2\", # filling lake\n", "}\n", - "region = deepicedrain.Region.from_gdf(gdf=lake, name=lakedict[lake.name])\n", + "region = deepicedrain.Region.from_gdf(gdf=lake, name=lakedict[lake_ids])\n", + "assert (lake.inner_dhdt < 0 and draining) or (lake.inner_dhdt > 0 and not draining)\n", "\n", "print(lake)\n", "lake.geometry" @@ -1019,7 +1026,12 @@ "source": [ "# Subset data to lake of interest\n", "placename: str = region.name.lower().replace(\" \", \"_\")\n", - "df_lake: cudf.DataFrame = region.subset(data=df_dhdt)" + "df_lake: cudf.DataFrame = region.subset(data=df_dhdt)\n", + "\n", + "# Save lake outline to OGR GMT file format\n", + "outline_points: str = f\"figures/{placename}/{placename}.gmt\"\n", + "if not os.path.exists(path=outline_points):\n", + " lakes.loc[list(lake_ids)].to_file(filename=outline_points, driver=\"OGR_GMT\")" ] }, { @@ -1044,7 +1056,7 @@ ], "source": [ "# Generate gridded time-series of ice elevation over lake\n", - "cycles: tuple = (3, 4, 5, 6, 7, 8)\n", + "cycles: tuple = (3, 4, 5, 6, 7, 8, 9)\n", "os.makedirs(name=f\"figures/{placename}\", exist_ok=True)\n", "ds_lake: xr.Dataset = deepicedrain.spatiotemporal_cube(\n", " table=df_lake.to_pandas(),\n", @@ -1092,15 +1104,9 @@ " time_nsec: pd.Timestamp = df_lake[f\"utc_time_{cycle}\"].to_pandas().mean()\n", " time_sec: str = np.datetime_as_string(arr=time_nsec.to_datetime64(), unit=\"s\")\n", "\n", - " grid = f\"figures/{placename}/h_corr_{placename}_cycle_{cycle}.nc\"\n", - " points = pd.DataFrame(\n", - " data=np.vstack(lake.geometry.boundary.coords.xy).T, columns=(\"x\", \"y\")\n", - " )\n", - " outline_points = pygmt.grdtrack(points=points, grid=grid, newcolname=\"z\")\n", - " outline_points[\"z\"] = outline_points.z.fillna(value=outline_points.z.median())\n", - "\n", + " # grid = ds_lake.sel(cycle_number=cycle).z\n", " fig = deepicedrain.plot_icesurface(\n", - " grid=grid, # ds_lake.sel(cycle_number=cycle).z\n", + " grid=f\"figures/{placename}/h_corr_{placename}_cycle_{cycle}.nc\",\n", " grid_region=grid_region,\n", " diff_grid=ds_lake_diff.sel(cycle_number=cycle).z,\n", " diff_grid_region=diff_grid_region,\n", diff --git a/atlxi_lake.py b/atlxi_lake.py index d83e343..8cf81fe 100644 --- a/atlxi_lake.py +++ b/atlxi_lake.py @@ -331,7 +331,7 @@ # %% # Save or load dhdt data from Parquet file -placename: str = "siple_coast" # "Recovery" # "Whillans" +placename: str = "siple_coast" # "slessor_downstream" # "Recovery" # "Whillans" try: drainage_basins: gpd.GeoDataFrame = drainage_basins.set_index(keys="NAME") region: deepicedrain.Region = deepicedrain.Region.from_gdf( @@ -347,29 +347,36 @@ # %% # Antarctic subglacial lake polygons with EPSG:3031 coordinates antarctic_lakes: gpd.GeoDataFrame = deepicedrain.catalog.subglacial_lakes.read() +antarctic_lakes = antarctic_lakes.set_crs(epsg=3031, allow_override=True) # %% # Choose one draining/filling lake draining: bool = False placename: str = "Whillans" # "Slessor" # "Kamb" # "Mercer" # -lakes: gpd.GeoDataFrame = antarctic_lakes.query(expr="basin_name == @placename") -lake = lakes.loc[lakes.inner_dhdt.idxmin() if draining else lakes.inner_dhdt.idxmax()] -lake = lakes.query(expr="inner_dhdt < 0" if draining else "inner_dhdt > 0").loc[48] +lakes: gpd.GeoDataFrame = antarctic_lakes # .query(expr="basin_name == @placename") + +lake_ids: int = (44,) # single lake +lake_ids: tuple = (41, 43, 45) # lake mega-cluster +# TODO handle Lake 78 cross-basin by using dissolve(by=None) available +# in geopandas v0.9.0 https://github.com/geopandas/geopandas/pull/1568 +lake = lakes.loc[list(lake_ids)].dissolve(by="basin_name", as_index=False).squeeze() lakedict = { - 21: "Mercer 2b", # filling lake - 40: "Lower Subglacial Lake Conway", # draining lake - 41: "Subglacial Lake Conway", # draining lake - 48: "Subglacial Lake Whillans", # filling lake - 50: "Whillans IX", # filling lake - 63: "Kamb 1", # filling lake - 65: "Kamb 12", # filling lake - 97: "MacAyeal 1", # draining lake - 109: "Slessor 45", # draining lake - 116: "Slessor 23", # filling lake - 151: "Recovery IV", # draining lake - 156: "Recovery 2", # filling lake + (15, 19): "Subglacial Lake Mercer", # filling lake + (32,): "Whillans 7", # draining lake + (34, 35): "Subglacial Lake Conway", # draining lake + (41, 43, 45): "Subglacial Lake Whillans", # filling lake + (16, 46, 48): "Lake 78", # filling lake + (44,): "Whillans IX", # filling lake + (62,): "Kamb 1", # filling lake + # (65): "Kamb 12", # filling lake + (84,): "MacAyeal 1", # draining lake + (95,): "Slessor 45", # draining lake + (101,): "Slessor 23", # filling lake + (141,): "Recovery IV", # draining lake + (143, 144): "Recovery 2", # filling lake } -region = deepicedrain.Region.from_gdf(gdf=lake, name=lakedict[lake.name]) +region = deepicedrain.Region.from_gdf(gdf=lake, name=lakedict[lake_ids]) +assert (lake.inner_dhdt < 0 and draining) or (lake.inner_dhdt > 0 and not draining) print(lake) lake.geometry @@ -379,13 +386,17 @@ placename: str = region.name.lower().replace(" ", "_") df_lake: cudf.DataFrame = region.subset(data=df_dhdt) +# Save lake outline to OGR GMT file format +outline_points: str = f"figures/{placename}/{placename}.gmt" +if not os.path.exists(path=outline_points): + lakes.loc[list(lake_ids)].to_file(filename=outline_points, driver="OGR_GMT") # %% [markdown] # ## Create an interpolated ice surface elevation grid for each ICESat-2 cycle # %% # Generate gridded time-series of ice elevation over lake -cycles: tuple = (3, 4, 5, 6, 7, 8) +cycles: tuple = (3, 4, 5, 6, 7, 8, 9) os.makedirs(name=f"figures/{placename}", exist_ok=True) ds_lake: xr.Dataset = deepicedrain.spatiotemporal_cube( table=df_lake.to_pandas(), @@ -413,15 +424,9 @@ time_nsec: pd.Timestamp = df_lake[f"utc_time_{cycle}"].to_pandas().mean() time_sec: str = np.datetime_as_string(arr=time_nsec.to_datetime64(), unit="s") - grid = f"figures/{placename}/h_corr_{placename}_cycle_{cycle}.nc" - points = pd.DataFrame( - data=np.vstack(lake.geometry.boundary.coords.xy).T, columns=("x", "y") - ) - outline_points = pygmt.grdtrack(points=points, grid=grid, newcolname="z") - outline_points["z"] = outline_points.z.fillna(value=outline_points.z.median()) - + # grid = ds_lake.sel(cycle_number=cycle).z fig = deepicedrain.plot_icesurface( - grid=grid, # ds_lake.sel(cycle_number=cycle).z + grid=f"figures/{placename}/h_corr_{placename}_cycle_{cycle}.nc", grid_region=grid_region, diff_grid=ds_lake_diff.sel(cycle_number=cycle).z, diff_grid_region=diff_grid_region, diff --git a/deepicedrain/features/subglacial_lakes.feature b/deepicedrain/features/subglacial_lakes.feature index 24b5ff6..a77e266 100644 --- a/deepicedrain/features/subglacial_lakes.feature +++ b/deepicedrain/features/subglacial_lakes.feature @@ -15,24 +15,39 @@ Feature: Mapping Antarctic subglacial lakes Scenario Outline: Subglacial Lake Animation - Given some altimetry data at spatially subsetted to with + Given some altimetry data at spatially subsetted to with When it is turned into a spatiotemporal cube over ICESat-2 cycles And visualized at each cycle using a 3D perspective at and Then the result is an animation of ice surface elevation changing over time Examples: - | location | lake_name | lake_id | cycles | azimuth | elevation | - # | whillans_downstream | Mercer 2b | 21 | 3-8 | 157.5 | 45 | - # | whillans_downstream | Lower Subglacial Lake Conway | 40 | 3-8 | 157.5 | 45 | - # | whillans_downstream | Subglacial Lake Conway | 41 | 3-8 | 157.5 | 45 | - # | whillans_downstream | Subglacial Lake Whillans | 48 | 3-8 | 157.5 | 45 | - # | whillans_upstream | Whillans IX | 50 | 3-8 | 157.5 | 45 | - # | whillans_upstream | Kamb 8 | 61 | 3-8 | 157.5 | 45 | - # | whillans_upstream | Kamb 1 | 62 | 3-8 | 157.5 | 45 | - | whillans_upstream | Kamb 34 | 63 | 4-7 | 157.5 | 45 | - # | siple_coast | Kamb 12 | 65 | 3-8 | 157.5 | 45 | - # | siple_coast | MacAyeal 1 | 97 | 3-8 | 157.5 | 60 | - # | slessor_downstream | Slessor 45 | 109 | 3-8 | 202.5 | 60 | - # | slessor_downstream | Slessor 23 | 116 | 3-8 | 202.5 | 60 | - | slessor_downstream | Recovery IV | 141 | 3-8 | 247.5 | 45 | - # | slessor_downstream | Recovery 2 | 156 | 3-8 | 202.5 | 45 | + | location | lake_name | lake_ids | cycles | azimuth | elevation | + # | whillans_downstream | Mercer XV | 17 | 3-8 | 157.5 | 45 | + # | whillans_upstream | Whillans 7 | 32 | 3-8 | 157.5 | 45 | + # | whillans_upstream | Whillans 6 | 33 | 3-8 | 157.5 | 45 | + # | whillans_upstream | Whillans X | 38 | 3-8 | 157.5 | 45 | + # | whillans_upstream | Whillans XI | 49 | 3-8 | 157.5 | 45 | + # | whillans_upstream | Whillans IX | 44 | 3-8 | 157.5 | 45 | + # | whillans_upstream | Kamb 8 | 61 | 3-8 | 157.5 | 45 | + # | whillans_upstream | Kamb 1 | 62 | 3-8 | 157.5 | 45 | + | whillans_upstream | Kamb 34 | 63 | 4-7 | 157.5 | 45 | + # | siple_coast | Kamb 12 | 65 | 3-8 | 157.5 | 45 | + # | siple_coast | MacAyeal 1 | 84 | 3-8 | 157.5 | 60 | + # | slessor_downstream | Slessor 45 | 95 | 3-8 | 202.5 | 60 | + # | slessor_downstream | Slessor 23 | 101 | 3-8 | 202.5 | 60 | + | slessor_downstream | Recovery IV | 141 | 3-8 | 247.5 | 45 | + + + Scenario Outline: Subglacial Lake Mega-Cluster Animation + Given some altimetry data at spatially subsetted to with + When it is turned into a spatiotemporal cube over ICESat-2 cycles + And visualized at each cycle using a 3D perspective at and + Then the result is an animation of ice surface elevation changing over time + + Examples: + | location | lake_name | lake_ids | cycles | azimuth | elevation | + # | whillans_downstream | Lake 78 | 16,46,48 | 3-8 | 157.5 | 45 | + # | whillans_downstream | Subglacial Lake Conway | 34,35 | 3-8 | 157.5 | 45 | + | whillans_downstream | Subglacial Lake Mercer | 15,19 | 3-8 | 157.5 | 45 | + # | whillans_downstream | Subglacial Lake Whillans | 41,43,45 | 3-8 | 157.5 | 45 | + # | slessor_downstream | Recovery 2 | 143,144 | 3-8 | 202.5 | 45 | diff --git a/deepicedrain/spatiotemporal.py b/deepicedrain/spatiotemporal.py index e36f2e6..fb8d1d4 100644 --- a/deepicedrain/spatiotemporal.py +++ b/deepicedrain/spatiotemporal.py @@ -76,7 +76,7 @@ def from_gdf( import pygmt xmin, xmax, ymin, ymax = pygmt.info( - table=np.vstack(gdf.geometry.exterior.coords.xy).T, + table=np.vstack(gdf.geometry.convex_hull.exterior.coords.xy).T, spacing=float(spacing), ) except (ImportError, TypeError): diff --git a/deepicedrain/tests/test_subglacial_lake_animation.py b/deepicedrain/tests/test_subglacial_lake_animation.py index 28d2ec7..5e54768 100644 --- a/deepicedrain/tests/test_subglacial_lake_animation.py +++ b/deepicedrain/tests/test_subglacial_lake_animation.py @@ -10,8 +10,10 @@ import numpy as np import pandas as pd import pygmt +import pytest import tqdm import xarray as xr +from packaging.version import Version from pytest_bdd import given, scenario, then, when import deepicedrain @@ -21,7 +23,7 @@ feature_name="features/subglacial_lakes.feature", scenario_name="Subglacial Lake Animation", example_converters=dict( - lake_name=str, lake_id=int, cycles=str, azimuth=float, elevation=float + lake_name=str, lake_ids=str, cycles=str, azimuth=float, elevation=float ), ) def test_subglacial_lake_animation(): @@ -31,12 +33,31 @@ def test_subglacial_lake_animation(): pass +@pytest.mark.skipif( + Version(deepicedrain.__version__) < Version("0.4.0"), + reason="Requires newer df_dhdt_*.parquet file", +) +@scenario( + feature_name="features/subglacial_lakes.feature", + scenario_name="Subglacial Lake Mega-Cluster Animation", + example_converters=dict( + lake_name=str, lake_ids=str, cycles=str, azimuth=float, elevation=float + ), +) +def test_subglacial_lake_megacluster_animation(): + """ + Generate an animated time-series visualization for multiple active + subglacial lakes in a mega-cluster. + """ + pass + + @given( - "some altimetry data at spatially subsetted to with ", + "some altimetry data at spatially subsetted to with ", target_fixture="df_lake", ) def lake_altimetry_data( - location: str, lake_name: str, lake_id: int, context + location: str, lake_name: str, lake_ids: str, context ) -> pd.DataFrame: """ Load up some pre-processed ICESat-2 ATL11 altimetry data from a Parquet @@ -50,13 +71,30 @@ def lake_altimetry_data( dataframe: pd.DataFrame = pd.read_parquet(openfile) antarctic_lakes: gpd.GeoDataFrame = deepicedrain.catalog.subglacial_lakes.read() + antarctic_lakes = antarctic_lakes.set_crs(epsg=3031, allow_override=True) context.lake_name: str = lake_name context.placename: str = context.lake_name.lower().replace(" ", "_") - context.lake: pd.Series = antarctic_lakes.loc[lake_id] + lake_ids: tuple = tuple(map(int, lake_ids.split(","))) + context.lake: pd.Series = ( + antarctic_lakes.loc[list(lake_ids)] + .dissolve(by="basin_name", as_index=False) + .squeeze() + ) context.draining = True if context.lake.inner_dhdt < 0 else False + # Save lake outline to OGR GMT file format + os.makedirs(name=f"figures/{context.placename}", exist_ok=True) + context.outline_points: str = f"figures/{context.placename}/{context.placename}.gmt" + try: + os.remove(path=context.outline_points) + except FileNotFoundError: + pass + antarctic_lakes.loc[list(lake_ids)].to_file( + filename=context.outline_points, driver="OGR_GMT", mode="w" + ) + context.region = deepicedrain.Region.from_gdf( gdf=context.lake, name=context.lake_name ) @@ -72,8 +110,6 @@ def create_spatiotemporal_cube( """ Generate gridded time-series of ice elevation over lake. """ - os.makedirs(name=f"figures/{context.placename}", exist_ok=True) - start, end = cycles.split("-") # E.g. "3-8" context.cycles = tuple(range(int(start), int(end) + 1)) # ICESat-2 cycles context.ds_lake: xr.Dataset = deepicedrain.spatiotemporal_cube( @@ -117,23 +153,14 @@ def visualize_grid_in_3D( time_nsec: pd.Timestamp = df_lake[f"utc_time_{cycle}"].mean() time_sec: str = np.datetime_as_string(arr=time_nsec.to_datetime64(), unit="s") - grid = ( - f"figures/{context.placename}/h_corr_{context.placename}_cycle_{cycle}.nc" - ) - points = pd.DataFrame( - data=np.vstack(context.lake.geometry.boundary.coords.xy).T, - columns=("x", "y"), - ) - outline_points = pygmt.grdtrack(points=points, grid=grid, newcolname="z") - outline_points["z"] = outline_points.z.fillna(value=outline_points.z.median()) - + # grid = ds_lake.sel(cycle_number=cycle).z fig = deepicedrain.plot_icesurface( - grid=grid, # ds_lake.sel(cycle_number=cycle).z + grid=f"figures/{context.placename}/h_corr_{context.placename}_cycle_{cycle}.nc", grid_region=grid_region, diff_grid=ds_lake_diff.sel(cycle_number=cycle).z, diff_grid_region=diff_grid_region, track_points=df_lake[["x", "y", f"h_corr_{cycle}"]].dropna().to_numpy(), - outline_points=outline_points, + outline_points=context.outline_points, azimuth=azimuth, elevation=elevation, title=f"{context.region.name} at Cycle {cycle} ({time_sec})", diff --git a/deepicedrain/vizplots.py b/deepicedrain/vizplots.py index 11ff10b..a699435 100644 --- a/deepicedrain/vizplots.py +++ b/deepicedrain/vizplots.py @@ -520,11 +520,20 @@ def plot_icesurface( ) # Plot lake boundary outline as yellow dashed line if outline_points is not None: - fig.plot3d( - data=outline_points.values, - region=grid_region, - pen="1.5p,yellow2,-", - zscale=True, - perspective=True, - ) + with pygmt.helpers.GMTTempFile() as tmpfile: + pygmt.grdtrack(points=outline_points, grid=grid, outfile=tmpfile.name) + _df = pd.read_csv(tmpfile.name, sep="\t", names=["x", "y", "z"]) + pygmt.grdtrack( + points=outline_points, + grid=grid, + outfile=tmpfile.name, + d=f"o{_df.z.median()}", # fill NaN points with median height + ) + fig.plot3d( + data=tmpfile.name, + region=grid_region, + pen="1.5p,yellow2,-", + zscale=True, + perspective=True, + ) return fig