Skip to content

Commit 87b5c21

Browse files
uwaguraUtheri WaguraUtheri WaguraUtheri WaguraUtheri Wagura
authored
Generalize sst_trends (NOAA-GFDL#99)
* sst_trends now uses config file and fixed typo in plot common * Refactored process_glorys function to be compatible with resampling, adding logging * Added type hints to process_glorys to clarify potential return values * Regrid mom to oisst, change plotting and metric variables to reflect new regridder * Read rename variables from config file so that target grid contains corner points * Removed do_regrid option from process_oisst * Simplified get_3d_trends, passed it to process_glorys to avoid simplify it's return values --------- Co-authored-by: Utheri Wagura <Utheri.Wagura@an102.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an202.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an103.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an104.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an005.princeton.rdhpcs.noaa.gov> Co-authored-by: Yi-Cheng Teng - NOAA GFDL <143743249+yichengt900@users.noreply.github.com>
1 parent 9675e2b commit 87b5c21

File tree

3 files changed

+137
-64
lines changed

3 files changed

+137
-64
lines changed

diagnostics/physics/config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,13 @@ levels_step: 2
6565
# Colorbar for sst difference plots
6666
bias_min: -2
6767
bias_max: 2.1
68+
bias_min_trends: -1.5
69+
bias_max_trends: 1.51
6870
bias_step: 0.25
71+
72+
ticks: [-2, -1, 0, 1, 2]
73+
74+
75+
# SST Trends Settings
76+
start_year: "2005"
77+
end_year: "2019"

diagnostics/physics/plot_common.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,14 @@ def load_config(config_path: str):
176176
logger.error(f"Error loading configuration from {config_path}: {e}")
177177
raise
178178

179-
def process_oisst(config, target_grid, model_ave):
179+
def process_oisst(config, target_grid, model_ave, start=1993, end = 2020, resamp_freq = None):
180180
"""Open and regrid OISST dataset, return relevant vars from dataset."""
181181
try:
182182
oisst = (
183-
xarray.open_mfdataset([config['oisst'] + f'sst.month.mean.{y}.nc' for y in range(1993, 2020)])
183+
xarray.open_mfdataset([config['oisst'] + f'sst.month.mean.{y}.nc' for y in range(start, end)])
184184
.sst
185185
.sel(lat=slice(config['lat']['south'], config['lat']['north']), lon=slice(config['lon']['west'], config['lon']['east']))
186+
.load()
186187
)
187188
except Exception as e:
188189
logger.error(f"Error processing OISST data: {e}")
@@ -193,20 +194,33 @@ def process_oisst(config, target_grid, model_ave):
193194

194195
oisst_lonc, oisst_latc = corners(oisst.lon, oisst.lat)
195196
oisst_lonc -= 360
197+
196198
mom_to_oisst = xesmf.Regridder(
197199
target_grid,
198200
{'lat': oisst.lat, 'lon': oisst.lon, 'lat_b': oisst_latc, 'lon_b': oisst_lonc},
199201
method='conservative_normed',
200202
unmapped_to_nan=True
201203
)
202204

203-
oisst_ave = oisst.mean('time').load()
205+
# If a resample frequency is provided, use it to resample the oisst data over time before taking the average
206+
if resamp_freq:
207+
oisst = oisst.resample( time = resamp_freq )
208+
209+
oisst_ave = oisst.mean('time')
210+
204211
mom_rg = mom_to_oisst(model_ave)
205212
logger.info("OISST data processed successfully.")
206213
return mom_rg, oisst_ave, oisst_lonc, oisst_latc
207214

208-
def process_glorys(config, target_grid, var):
209-
""" Open and regrid glorys data, return regridded glorys data """
215+
def process_glorys(config, target_grid, var, sel_time = None, resamp_freq = None, preprocess_regrid = None):
216+
"""
217+
Open and regrid glorys data, return regridded glorys data
218+
If a function is passed to the preprocess_regrid option, it will be called on the
219+
data before it is passed to the regridder but after the regridder
220+
is created and the average is calculated
221+
NOTE: if preprocess_regrid returns numpy array, the return value of glorys_ave will
222+
be a numpy array, not an xarray dataarray as is the default
223+
"""
210224
glorys = xarray.open_dataset( config['glorys'] ).squeeze(drop=True) #.rename({'longitude': 'lon', 'latitude': 'lat'})
211225
if var in glorys:
212226
glorys = glorys[var]
@@ -225,15 +239,30 @@ def process_glorys(config, target_grid, var):
225239
logger.info("Glorys data is using longitude/latitude")
226240
except:
227241
logger.error("Name of longitude and latitude variables is unknown")
228-
raise Exception("Error: Lat/Latitude, Lon/Longitdue not found in glorys data")
242+
raise Exception("Error: Lat/Latitude, Lon/Longitude not found in glorys data")
243+
244+
# If a time slice is provided use it to select a portion of the glorys data
245+
if sel_time:
246+
glorys = glorys.sel( time = sel_time )
247+
248+
# If a resample frequency is provided, use it to resample the glorys data over time before taking the average
249+
if resamp_freq:
250+
glorys = glorys.resample(time = resamp_freq)
229251

230252
glorys_ave = glorys.mean('time').load()
253+
231254
glorys_to_mom = xesmf.Regridder(glorys_ave, target_grid, method='bilinear', unmapped_to_nan=True)
232-
glorys_rg = glorys_to_mom(glorys_ave)
233255

256+
# If a preprocessing function is provided, call it before doing any regridding
257+
# glorys_ave may not remain a xarray dataset after this step
258+
if preprocess_regrid:
259+
glorys_ave = preprocess_regrid(glorys_ave)
260+
261+
glorys_rg = glorys_to_mom(glorys_ave)
234262
logger.info("Glorys data processed successfully.")
235263
return glorys_rg, glorys_ave, glorys_lonc, glorys_latc
236264

265+
237266
def get_end_of_climatology_period(clima_file):
238267
"""
239268
Determine the time period covered by the last climatology file. This function is needed

diagnostics/physics/sst_trends.py

Lines changed: 92 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Compare the model 2005-019 sea surface temperature trends from OISST and GLORYS.
33
How to use:
4-
python sst_trends.py /archive/acr/fre/NWA/2023_04/NWA12_COBALT_2023_04_kpo4-coastatten-physics/gfdl.ncrc5-intel22-prod
4+
python sst_trends.py -p /archive/acr/fre/NWA/2023_04/NWA12_COBALT_2023_04_kpo4-coastatten-physics/gfdl.ncrc5-intel22-prod -c config.yaml
55
"""
66
import cartopy.crs as ccrs
77
from cartopy.mpl.geoaxes import GeoAxes
@@ -10,116 +10,147 @@
1010
from mpl_toolkits.axes_grid1 import AxesGrid
1111
import numpy as np
1212
import xarray
13-
import xesmf
13+
import logging
1414

15-
from plot_common import autoextend_colorbar, corners, get_map_norm, annotate_skill, open_var, save_figure
15+
from plot_common import( autoextend_colorbar, corners, get_map_norm,
16+
annotate_skill, open_var, save_figure, load_config,
17+
process_glorys, process_oisst)
1618

17-
PC = ccrs.PlateCarree()
19+
# Configure logging for sst_eval
20+
logger = logging.getLogger(__name__)
21+
logging.basicConfig(filename="sst_trends.log", format='%(asctime)s %(levelname)s:%(name)s: %(message)s',level=logging.INFO)
1822

19-
20-
def get_3d_trends(x, y):
21-
x = np.array(x)
23+
def get_3d_trends(y):
24+
x = np.array( y['time.year'] )
2225
y2 = np.array(y).reshape((len(x), -1))
2326
coefs = np.polyfit(x, y2, 1)
24-
trends = coefs[0, :].reshape(y.shape[1:])
27+
trends = coefs[0, :].reshape(y.shape[1:]) * 10 # -> C / decade
28+
2529
return trends
2630

2731

28-
def plot_sst_trends(pp_root, label):
32+
def plot_sst_trends(pp_root, label, config):
2933
model = (
3034
open_var(pp_root, 'ocean_monthly', 'tos')
31-
.sel(time=slice('2005', '2019'))
35+
.sel(time=slice(config['start_year'], config['end_year']))
3236
.resample(time='1AS')
3337
.mean('time')
3438
.load()
3539
)
36-
model_grid = xarray.open_dataset('../data/geography/ocean_static.nc')
40+
logger.info("MODEL: %s",model)
41+
model_grid = xarray.open_dataset( config['model_grid'])
42+
logger.info("MODEL_GRID: %s",model_grid)
43+
44+
# Verify that xh/yh are set as coordinates, then make sure model coordinates match grid data
45+
model_grid = model_grid.assign_coords( {'xh':model_grid.xh, 'yh':model_grid.yh } )
46+
model = xarray.align(model_grid, model, join='override', exclude='time')[1]
47+
logger.info("Successfully modified coordinates of model grid, and aligned model coordinates to grid coordinates")
3748

38-
model_trend = get_3d_trends(model['time.year'], model) * 10 # -> C / decade
49+
model_trend = get_3d_trends(model)
50+
# Convert to Data Array, since xskillscore expects dataarrays to calculate skill metrics
3951
model_trend = xarray.DataArray(model_trend, dims=['yh', 'xh'], coords={'yh': model.yh, 'xh': model.xh})
52+
logger.info("MODEL_TREND: %s", model_trend)
4053

41-
oisst = (
42-
xarray.open_mfdataset([f'/work/acr/oisstv2/sst.month.mean.{y}.nc' for y in range(2005, 2020)])
43-
.sst
44-
.sel(lat=slice(0, 60), lon=slice(360-100, 360-30))
45-
.resample(time='1AS')
46-
.mean('time')
47-
.load()
48-
)
49-
oisst_trend = get_3d_trends(oisst['time.year'], oisst) * 10 # -> C / decade
54+
target_grid = model_grid[ config['rename_map'].keys() ].rename( config['rename_map'] )
5055

51-
glorys = (
52-
xarray.open_dataset('/work/acr/mom6/diagnostics/glorys/glorys_sfc.nc')
53-
['thetao']
54-
.sel(time=slice('2005', '2019'))
55-
.resample(time='1AS')
56-
.mean('time')
57-
)
58-
glorys_trend = get_3d_trends(glorys['time.year'], glorys) * 10 # -> C / decade
56+
# Process OISST and get trend
57+
mom_rg, oisst, oisst_lonc, oisst_latc = process_oisst(config, target_grid, model_trend, start = int(config['start_year']),
58+
end = int(config['end_year'])+1, resamp_freq = '1AS')
59+
logger.info("OISST: %s", oisst )
60+
oisst_trend = get_3d_trends(oisst)
61+
oisst_trend = xarray.DataArray(oisst_trend, dims=['lat','lon'], coords={'lat':oisst.lat,'lon':oisst.lon} )
62+
logger.info("OISST_TREND: %s",oisst_trend)
5963

60-
oisst_lonc, oisst_latc = corners(oisst.lon, oisst.lat)
61-
oisst_lonc -= 360
62-
oisst_to_mom = xesmf.Regridder({'lat': oisst.lat, 'lon': oisst.lon}, model_grid[['geolon', 'geolat']].rename({'geolon': 'lon', 'geolat': 'lat'}), method='bilinear')
64+
oisst_delta = mom_rg - oisst_trend
65+
logger.info("MOM_RG: %s",mom_rg)
66+
logger.info("OISST_DELTA: %s",oisst_delta)
6367

64-
glorys_lonc, glorys_latc = corners(glorys.lon, glorys.lat)
65-
glorys_to_mom = xesmf.Regridder(glorys, model_grid[['geolon', 'geolat']].rename({'geolon': 'lon', 'geolat': 'lat'}), method='bilinear')
68+
# Process Glorys and get trend
69+
# NOTE: Glorys_ave is glorys_trends because we call get_3d_trends on it.
70+
glorys_rg, glorys_trend, glorys_lonc, glorys_latc = process_glorys(config, target_grid, 'thetao',
71+
sel_time = slice(config['start_year'], config['end_year']),
72+
resamp_freq = '1AS', preprocess_regrid = get_3d_trends)
73+
logger.info("GLORYS_TREND: %s",glorys_trend)
6674

67-
glorys_rg = glorys_to_mom(glorys_trend)
6875
glorys_rg = xarray.DataArray(glorys_rg, dims=['yh', 'xh'], coords={'yh': model.yh, 'xh': model.xh})
6976
glorys_delta = model_trend - glorys_rg
77+
logger.info("GLORYS_RG: %s",glorys_rg)
78+
logger.info("GLORYS_DELTA: %s",glorys_delta)
7079

71-
oisst_rg = oisst_to_mom(oisst_trend)
72-
oisst_rg = xarray.DataArray(oisst_rg, dims=['yh', 'xh'], coords={'yh': model.yh, 'xh': model.xh})
73-
oisst_delta = model_trend - oisst_rg
80+
# Set projection of each grid in the plot
81+
# For now, sst_eval.py will only support a projection for the arctic and a projection for all other domains
82+
if config['projection_grid'] == 'NorthPolarStereo':
83+
p = ccrs.NorthPolarStereo()
84+
else:
85+
p = ccrs.PlateCarree()
7486

7587
fig = plt.figure(figsize=(10, 14))
7688
grid = AxesGrid(fig, 111,
7789
nrows_ncols=(2, 3),
78-
axes_class = (GeoAxes, dict(projection=PC)),
90+
axes_class = (GeoAxes, dict(projection=p)),
7991
axes_pad=0.3,
8092
cbar_location='bottom',
8193
cbar_mode='edge',
8294
cbar_pad=0.2,
8395
cbar_size='15%',
84-
label_mode=''
96+
label_mode='keep'
8597
)
98+
logger.info("Successfully created grid")
8699

87-
cmap, norm = get_map_norm('cet_CET_D1', np.arange(-2, 2.1, .25), no_offset=True)
100+
cmap, norm = get_map_norm('cet_CET_D1', np.arange(config['bias_min'], config['bias_max'], config['bias_step']), no_offset=True)
88101
common = dict(cmap=cmap, norm=norm)
89102

90-
bias_cmap, bias_norm = get_map_norm('RdBu_r', np.arange(-1.5, 1.51, .25), no_offset=True)
103+
bias_cmap, bias_norm = get_map_norm('RdBu_r', np.arange(config['bias_min_trends'], config['bias_max_trends'], config['bias_step']), no_offset=True)
91104
bias_common = dict(cmap=bias_cmap, norm=bias_norm)
92105

93-
p0 = grid[0].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, model_trend, **common)
106+
# Set projection of input data files so that data is correctly tranformed when plotting
107+
# For now, sst_eval.py will only support a projection for the arctic and a projection for all other domains
108+
if config['projection_data'] == 'NorthPolarStereo':
109+
proj = ccrs.NorthPolarStereo()
110+
else:
111+
proj = ccrs.PlateCarree()
112+
113+
# MODEL
114+
p0 = grid[0].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, model_trend, transform = proj, **common)
94115
grid[0].set_title('(a) Model')
95116
cbar0 = autoextend_colorbar(grid.cbar_axes[0], p0)
96117
cbar0.ax.set_xlabel('SST trend (°C / decade)')
97-
cbar0.set_ticks([-2, -1, 0, 1, 2])
98-
cbar0.set_ticklabels([-2, -1, 0, 1, 2])
118+
cbar0.set_ticks( config['ticks'] )
119+
cbar0.set_ticklabels( config['ticks'] )
120+
logger.info("Successfully plotted model data")
99121

