|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +""" |
| 4 | +Plot time series of thermal forcing and/or surface mass balance at specified |
| 5 | +locations and depths. |
| 6 | +@author: trevorhillebrand |
| 7 | +""" |
| 8 | + |
| 9 | + |
| 10 | +import numpy as np |
| 11 | +import csv |
| 12 | +from netCDF4 import Dataset |
| 13 | +from optparse import OptionParser |
| 14 | +from scipy.interpolate import LinearNDInterpolator, interp1d |
| 15 | +import matplotlib.pyplot as plt |
| 16 | +from matplotlib.pyplot import cm |
| 17 | + |
| 18 | + |
| 19 | +parser = OptionParser(description='Plot time series of thermal forcing and/or surface mass balance') |
| 20 | +parser.add_option("-t", "--tf", dest="thermal_forcing_file", |
| 21 | + help="List of MPAS netCDF files that contains the ismip6shelfMelt_3dThermalForcing" \ |
| 22 | + " field and zOcean variable. Comma-separated, no spaces.") |
| 23 | +parser.add_option("-s", "--smb", dest="smb_file", |
| 24 | + help="List of MPAS netCDF files that contains the sfcMassBal" \ |
| 25 | + " field. Comma-separated, no spaces.") |
| 26 | +parser.add_option("-m", "--mesh", dest="mesh_file", |
| 27 | + help="the MPAS netCDF file that contains the mesh variables, as well as thickness and bedTopography") |
| 28 | +parser.add_option("-r", "--regions", dest="regions_file", default=None, |
| 29 | + help="the MPAS netCDF file that contains the region masks") |
| 30 | +parser.add_option("--start_time", dest="start_time", default="0", |
| 31 | + help="beginning of time range to plot") |
| 32 | +parser.add_option("--end_time", dest="end_time", default="-1", |
| 33 | + help="end of time range to plot") |
| 34 | +parser.add_option('-c', dest='coords_file', default=None, |
| 35 | + help='CSV file containing x in first column, y in second. No header.') |
| 36 | +parser.add_option('-x', dest='x_coords', default=None, |
| 37 | + help='List of x coordinates of transect if not \ |
| 38 | + using a csv file. Comma-separated, no spaces.') |
| 39 | +parser.add_option('-y', dest='y_coords', default=None, |
| 40 | + help='List of y coordinates of transect if not \ |
| 41 | + using a csv file. Comma-separated, no spaces.') |
| 42 | +parser.add_option('-d', dest='depth', default=None, |
| 43 | + help='Depth in meters at which to plot thermal forcing.' \ |
| 44 | + ' If a single value, the script will use linear 1d' \ |
| 45 | + ' interpolation to determine the thermal forcing' \ |
| 46 | + ' at that depth. If two comma-delimited values are given, the script' \ |
| 47 | + ' will provide the average over that depth range.') |
| 48 | +parser.add_option('-n', dest='region_number', default=None, |
| 49 | + help='Region number to plot. If None, use entire domain.') |
| 50 | +parser.add_option("--seafloor", dest="plot_seafloor_thermal_forcing", |
| 51 | + action="store_true", |
| 52 | + help="plot thermal forcing at the seafloor, instead of at specific depth") |
| 53 | +parser.add_option("--shelf_base", dest="plot_shelf_base_thermal_forcing", |
| 54 | + action="store_true", |
| 55 | + help="plot thermal forcing at the base of floating ice, instead of at specific depth") |
| 56 | +parser.add_option("--shelf_range", dest="plot_shelf_depth_range_thermal_forcing", |
| 57 | + action="store_true", |
| 58 | + help="plot average thermal forcing over the whole depth range spanned by the ice shelf base") |
| 59 | +parser.add_option("--average", dest="plot_average_thermal_forcing", |
| 60 | + action="store_true", help='Whether to plot average' \ |
| 61 | + ' thermal forcing across all coordinates provided.') |
| 62 | +parser.add_option("--n_samples", dest="n_samples", default=None, |
| 63 | + help="Number of random samples to take from provided coordinates.") |
| 64 | +parser.add_option("--save", dest="save_filename", default=None, |
| 65 | + help="File to save figure to.") |
| 66 | + |
| 67 | +options, args = parser.parse_args() |
| 68 | + |
| 69 | +rhoi = 910. |
| 70 | +rhosw = 1028. |
| 71 | +start_year = 1995 # first year in TF forcings |
| 72 | +scyr = 60. * 60. * 24. * 365. |
| 73 | +forcing_interval_years = 1. |
| 74 | + |
| 75 | +times_list = [options.start_time, options.end_time] # list of string times for plotting |
| 76 | +times = [int(i) for i in times_list] # list of integer time indices |
| 77 | + |
| 78 | +if options.coords_file is not None: |
| 79 | + x = [] |
| 80 | + y = [] |
| 81 | + with open(options.coords_file, newline='') as csvfile: |
| 82 | + reader = csv.reader(csvfile, delimiter=',') |
| 83 | + |
| 84 | + for row in reader: |
| 85 | + x.append(float(row[0])) |
| 86 | + y.append(float(row[1])) |
| 87 | + if [options.x_coords, options.y_coords] != [None, None]: |
| 88 | + print('-c and -x/-y options were both provided. Reading from ', |
| 89 | + f'{options.coords_file} and ignoring -x and -y settings.') |
| 90 | + x = np.asarray(x) |
| 91 | + y = np.asarray(y) |
| 92 | +else: |
| 93 | + x = np.array([float(i) for i in options.x_coords.split(',')]) |
| 94 | + y = np.array([float(i) for i in options.y_coords.split(',')]) |
| 95 | + |
| 96 | +if options.n_samples is not None: |
| 97 | + rand_idx = np.random.choice(x.shape[0], int(options.n_samples), replace=False) |
| 98 | + print(f"Using {options.n_samples} random samples from the {x.shape[0]} points provided.") |
| 99 | + x = x[rand_idx] |
| 100 | + y = y[rand_idx] |
| 101 | + |
| 102 | +# Mesh and geometry fields |
| 103 | +mesh = Dataset(options.mesh_file, 'r') |
| 104 | +mesh.set_always_mask(False) |
| 105 | +bed = mesh.variables["bedTopography"][:] |
| 106 | +thk = mesh.variables["thickness"][:] |
| 107 | +xCell = mesh.variables["xCell"][:] |
| 108 | +yCell = mesh.variables["yCell"][:] |
| 109 | +nCells = mesh.dimensions['nCells'].size |
| 110 | +areaCell = mesh.variables["areaCell"][:] |
| 111 | +ice_mask = thk[0, :] > 1. |
| 112 | +mesh.close() |
| 113 | + |
| 114 | +fig, ax = plt.subplots(1,2, sharex=True, figsize=(8,3), layout='constrained') |
| 115 | + |
| 116 | +# Get region information, if desired |
| 117 | +if options.region_number is not None: |
| 118 | + region_number = int(options.region_number) |
| 119 | + regions = Dataset(options.regions_file, 'r') |
| 120 | + regionCellMasks = regions.variables["regionCellMasks"][:, region_number] |
| 121 | + # Update ice_mask to only include desired region |
| 122 | + ice_mask = np.logical_and(ice_mask, regionCellMasks) |
| 123 | + regions.close() |
| 124 | + |
| 125 | +def interp_and_plot_tf(tf_file, plot_ax): |
| 126 | + # Thermal forcing fields |
| 127 | + tf_data = Dataset(tf_file, 'r') |
| 128 | + tf_data.set_always_mask(False) |
| 129 | + tf = tf_data.variables["ismip6shelfMelt_3dThermalForcing"][:] |
| 130 | + z = tf_data.variables["ismip6shelfMelt_zOcean"][:] |
| 131 | + n_time_levs = tf_data.dimensions["Time"].size |
| 132 | + tf_data.close() |
| 133 | + if times[1] == -1: |
| 134 | + times[1] = n_time_levs - 1 |
| 135 | + plot_times = np.arange(times[0], times[1], step=1) # annual posting |
| 136 | + |
| 137 | + # Find nearest cell to desired x,y locations |
| 138 | + nearest_cell = [] |
| 139 | + for x_i, y_i in zip(x, y): |
| 140 | + nearest_cell.append(np.argmin( np.sqrt((xCell - x_i)**2. + (yCell - y_i)**2) )) |
| 141 | + |
| 142 | + # Find depth to seafloor or ice-shelf base |
| 143 | + plot_depth_range = False |
| 144 | + if options.depth is not None: |
| 145 | + depth = np.array([float(i) for i in options.depth.split(',')]) |
| 146 | + if len(depth) == 2: |
| 147 | + plot_depth_range = True |
| 148 | + |
| 149 | + if options.plot_seafloor_thermal_forcing: |
| 150 | + depth = bed[0, nearest_cell] |
| 151 | + elif options.plot_shelf_base_thermal_forcing: |
| 152 | + # Assume this is floating ice |
| 153 | + depth = -1. * rhoi / rhosw * thk[0, nearest_cell] |
| 154 | + elif options.plot_shelf_depth_range_thermal_forcing: |
| 155 | + depth = np.zeros(2) |
| 156 | + depth[0] = np.max( (-1. * rhoi / rhosw * thk[0, nearest_cell]) |
| 157 | + [thk[0, nearest_cell] > 10.] ) # ignore very thin ice |
| 158 | + depth[1] = np.min( (-1. * rhoi / rhosw * thk[0, nearest_cell]) |
| 159 | + [thk[0, nearest_cell] > 10.] ) |
| 160 | + plot_depth_range = True |
| 161 | + |
| 162 | + # Clip depth to within the range of the TF data |
| 163 | + depth[depth > np.max(z)] = np.max(z) |
| 164 | + depth[depth < np.min(z)] = np.min(z) |
| 165 | + |
| 166 | + # Vertical interpolation of ocean forcing. |
| 167 | + if plot_depth_range: |
| 168 | + # We'll just use the nearest zOcean depths to the |
| 169 | + # requested top and bottom depths for simplicity and speed |
| 170 | + print(f"Averaging TF over the depth range {depth}") |
| 171 | + top_depth_index = np.argmin(np.abs(z - np.max(depth))) |
| 172 | + bottom_depth_index = np.argmin(np.abs(z - np.min(depth))) |
| 173 | + tf_depth = np.mean(tf[plot_times[0]:plot_times[-1] + 1, :, |
| 174 | + top_depth_index:bottom_depth_index+1], axis=2) |
| 175 | + else: |
| 176 | + tf_depth = [] |
| 177 | + for time in plot_times: |
| 178 | + tf_tmp = [] |
| 179 | + for cell, cell_depth in zip(nearest_cell, depth): |
| 180 | + tf_interp = interp1d(z, tf[time, cell, :]) |
| 181 | + tf_tmp.append(tf_interp(cell_depth)) |
| 182 | + tf_depth.append(tf_tmp) |
| 183 | + |
| 184 | + if "UKESM" in tf_file: |
| 185 | + if "SSP126" in tf_file: |
| 186 | + plot_color = 'tab:green' |
| 187 | + else: |
| 188 | + plot_color = 'tab:blue' |
| 189 | + else: |
| 190 | + plot_color = 'tab:grey' |
| 191 | + |
| 192 | + if "CESM" in tf_file: |
| 193 | + linestyle = "dashed" |
| 194 | + elif "CCSM" in tf_file: |
| 195 | + linestyle = "dotted" |
| 196 | + else: |
| 197 | + linestyle = "solid" |
| 198 | + |
| 199 | + if options.plot_average_thermal_forcing: |
| 200 | + tf_avg = np.mean(tf_depth, axis=1) |
| 201 | + tf_std = np.std(tf_depth, axis=1) |
| 202 | + plot_ax.plot(plot_times + start_year, tf_avg, c=plot_color, linestyle=linestyle) |
| 203 | + plot_ax.fill_between(plot_times + start_year, tf_avg - tf_std, |
| 204 | + tf_avg + tf_std, fc=plot_color, |
| 205 | + alpha = 0.5) |
| 206 | + else: |
| 207 | + plot_ax.plot(plot_times + start_year, tf_depth, c=plot_color, linestyle=linestyle) |
| 208 | + |
| 209 | + |
| 210 | +def plot_smb(smb_file, plot_ax): |
| 211 | + smb_data = Dataset(smb_file, 'r') |
| 212 | + smb_data.set_always_mask(False) |
| 213 | + smb = smb_data.variables["sfcMassBal"][:, ice_mask] |
| 214 | + smb_tot = np.sum(smb * areaCell[ice_mask] * scyr / 1.e12, axis=1) # Gt/yr |
| 215 | + |
| 216 | + n_time_levs = smb_data.dimensions["Time"].size |
| 217 | + smb_data.close() |
| 218 | + if times[1] == -1: |
| 219 | + times[1] = n_time_levs - 1 |
| 220 | + plot_times = np.arange(times[0], times[1], step=1) # annual posting |
| 221 | + |
| 222 | + # filter smb for plotting |
| 223 | + filtered_smb = np.ones_like(smb_tot) |
| 224 | + filtered_smb_std = np.ones_like(smb_tot) |
| 225 | + window_width_years = 10 |
| 226 | + for time in range(1, n_time_levs): |
| 227 | + n_t = min(time, window_width_years) |
| 228 | + filtered_smb[time] = np.mean(smb_tot[time-n_t:time]) |
| 229 | + filtered_smb_std[time] = np.std(smb_tot[time-n_t:time]) |
| 230 | + |
| 231 | + if "UKESM" in smb_file: |
| 232 | + if "SSP126" in tf_file: |
| 233 | + plot_color = 'tab:green' |
| 234 | + else: |
| 235 | + plot_color = 'tab:blue' |
| 236 | + else: |
| 237 | + plot_color = 'tab:grey' |
| 238 | + |
| 239 | + if "CESM" in smb_file: |
| 240 | + linestyle = "dashed" |
| 241 | + elif "CCSM" in smb_file: |
| 242 | + linestyle = "dotted" |
| 243 | + else: |
| 244 | + linestyle = "solid" |
| 245 | + |
| 246 | + plot_smb = filtered_smb[plot_times[0]:plot_times[-1]+1] |
| 247 | + plot_smb_std = filtered_smb_std[plot_times[0]:plot_times[-1]+1] |
| 248 | + |
| 249 | + plot_ax.plot(plot_times + start_year, plot_smb, c=plot_color, linestyle=linestyle) |
| 250 | + plot_ax.fill_between(plot_times + start_year, plot_smb - plot_smb_std, |
| 251 | + plot_smb + plot_smb_std, fc=plot_color, |
| 252 | + alpha = 0.5) |
| 253 | + |
| 254 | + |
| 255 | +tf_files = [i for i in options.thermal_forcing_file.split(',')] |
| 256 | +smb_files = [i for i in options.smb_file.split(',')] |
| 257 | +for tf_file, smb_file in zip(tf_files, smb_files): |
| 258 | + print(f"Plotting from {tf_file}") |
| 259 | + interp_and_plot_tf(tf_file, ax[0]) |
| 260 | + print(f"Plotting from {smb_file}") |
| 261 | + plot_smb(smb_file, ax[1]) |
| 262 | + |
| 263 | +ax[0].set_xlabel("Year") |
| 264 | +ax[0].set_ylabel("Thermal forcing (°C)") |
| 265 | +ax[0].grid('on') |
| 266 | +ax[1].set_xlabel("Year") |
| 267 | +ax[1].set_ylabel("Total surface mass balance (Gt yr$^{-1}$)") |
| 268 | +ax[1].grid('on') |
| 269 | +if options.save_filename is not None: |
| 270 | + fig.savefig(options.save_filename, dpi=400, bbox_inches='tight') |
| 271 | + fig.savefig(options.save_filename + ".pdf", format='pdf', bbox_inches='tight') |
| 272 | +plt.show() |
0 commit comments