Skip to content

Commit 598d267

Browse files
Merge pull request #520 from matthewhoffman/landice/hydro_stats_plot
Add script to plot hydro mass balance terms This PR adds new scripts related to MALI subglacial hydrology: * script with plots for evaluating subglacial hydrology mass balance from global stats * script for preprocessing ice geometry input file to remove surface depressions. This greatly helps with lake-related instabilities
2 parents bb77750 + 2377ea1 commit 598d267

File tree

2 files changed

+390
-0
lines changed

2 files changed

+390
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
#!/usr/bin/env python3
2+
"""
3+
fill_supraglacial_lakes.py
4+
5+
fill depressions in the grounded ice-sheet upper surface
6+
"""
7+
8+
import argparse
9+
import heapq
10+
import mosaic
11+
import numpy as np
12+
import sys
13+
import xarray as xr
14+
15+
from matplotlib import pyplot as plt
16+
import matplotlib.cm as cm
17+
import matplotlib.colors as mcolors
18+
from compass.landice.mesh import mpas_flood_fill
19+
20+
rhoi = 910.0
21+
rhoo = 1028.0
22+
23+
24+
class CyclicNormalize(mcolors.Normalize):
25+
def __init__(self, cmin=0, cmax=1, vmin=0, vmax=1, clip=False):
26+
self.cmin = cmin
27+
self.cmax = cmax
28+
mcolors.Normalize.__init__(self, vmin, vmax, clip=clip)
29+
30+
def __call__(self, value, clip=False):
31+
x, y = [self.cmin, self.cmax], [0, 1]
32+
return np.ma.masked_array(np.interp(value, x, y, period=self.cmax - self.cmin))
33+
34+
35+
def parse_arguments():
36+
"""
37+
Parse command-line arguments.
38+
39+
Returns
40+
-------
41+
argparse.Namespace
42+
Parsed arguments containing input file paths, parameters, and options.
43+
"""
44+
parser = argparse.ArgumentParser(
45+
description="Run a priority-flood fill on a 2D unstructured potential surface."
46+
)
47+
48+
parser.add_argument(
49+
"-i", "--input",
50+
type=str,
51+
required=True,
52+
help="Path to input file"
53+
)
54+
55+
parser.add_argument(
56+
"-o", "--output",
57+
type=str,
58+
required=True,
59+
help="Path to output file"
60+
)
61+
62+
parser.add_argument(
63+
"--verbose",
64+
action="store_true",
65+
help="Enable verbose output for debugging."
66+
)
67+
68+
return parser.parse_args()
69+
70+
def run(args):
71+
"""
72+
Load MALI geometry data and calculates needed derived fields
73+
74+
Returns
75+
-------
76+
xarray.dataset
77+
78+
79+
"""
80+
ds = xr.open_dataset(args.input, decode_times=False, decode_cf=False)
81+
thk = ds.thickness[0, :]
82+
bed = ds.bedTopography[0, :]
83+
ds["grd_mask"] = ((thk * rhoi / rhoo +bed) > 0.0) * (thk > 0.0)
84+
ds["upperSurfaceGrd"] = ds.grd_mask * (bed + thk)
85+
usrf = ds['upperSurfaceGrd'].values[:]
86+
nCells = ds.nCells.values
87+
cellsOnCell = ds.cellsOnCell.values
88+
nEdgesOnCell = ds.nEdgesOnCell.values
89+
90+
# Create mask for grounded margin, defined here to include transition
91+
# between floating and grounded
92+
ds["grd_margin_mask"] = xr.zeros_like(ds.upperSurfaceGrd, dtype=int)
93+
for iCell in np.argwhere(ds.grd_mask.values == 1):
94+
neighbor_indices = cellsOnCell[iCell, :nEdgesOnCell[iCell][0]] - 1
95+
neighbor_indices = neighbor_indices[neighbor_indices >= 0]
96+
if ds.grd_mask.values[neighbor_indices].sum() != len(neighbor_indices):
97+
ds.grd_margin_mask[iCell] = 1
98+
99+
# create mask for hydrologically connected region to margin
100+
#print("Creating mask of hydrologically connected region to margin")
101+
#ds['mgn_draining_mask'] = xr.zeros_like(ds.upperSurfaceGrd, dtype=int)
102+
#iter = 0
103+
#seed_mask = ds['grd_margin_mask'].values[:]
104+
#print(f"Size of grd_margin_mask={len(seed_mask)}")
105+
#grow_mask = ds["grd_mask"].values[:]
106+
#new_keep_mask = seed_mask.copy()
107+
#keep_mask = new_keep_mask * 0
108+
#n_mask_cells = keep_mask.sum()
109+
#grow_iters=sys.maxsize
110+
#new_ind = np.nonzero((new_keep_mask - keep_mask) == 1)[0]
111+
#for iter in range(grow_iters):
112+
# new_keep_mask = keep_mask.copy()
113+
# print(f'iter={iter}, keep_mask size={keep_mask.sum()}')
114+
115+
# for iCell in new_ind:
116+
# neighs = cellsOnCell[iCell, :nEdgesOnCell[iCell]] - 1
117+
# neighs = neighs[neighs >= 0] # drop garbage cell
118+
# for jCell in neighs:
119+
# if grow_mask[jCell] == 1 and usrf[jCell] >= usrf[iCell]:
120+
# new_keep_mask[jCell] = 1
121+
122+
# # only search over new cells added previous iteration
123+
# new_ind = np.nonzero((new_keep_mask - keep_mask) == 1)[0]
124+
# keep_mask = new_keep_mask.copy()
125+
126+
# n_mask_cells_new = keep_mask.sum()
127+
# if n_mask_cells_new == n_mask_cells:
128+
# break
129+
# n_mask_cells = n_mask_cells_new
130+
# iter += 1
131+
#ds["mgn_draining_mask"][:] = keep_mask
132+
133+
# ---------------
134+
# find depressions
135+
grd_mask = ds['grd_mask'].values[:]
136+
margin = ds['grd_margin_mask'].values[:]
137+
filled = usrf.copy()
138+
visited = np.zeros_like(usrf, dtype=bool)
139+
pq = []
140+
141+
# add margin cells to heap to start
142+
for iCell in np.where(margin == 1)[0]:
143+
heapq.heappush(pq, (usrf[iCell], iCell))
144+
visited[iCell] = True
145+
146+
while pq:
147+
ht, iCell = heapq.heappop(pq)
148+
neighbor_indices = cellsOnCell[iCell, 0:nEdgesOnCell[iCell]] - 1
149+
neighbor_indices = neighbor_indices[neighbor_indices >= 0]
150+
for jCell in neighbor_indices:
151+
if not visited[jCell] and grd_mask[jCell]:
152+
visited[jCell] = True
153+
filled[jCell] = max(usrf[jCell], ht)
154+
heapq.heappush(pq, (filled[jCell], jCell))
155+
ds['usrf_filled'] = xr.zeros_like(ds.upperSurfaceGrd)
156+
ds['usrf_filled'][:] = filled
157+
158+
diff = ds['usrf_filled'] - ds['upperSurfaceGrd']
159+
diff = diff.values
160+
161+
print("Filling statistics:")
162+
print(f"#cells adjusted by > 0m: {(diff>0.).sum()}")
163+
print(f"#cells adjusted by > 5m: {(diff>5.).sum()}")
164+
print(f"#cells adjusted by > 10m: {(diff>10.).sum()}")
165+
print(f"#cells adjusted by > 20m: {(diff>20.).sum()}")
166+
print(f"#cells adjusted by > 50m: {(diff>50.).sum()}")
167+
print(f"#cells adjusted by >100m: {(diff>100.).sum()}")
168+
169+
170+
dsout = xr.open_dataset(args.input, decode_times=False, decode_cf=False)
171+
dsout['thickness'] = dsout['thickness'] + (ds['usrf_filled'] - ds['upperSurfaceGrd'])
172+
dsout.to_netcdf(args.output)
173+
174+
175+
if args.verbose:
176+
# plot result
177+
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(14, 10),
178+
constrained_layout=True,
179+
sharex=True, sharey=True)
180+
axes = axes.flatten()
181+
descriptor = mosaic.Descriptor(ds)
182+
183+
ax = 0
184+
pc = mosaic.polypcolor(axes[ax], descriptor,
185+
c=(thk > 0.0), aa=False)
186+
axes[ax].set_title(f"ice mask")
187+
188+
ax += 1
189+
pc = mosaic.polypcolor(axes[ax], descriptor,
190+
ds.grd_mask, aa=False)
191+
axes[ax].set_title(f"grd mask")
192+
193+
ax += 1
194+
pc = mosaic.polypcolor(axes[ax], descriptor,
195+
ds.grd_margin_mask, aa=False)
196+
axes[ax].set_title(f"grounded margin mask")
197+
198+
#pc = mosaic.polypcolor(axes[3], descriptor,
199+
# ds.mgn_draining_mask + ds.grd_mask, aa=False)
200+
#axes[3].set_title(f"margin draining mask + grd mask")
201+
202+
ax += 1
203+
period = 200.0
204+
norm = mcolors.Normalize(vmin=0, vmax=period)
205+
cyclicnorm = CyclicNormalize(cmin=0., cmax=400.0, vmin=0.0, vmax=4500.0)
206+
pc = mosaic.polypcolor(axes[ax], descriptor,
207+
ds['upperSurfaceGrd'], aa=False,
208+
cmap="hsv", norm=cyclicnorm)
209+
#pc = mosaic.polypcolor(axes[ax], descriptor,
210+
# ds.mgn_draining_mask * np.nan,
211+
# ec=np.where(ds.mgn_draining_mask.values == 1, "white", "black"))
212+
##fig.colorbar(pc, ax=axes[ax], fraction=0.1,
213+
## label=f"surface elevation (m)")
214+
pc = mosaic.polypcolor(axes[ax], descriptor,
215+
ds['usrf_filled'] * np.nan,
216+
ec=np.where(usrf == filled, "white", "black"),
217+
lw=np.where(usrf == filled, 0.01, 0.5))
218+
axes[ax].set_title(f"upper surface")
219+
220+
ax += 1
221+
pc = mosaic.polypcolor(axes[ax], descriptor,
222+
ds['usrf_filled'], aa=False,
223+
cmap="hsv", norm=cyclicnorm)
224+
pc = mosaic.polypcolor(axes[ax], descriptor,
225+
ds['usrf_filled'] * np.nan,
226+
ec=np.where(usrf == filled, "white", "black"),
227+
lw=np.where(usrf == filled, 0.01, 0.5))
228+
axes[ax].set_title(f"upper surface filled")
229+
230+
ax += 1
231+
diff = ds['usrf_filled'] - ds['upperSurfaceGrd']
232+
diff = diff.where(diff != 0.0)
233+
cmap = cm.get_cmap("rainbow", 10).copy()
234+
cmap.set_bad(color="white")
235+
pc = mosaic.polypcolor(axes[ax], descriptor,
236+
diff,
237+
vmin=0, vmax=50,
238+
aa=False, cmap=cmap)
239+
axes[ax].set_title(f"upper surface adjustment")
240+
fig.colorbar(pc, ax=axes[ax], fraction=0.1)
241+
242+
243+
for ax in axes:
244+
ax.set_aspect('equal')
245+
ax.set_xlabel("X (m)")
246+
ax.set_ylabel("Y (m)")
247+
248+
249+
250+
251+
def main():
252+
"""
253+
Main entry point for the script.
254+
"""
255+
args = parse_arguments()
256+
257+
if args.verbose:
258+
print("Arguments parsed successfully:")
259+
print(args)
260+
261+
# perform processing
262+
run(args)
263+
264+
if args.verbose:
265+
print("Execution complete.")
266+
plt.show()
267+
268+
return 0
269+
270+
271+
if __name__ == "__main__":
272+
sys.exit(main())
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python
2+
'''
3+
Script to plot subglacial hydrology time-series from a landice globalStats file.
4+
'''
5+
6+
from __future__ import absolute_import, division, print_function, unicode_literals
7+
8+
import netCDF4 as nc
9+
import numpy as np
10+
from matplotlib import pyplot as plt
11+
import argparse
12+
13+
rhow = 1000.0
14+
secyr = 3600.0 * 24.0 * 365.0
15+
16+
parser = argparse.ArgumentParser(description=__doc__)
17+
parser.add_argument("-f", dest="filename", help="input filename", default="globalStats.nc", metavar="FILENAME")
18+
parser.add_argument("-u", dest="units", help="units for mass: kg, Gt", default="Gt", metavar="FILENAME")
19+
args = parser.parse_args()
20+
21+
# Scaling assuming variables are in kg
22+
if args.units == "kg":
23+
massUnit = "kg"
24+
fluxUnit = "kg yr$^{-1}$"
25+
unitScaling = 1.0
26+
elif args.units == "Gt":
27+
massUnit = "Gt"
28+
fluxUnit = "Gt yr$^{-1}$"
29+
unitScaling = 1.0e-12
30+
else:
31+
sys.exit("Unknown mass unit")
32+
33+
print("Using mass units of: ", massUnit)
34+
35+
36+
dataset = nc.Dataset(args.filename)
37+
38+
# Read variables
39+
# convert everything to kg and years before unit conversion
40+
totalSubglacialWaterMass = \
41+
dataset.variables['totalSubglacialWaterVolume'][:] * rhow * unitScaling
42+
melt = dataset.variables['totalBasalMeltInput'][:] * unitScaling * secyr
43+
distFluxMarine = dataset.variables['totalDistWaterFluxMarineMargin'][:] * unitScaling * secyr
44+
chnlFluxMarine = dataset.variables['totalChnlWaterFluxMarineMargin'][:] * unitScaling * secyr
45+
distFluxLand = dataset.variables['totalDistWaterFluxTerrestrialMargin'][:] * unitScaling * secyr
46+
chnlFluxLand = dataset.variables['totalChnlWaterFluxTerrestrialMargin'][:] * unitScaling * secyr
47+
chnlMelt = dataset.variables['totalChannelMelt'][:] * unitScaling * secyr
48+
flotFrac = dataset.variables['avgFlotationFraction'][:]
49+
lakeArea = dataset.variables['totalSubglacialLakeArea'][:] / 1000.0**2 # km^2
50+
lakeMass = dataset.variables['totalSubglacialLakeVolume'][:] * rhow * unitScaling
51+
grdArea = dataset.variables['groundedIceArea'][:] / 1000.0**2 # km^2
52+
53+
deltat = dataset.variables['deltat'][:] / secyr # in years
54+
yr = dataset.variables['daysSinceStart'][:] / 365.0
55+
56+
subglacialWaterMassRate = np.zeros((len(melt),))
57+
58+
for i in range(len(totalSubglacialWaterMass) - 1):
59+
subglacialWaterMassRate[i] = ((totalSubglacialWaterMass[i+1] -
60+
totalSubglacialWaterMass[i]) / deltat[i])
61+
62+
# plot mass balance time-series
63+
fig, ax = plt.subplots(1, 1, layout='tight', figsize=(8,6))
64+
# input
65+
plt.plot(yr, melt, 'r:', label='basal melt')
66+
plt.plot(yr, chnlMelt, 'r--', label='channel melt')
67+
total_melt = melt + chnlMelt
68+
plt.plot(yr, total_melt, 'r-', label='total melt')
69+
70+
# output
71+
plt.plot(yr, distFluxMarine, 'b--', label='marine sheet outflux')
72+
plt.plot(yr, distFluxLand, 'b:', label='land sheet outflux')
73+
plt.plot(yr, chnlFluxMarine, 'c--', label='marine chnl outflux')
74+
plt.plot(yr, chnlFluxLand, 'c:', label='land chnl outflux')
75+
total_outflux = distFluxMarine + distFluxLand + chnlFluxMarine + chnlFluxLand
76+
plt.plot(yr, total_outflux, 'b-', lw=2, label='total outflux')
77+
78+
plt.plot(yr[1:-1], subglacialWaterMassRate[1:-1], 'g-', label='dV/dt')
79+
80+
plt.plot(yr, total_melt - total_outflux, 'k', label='I-O')
81+
82+
plt.legend(loc='best')
83+
plt.xlabel('Year')
84+
plt.ylabel(f'Mass flux ({fluxUnit})')
85+
86+
# Plot other time-series of interest
87+
fig, axes = plt.subplots(2, 2, sharex=True, layout='tight',
88+
figsize=(10, 7))
89+
axes = axes.flatten()
90+
91+
ax = 0
92+
axes[ax].plot(yr, flotFrac)
93+
axes[ax].set_ylabel('Flotation fraction')
94+
95+
ax += 1
96+
axes[ax].plot(yr, totalSubglacialWaterMass)
97+
axes[ax].set_ylabel(f'Water mass ({massUnit})')
98+
99+
ax += 1
100+
axes[ax].plot(yr, lakeArea)
101+
axes[ax].set_ylabel('Lake area (km$^2$)')
102+
# second axis for % area
103+
ax2 = axes[ax].twinx()
104+
ax2.plot(yr, lakeArea / grdArea, ':', color="blue")
105+
ax2.set_ylabel("Lake area percentage", color="blue")
106+
ax2.tick_params(axis="y", colors="blue")
107+
108+
ax += 1
109+
axes[ax].plot(yr, lakeMass)
110+
axes[ax].set_ylabel(f'Lake mass ({massUnit})')
111+
112+
for ax in axes:
113+
ax.grid(True)
114+
ax.set_xlabel("Year")
115+
116+
117+
plt.show()
118+

0 commit comments

Comments
 (0)