100-
p1 = grid[1].pcolormesh(oisst_lonc, oisst_latc, oisst_trend, **common)
122+
# OISST
123+
p1 = grid[1].pcolormesh(oisst_lonc, oisst_latc, oisst_trend, transform = proj, **common)
101124
grid[1].set_title('(b) OISST')
125+
logger.info("Successfully plotted oisst")
102126

103-
grid[2].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, oisst_delta, **bias_common)
127+
# MODEL - OISST
128+
grid[2].pcolormesh(oisst_lonc, oisst_latc, oisst_delta, transform = proj, **bias_common)
104129
grid[2].set_title('(c) Model - OISST')
105-
annotate_skill(model_trend, oisst_rg, grid[2], weights=model_grid.areacello)
130+
# NOTE: Oisst dims are [lat,lon], so dim argument is needed. Must use mom_rg though, since oisst also contains
131+
# an extra time dimension that changes output of xskillscore functions and leads to error when annotating plot
132+
annotate_skill(mom_rg, oisst_trend, grid[2], dim= list(mom_rg.dims), x0=config['text_x'], y0=config['text_y'], xint=config['text_xint'], plot_lat=config['plot_lat'])
133+
logger.info("Successfully plotted difference between model and oisst")
106134

107-
grid[4].pcolormesh(glorys_lonc, glorys_latc, glorys_trend, **common)
135+
# GLORYS
136+
grid[4].pcolormesh(glorys_lonc, glorys_latc, glorys_trend, transform = proj, **common)
108137
grid[4].set_title('(d) GLORYS12')
109138
cbar1 = autoextend_colorbar(grid.cbar_axes[1], p1)
110139
cbar1.ax.set_xlabel('SST trend (°C / decade)')
111-
cbar1.set_ticks([-2, -1, 0, 1, 2])
112-
cbar1.set_ticklabels([-2, -1, 0, 1, 2])
140+
cbar1.set_ticks( config['ticks'] )
141+
cbar1.set_ticklabels( config['ticks'] )
142+
logger.info("Successfully plotted glorys")
113143

