Skip to content

Commit 811bbeb

Browse files
Merge pull request #566 from trhille/landice/plot_thermal_forcing
Add plotting script for thermal forcing and surface mass balance This merge adds a script that plots thermal forcing and surface mass balance time series. Currently the thermal forcing-related fields are hard-coded to ISMIP6 variable names, but that could be changed in the future. The user must specify files from which to plot thermal forcing and SMB; a mesh file containing coordinates, ice geometry, etc; and optionally a regions file that contains the regionCellMasks field. There are many (maybe too many) user-defined options for plotting: The option to plot at (i) a specified depth (or average over a depth range) using the -d argument, (ii) to interpolate the thermal forcing field to the seafloor (--seafloor), ice-shelf base (--shelf_base), or (iii) average over the full ice-shelf depth range (--shelf_range). The option to specify a csv file containing x and y coordinates for thermal forcing using the -c flag, or to specify coordinates in the command using -x and -y as comma-separated lists. The option to set start and end time indices using --start_time and --end_time. The option to take a random sample of point from the thermal forcing coordinates using --n_samples. This is just for prototyping, and the random samples are not currently reproducible. The option to average thermal forcing over all cells using the --average flag. If this flag is not set, a separate time series will be plotted for every cell, which will get messy when plotting more than a few cells. Note that averaging is not currently weighted by cell area. The option to save .png and .pdf files using the --save flag. The option to plot SMB for a specified region using the -n flag, which corresponds to the region number in regionCellMasks.
2 parents b0ac9f9 + c76d1bc commit 811bbeb

1 file changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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

Comments
 (0)