Skip to content

Commit 5cc1b97

Browse files
Merge pull request #481 from trhille/landice/add_general_maps_plotting_script
Add a general script for plotting 2D output Add a generalized script for plotting 2D output for arbitrary numbers of output files, variables, and time levels. The purposes of this script are: to serve as a quick-view tool so that we don't have to scp large output files for simple visualization. to serve as a template for more customized plotting scripts for presentations and publications.
2 parents 99490de + bee0a16 commit 5cc1b97

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""
4+
Created on Dec 9, 2022
5+
6+
@author: Trevor Hillebrand, Matthew Hoffman
7+
8+
Script to plot snapshot maps of MALI output for an arbitrary number of files,
9+
variables, and output times. There is no requirement for all output files
10+
to be on the same mesh. Each output file gets its own figure, each
11+
variable gets its own row, and each time gets its own column. Three contours
12+
are automatically plotted, showing intial ice extent (black), initial
13+
grounding-line position (grey), and grounding-line position at the desired
14+
time (white).
15+
16+
"""
17+
import numpy as np
18+
from netCDF4 import Dataset
19+
import argparse
20+
import matplotlib.pyplot as plt
21+
import matplotlib.tri as tri
22+
import matplotlib.gridspec as gridspec
23+
from matplotlib.colorbar import Colorbar
24+
from matplotlib.colors import Normalize, TwoSlopeNorm
25+
26+
27+
print("** Gathering information. (Invoke with --help for more details. All arguments are optional)")
28+
parser = argparse.ArgumentParser(description=__doc__)
29+
parser.add_argument("-r", dest="runs", default=None, metavar="FILENAME",
30+
help="path to .nc file or dir containing output.nc \
31+
file (strings separated by commas; no spaces)")
32+
parser.add_argument("-t", dest="timeLevels", default="-1",
33+
help="integer time levels at which to plot \
34+
(int separated by commas; no spaces)")
35+
parser.add_argument("-v", dest="variables", default='thickness',
36+
help="variable(s) to plot (list separated by commas; no spaces)")
37+
parser.add_argument("-l", dest="log_plot", default=None,
38+
help="Whether to plot the log10 of each variable \
39+
(True or False list separated by commas; no spaces)")
40+
parser.add_argument("-c", dest="colormaps", default=None,
41+
help="colormaps to use for plotting (list separated by commas \
42+
, no spaces). This overrides default colormaps.")
43+
parser.add_argument("-m", dest="mesh", default=None, metavar="FILENAME",
44+
help="Optional input file(s) containing mesh variables. This \
45+
is useful when plotting from files that have no mesh \
46+
variables to limit file size. Define either one mesh file \
47+
to be applied to all run files, or one mesh file per \
48+
run file (list separated by commas; no spaces)")
49+
parser.add_argument("-s", dest="saveNames", default=None, metavar="FILENAME",
50+
help="filename for saving. If empty or None, will plot \
51+
to screen instead of saving.")
52+
53+
args = parser.parse_args()
54+
runs = args.runs.split(',') # split run directories into list
55+
variables = args.variables.split(',')
56+
timeLevs = args.timeLevels.split(',') # split time levels into list
57+
# convert timeLevs to list of ints
58+
timeLevs = [int(i) for i in timeLevs]
59+
sec_per_year = 60. * 60. * 24. * 365.
60+
rhoi = 910.
61+
rhosw = 1028.
62+
63+
if args.log_plot is not None:
64+
log_plot = args.log_plot.split(',')
65+
else:
66+
log_plot = [False] * len(variables)
67+
68+
if args.colormaps is not None:
69+
colormaps = args.colormaps.split(',')
70+
else:
71+
colormaps = ['viridis'] * len(variables)
72+
73+
# If separate mesh file(s) specified, use those.
74+
# Otherwise, get mesh variables from runs files.
75+
# If -m is used, there will either be one 'master'
76+
# mesh file that is used for all run files, or
77+
# there will be one mesh file per run file.
78+
if args.mesh is not None:
79+
mesh = args.mesh.split(',')
80+
if len(mesh) == 1 and len(runs) > 1:
81+
mesh *= len(runs)
82+
assert len(mesh) == len(runs), ("Define either one master mesh file, "
83+
"or one mesh file per run file. "
84+
f"You defined {len(mesh)} files and "
85+
f"{len(runs)} run files.")
86+
else:
87+
mesh = runs
88+
89+
if args.saveNames is not None:
90+
saveNames = args.saveNames.split(',')
91+
92+
# Set up a dictionary of default colormaps for common variables.
93+
# These can be overridden by the -c flag.
94+
defaultColors = {'thickness' : 'Blues',
95+
'surfaceSpeed' : 'plasma',
96+
'basalSpeed' : 'plasma',
97+
'bedTopography' : 'BrBG',
98+
'floatingBasalMassBalApplied' : 'cividis'
99+
}
100+
101+
if args.colormaps is not None:
102+
colormaps = args.colormaps.split(',')
103+
else:
104+
colormaps = []
105+
for variable in variables:
106+
if variable in defaultColors.keys():
107+
colormaps.append(defaultColors[variable])
108+
else:
109+
# All other variables default to viridis
110+
colormaps.append('viridis')
111+
112+
# Set bitmask values
113+
initialExtentValue = 1
114+
dynamicValue = 2
115+
floatValue = 4
116+
groundingLineValue = 256
117+
118+
# List of diverging colormaps for use in plotting bedTopography.
119+
# I don't see a way around hard-coding this.
120+
divColorMaps = ['PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu',
121+
'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']
122+
123+
def dist(i1, i2, xCell, yCell): # helper distance fn
124+
dist = ((xCell[i1]-xCell[i2])**2 + (yCell[i1]-yCell[i2])**2)**0.5
125+
return dist
126+
127+
# Loop over runs
128+
# Each run gets its own figure
129+
# Each variable gets its own row
130+
# Each time level gets its own column
131+
varPlot = {}
132+
figs = {}
133+
gs = {}
134+
for ii, run in enumerate(runs):
135+
if '.nc' not in run:
136+
run = run + '/output.nc'
137+
f = Dataset(run, 'r')
138+
if 'daysSinceStart' in f.variables.keys():
139+
yr = f.variables['daysSinceStart'][:] / 365.
140+
else:
141+
yr = [0.]
142+
143+
f.set_auto_mask(False)
144+
145+
# Get mesh geometry and calculate triangulation.
146+
# It would be more efficient to do this outside
147+
# this loop if all runs are on the same mesh, but we
148+
# want this to be as general as possible.
149+
if args.mesh is not None:
150+
m = Dataset(mesh[ii], 'r')
151+
else:
152+
m = f # use run file for mesh variables
153+
154+
xCell = m.variables["xCell"][:]
155+
yCell = m.variables["yCell"][:]
156+
dcEdge = m.variables["dcEdge"][:]
157+
158+
if args.mesh is not None:
159+
m.close()
160+
161+
triang = tri.Triangulation(xCell, yCell)
162+
triMask = np.zeros(len(triang.triangles))
163+
# Maximum distance in m of edges between points.
164+
# Make twice dcEdge to be safe
165+
maxDist = np.max(dcEdge) * 2.0
166+
for t in range(len(triang.triangles)):
167+
thisTri = triang.triangles[t, :]
168+
if dist(thisTri[0], thisTri[1], xCell, yCell) > maxDist:
169+
triMask[t] = True
170+
if dist(thisTri[1], thisTri[2], xCell, yCell) > maxDist:
171+
triMask[t] = True
172+
if dist(thisTri[0], thisTri[2], xCell, yCell) > maxDist:
173+
triMask[t] = True
174+
triang.set_mask(triMask)
175+
176+
# set up figure for this run
177+
figs[run] = plt.figure()
178+
figs[run].suptitle(run)
179+
nRows = len(variables)
180+
nCols = len(timeLevs) + 1
181+
182+
# last column is for colorbars
183+
gs[run] = gridspec.GridSpec(nRows, nCols,
184+
height_ratios=[1] * nRows,
185+
width_ratios=[1] * (nCols - 1) + [0.1])
186+
axs = []
187+
cbar_axs = []
188+
for row in np.arange(0, nRows):
189+
cbar_axs.append(plt.subplot(gs[run][row,-1]))
190+
for col in np.arange(0, nCols-1):
191+
if axs == []:
192+
axs.append(plt.subplot(gs[run][row, col]))
193+
else:
194+
axs.append(plt.subplot(gs[run][row, col], sharex=axs[0], sharey=axs[0]))
195+
196+
varPlot[run] = {} # is a dict of dicts too complicated?
197+
cbars = []
198+
# Loop over variables
199+
for row, (variable, log, colormap, cbar_ax) in enumerate(zip(variables, log_plot, colormaps, cbar_axs)):
200+
if variable == 'observedSpeed':
201+
var_to_plot = np.sqrt(f.variables['observedSurfaceVelocityX'][:]**2 +
202+
f.variables['observedSurfaceVelocityY'][:]**2)
203+
else:
204+
var_to_plot = f.variables[variable][:]
205+
206+
if 'Speed' in variable:
207+
units = 'm yr^{-1}'
208+
var_to_plot *= sec_per_year
209+
else:
210+
try:
211+
units = f.variables[variable].units
212+
except AttributeError:
213+
units='no-units'
214+
215+
if log == 'True':
216+
var_to_plot = np.log10(var_to_plot)
217+
# Get rid of +/- inf values that ruin vmin and vmax
218+
# calculations below.
219+
var_to_plot[np.isinf(var_to_plot)] = np.nan
220+
colorbar_label_prefix = 'log10 '
221+
else:
222+
colorbar_label_prefix = ''
223+
224+
varPlot[run][variable] = []
225+
226+
# Plot bedTopography on an asymmetric colorbar if appropriate
227+
if ( (variable == 'bedTopography') and
228+
(np.nanquantile(var_to_plot[timeLevs, :], 0.99) > 0.) and
229+
(colormap in divColorMaps) ):
230+
norm = TwoSlopeNorm(vmin=np.nanquantile(var_to_plot[timeLevs, :], 0.01),
231+
vmax=np.nanquantile(var_to_plot[timeLevs, :], 0.99),
232+
vcenter=0.)
233+
else:
234+
norm = Normalize(vmin=np.nanquantile(var_to_plot[timeLevs, :], 0.01),
235+
vmax=np.nanquantile(var_to_plot[timeLevs, :], 0.99))
236+
237+
if 'cellMask' in f.variables.keys():
238+
calc_mask = True
239+
cellMask = f.variables["cellMask"][:]
240+
floatMask = (cellMask & floatValue) // floatValue
241+
dynamicMask = (cellMask & dynamicValue) // dynamicValue
242+
groundingLineMask = (cellMask & groundingLineValue) // groundingLineValue
243+
initialExtentMask = (cellMask & initialExtentValue) // initialExtentValue
244+
elif ( 'cellMask' not in f.variables.keys() and
245+
'thickness' in f.variables.keys() and
246+
'bedTopography' in f.variables.keys() ):
247+
print(f'cellMask is not present in output file {run}; calculating masks from ice thickness')
248+
calc_mask = True
249+
groundedMask = (f.variables['thickness'][:] > (-rhosw / rhoi * f.variables['bedTopography'][:]))
250+
groundingLineMask = groundedMask.copy() # This isn't technically correct, but works for plotting
251+
initialExtentMask = (f.variables['thickness'][:] > 0.)
252+
else:
253+
print(f'cellMask and thickness and/or bedTopography not present in output file {run};'
254+
' Skipping mask calculation.')
255+
calc_mask = False
256+
257+
# Loop over time levels
258+
for col, timeLev in enumerate(timeLevs):
259+
index = row * (nCols - 1) + col
260+
# plot initial grounding line position, initial extent, and GL position at t=timeLev
261+
if calc_mask:
262+
axs[index].tricontour(triang, groundingLineMask[0, :],
263+
levels=[0.9999], colors='grey',
264+
linestyles='solid')
265+
axs[index].tricontour(triang, groundingLineMask[timeLev, :],
266+
levels=[0.9999], colors='white',
267+
linestyles='solid')
268+
axs[index].tricontour(triang, initialExtentMask[timeLev, :],
269+
levels=[0.9999], colors='black',
270+
linestyles='solid')
271+
272+
# Plot 2D field at each desired time. Use quantile range of 0.01-0.99 to cut out
273+
# outliers. Could improve on this by accounting for areaCell, as currently all cells
274+
# are weighted equally in determining vmin and vmax.
275+
varPlot[run][variable].append(
276+
axs[index].tripcolor(
277+
triang, var_to_plot[timeLev, :], cmap=colormap,
278+
shading='flat', norm=norm))
279+
axs[index].set_aspect('equal')
280+
axs[index].set_title(f'year = {yr[timeLev]:0.2f}')
281+
282+
cbars.append(Colorbar(ax=cbar_ax, mappable=varPlot[run][variable][0], orientation='vertical',
283+
label=f'{colorbar_label_prefix}{variable} (${units}$)'))
284+
285+
figs[run].tight_layout()
286+
if args.saveNames is not None:
287+
figs[run].savefig(saveNames[ii], dpi=400, bbox_inches='tight')
288+
289+
f.close()
290+
291+
plt.show()

0 commit comments

Comments
 (0)