114-
p2 = grid[5].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, glorys_delta, **bias_common)
144+
# MODEL - GLORYS
145+
p2 = grid[5].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, glorys_delta, transform = proj, **bias_common)
115146
grid[5].set_title('(e) Model - GLORYS12')
116147
cbar2 = autoextend_colorbar(grid.cbar_axes[2], p2)
117148
cbar2.ax.set_xlabel('SST trend difference (°C / decade)')
118-
annotate_skill(model_trend, glorys_rg, grid[5], weights=model_grid.areacello)
149+
annotate_skill(model_trend, glorys_rg, grid[5], weights=model_grid.areacello, x0=config['text_x'], y0=config['text_y'], xint=config['text_xint'], plot_lat=config['plot_lat'])
150+
logger.info("Successfully plotted difference between glorys and model")
119151

120152
for i, ax in enumerate(grid):
121-
ax.set_xlim(-99, -35)
122-
ax.set_ylim(4, 59)
153+
ax.set_extent([ config['x']['min'], config['x']['max'], config['y']['min'], config['y']['max'] ], crs=proj)
123154
ax.set_xticks([])
124155
ax.set_yticks([])
125156
ax.set_xticklabels([])
@@ -128,14 +159,18 @@ def plot_sst_trends(pp_root, label):
128159
ax.set_facecolor('#bbbbbb')
129160
for s in ax.spines.values():
130161
s.set_visible(False)
162+
logger.info("Successfully set extent of each axis")
131163

132164
save_figure('sst_trends', label=label)
165+
logger.info("Successfully saved figure")
133166

134167

135168
if __name__ == '__main__':
136169
from argparse import ArgumentParser
137170
parser = ArgumentParser()
138-
parser.add_argument('pp_root', help='Path to postprocessed data (up to but not including /pp/)')
171+
parser.add_argument('-p','--pp_root', help='Path to postprocessed data (up to but not including /pp/)', required = True)
172+
parser.add_argument('-c','--config', help='Path to yaml config file', required = True)
139173
parser.add_argument('-l', '--label', help='Label to add to figure file names', type=str, default='')
140174
args = parser.parse_args()
141-
plot_sst_trends(args.pp_root, args.label)
175+
config = load_config(args.config)
176+
plot_sst_trends(args.pp_root, args.label, config)

0 commit comments

Comments
 (0)