|
| 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