Skip to content

Commit 33500c1

Browse files
uwaguraUtheri WaguraUtheri WaguraUtheri WaguraUtheri Wagura
authored
Add config file for diagnostic scripts and use it to generalize sst_eval.py (#74)
* Added config file for diagnostic scripts, modified sst_eval to use config file * Added options to set location of annotations and fig size, used rename_map keys instead of separate list * Added option to choose projection * Added transforms and changed x/ylim to setextent to handle different projections * Added else statement to try/except * Abstracted glorys/oisst processing to function, added logging, added option to select data projection * Align model and model grid before regridding, remove else statement from process_glorys * Added support for plotting along lines of latitude, removed corners from sst_eval import list * Dropped lat points that caused regridding error, factored out common line from annotate function, addressed label_mode error * Made command line flags required --------- Co-authored-by: Utheri Wagura <Utheri.Wagura@an105.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an002.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an202.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an200.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an106.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an102.princeton.rdhpcs.noaa.gov> Co-authored-by: Utheri Wagura <Utheri.Wagura@an104.princeton.rdhpcs.noaa.gov>
1 parent d63458c commit 33500c1

3 files changed

Lines changed: 222 additions & 56 deletions

File tree

diagnostics/physics/config.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
figures_dir: 'figures/'
2+
glorys: '/work/acr/mom6/diagnostics/glorys/glorys_sfc.nc'
3+
model_grid: '../data/geography/ocean_static.nc'
4+
5+
# Variables to rename
6+
rename_map:
7+
geolon: lon
8+
geolat: lat
9+
geolon_c: lon_b
10+
geolat_c: lat_b
11+
12+
# Domain and plotting details
13+
domain: ocean_monthly
14+
15+
# Lat/lon range
16+
lat:
17+
south: 0
18+
north: 60
19+
20+
lon:
21+
west: 260
22+
east: 330
23+
24+
# Xlim/ylim to plot
25+
x:
26+
min: -99
27+
max: -35
28+
29+
y:
30+
min: 4
31+
max: 59
32+
33+
# Projection ( current options are either NorthPolarStereo for the arctic, or PlateCarree for all other domains
34+
projection_grid: 'PlateCarree'
35+
projection_data: 'PlateCarree'
36+
37+
# Figure size
38+
fig_width: 11
39+
fig_height: 14
40+
41+
# Location of skill score annotations in plot
42+
text_x: -98.5
43+
text_y: 54
44+
45+
# Space between skill score text
46+
text_xint: 4 # This is unused if col=1, so it is only included for completeness
47+
text_yint: 4
48+
plot_lat: False
49+
50+
# SST_eval settings
51+
oisst: '/work/acr/oisstv2/'
52+
53+
# Colorbar for sst plots
54+
levels_min: 2
55+
levels_max: 31
56+
levels_step: 2
57+
58+
# Colorbar for sst difference plots
59+
bias_min: -2
60+
bias_max: 2.1
61+
bias_step: 0.25

diagnostics/physics/plot_common.py

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
import os
88
import xarray
99
import xskillscore
10+
import logging
11+
import yaml
12+
import xesmf
13+
14+
# Configure logging for plot_common
15+
logger = logging.getLogger(__name__)
1016

1117
def center_to_outer(center, left=None, right=None):
1218
"""
@@ -46,25 +52,39 @@ def get_map_norm(cmap, levels, no_offset=True):
4652
norm = BoundaryNorm(levels, ncolors=nlev, clip=False)
4753
return cmap, norm
4854

49-
def annotate_skill(model, obs, ax, dim=['yh', 'xh'], x0=-98.5, y0=54, yint=4, xint=4, weights=None, cols=1, **kwargs):
55+
def annotate_skill(model, obs, ax, dim=['yh', 'xh'], x0=-98.5, y0=54, yint=4, xint=4, weights=None, cols=1, proj = ccrs.PlateCarree(), plot_lat=False,**kwargs):
5056
"""
5157
Annotate an axis with model vs obs skill metrics
5258
"""
5359
bias = xskillscore.me(model, obs, dim=dim, skipna=True, weights=weights)
5460
rmse = xskillscore.rmse(model, obs, dim=dim, skipna=True, weights=weights)
5561
corr = xskillscore.pearson_r(model, obs, dim=dim, skipna=True, weights=weights)
5662
medae = xskillscore.median_absolute_error(model, obs, dim=dim, skipna=True)
57-
ax.text(x0, y0, f'Bias: {float(bias):2.2f}', **kwargs)
58-
ax.text(x0, y0-yint, f'RMSE: {float(rmse):2.2f}', **kwargs)
59-
if cols == 1:
60-
ax.text(x0, y0-yint*2, f'MedAE: {float(medae):2.2f}', **kwargs)
61-
ax.text(x0, y0-yint*3, f'Corr: {float(corr):2.2f}', **kwargs)
62-
elif cols == 2:
63-
ax.text(x0+xint, y0, f'MedAE: {float(medae):2.2f}', **kwargs)
64-
ax.text(x0+xint, y0-yint, f'Corr: {float(corr):2.2f}', **kwargs)
63+
64+
ax.text(x0, y0, f'Bias: {float(bias):2.2f}', transform=proj, **kwargs)
65+
# Set plot_lat=True in order to plot skill along a line of latitude. Otherwise, plot along longitude
66+
if plot_lat:
67+
ax.text(x0-xint, y0, f'RMSE: {float(rmse):2.2f}', transform=proj, **kwargs)
68+
if cols == 1:
69+
ax.text(x0-xint*2, y0, f'MedAE: {float(medae):2.2f}', transform=proj, **kwargs)
70+
ax.text(x0-xint*3, y0, f'Corr: {float(corr):2.2f}', transform=proj, **kwargs)
71+
elif cols == 2:
72+
ax.text(x0, y0+yint, f'MedAE: {float(medae):2.2f}', transform=proj, **kwargs)
73+
ax.text(x0-xint, y0+yint, f'Corr: {float(corr):2.2f}', transform=proj, **kwargs)
74+
else:
75+
raise ValueError(f'Unsupported number of columns: {cols}')
76+
6577
else:
66-
raise ValueError(f'Unsupported number of columns: {cols}')
67-
78+
ax.text(x0, y0-yint, f'RMSE: {float(rmse):2.2f}', transform=proj, **kwargs)
79+
if cols == 1:
80+
ax.text(x0, y0-yint*2, f'MedAE: {float(medae):2.2f}', transform=proj, **kwargs)
81+
ax.text(x0, y0-yint*3, f'Corr: {float(corr):2.2f}', transform=proj, **kwargs)
82+
elif cols == 2:
83+
ax.text(x0+xint, y0, f'MedAE: {float(medae):2.2f}', transform=proj, **kwargs)
84+
ax.text(x0+xint, y0-yint, f'Corr: {float(corr):2.2f}', transform=proj, **kwargs)
85+
else:
86+
raise ValueError(f'Unsupported number of columns: {cols}')
87+
6888
def autoextend_colorbar(ax, plot, plot_array=None, **kwargs):
6989
"""
7090
Add a colorbar, setting the extend metric based on
@@ -141,3 +161,67 @@ def save_figure(fname, label='', pdf=False, output_dir='figures'):
141161
plt.savefig(os.path.join(output_dir, f'{fname}_{label}.png'), dpi=200, bbox_inches='tight')
142162
if pdf:
143163
plt.savefig(os.path.join(output_dir, f'{fname}_{label}.pdf'), bbox_inches='tight')
164+
165+
def load_config(config_path: str):
166+
"""Load the configuration file."""
167+
try:
168+
with open(config_path, 'r') as f:
169+
config = yaml.safe_load(f)
170+
logger.info(f"Loaded configuration from {config_path}")
171+
return config
172+
except Exception as e:
173+
logger.error(f"Error loading configuration from {config_path}: {e}")
174+
raise
175+
176+
def process_oisst(config, target_grid, model_ave):
177+
"""Open and regrid OISST dataset, return relevant vars from dataset."""
178+
try:
179+
oisst = (
180+
xarray.open_mfdataset([config['oisst'] + f'sst.month.mean.{y}.nc' for y in range(1993, 2020)])
181+
.sst
182+
.sel(lat=slice(config['lat']['south'], config['lat']['north']), lon=slice(config['lon']['west'], config['lon']['east']))
183+
)
184+
except Exception as e:
185+
logger.error(f"Error processing OISST data: {e}")
186+
raise e("Could not open OISST dataset")
187+
188+
# Drop any latitude points greater than or equal to 90 to prevent errors when regridding
189+
oisst = oisst.where( oisst.lat < 90, drop = True)
190+
191+
oisst_lonc, oisst_latc = corners(oisst.lon, oisst.lat)
192+
oisst_lonc -= 360
193+
mom_to_oisst = xesmf.Regridder(
194+
target_grid,
195+
{'lat': oisst.lat, 'lon': oisst.lon, 'lat_b': oisst_latc, 'lon_b': oisst_lonc},
196+
method='conservative_normed',
197+
unmapped_to_nan=True
198+
)
199+
200+
oisst_ave = oisst.mean('time').load()
201+
mom_rg = mom_to_oisst(model_ave)
202+
logger.info("OISST data processed successfully.")
203+
return mom_rg, oisst_ave, oisst_lonc, oisst_latc
204+
205+
def process_glorys(config, target_grid):
206+
""" Open and regrid glorys data, return regridded glorys data """
207+
glorys = xarray.open_dataset( config['glorys'] ).squeeze(drop=True)['thetao'] #.rename({'longitude': 'lon', 'latitude': 'lat'})
208+
209+
# Drop any latitude points greater than or equal to 90 to prevent errors when regridding, then get corner points
210+
try:
211+
glorys = glorys.where( glorys.lat < 90, drop = True)
212+
glorys_lonc, glorys_latc = corners(glorys.lon, glorys.lat)
213+
logger.info("Glorys data is using lon/lat")
214+
except AttributeError:
215+
glorys = glorys.where( glorys.latitude < 90, drop = True)
216+
glorys_lonc, glorys_latc = corners(glorys.longitude, glorys.latitude)
217+
logger.info("Glorys data is using longitude/latitude")
218+
except:
219+
logger.error("Name of longitude and latitude variables is unknown")
220+
raise Exception("Error: Lat/Latitude, Lon/Longitdue not found in glorys data")
221+
222+
glorys_ave = glorys.mean('time').load()
223+
glorys_to_mom = xesmf.Regridder(glorys_ave, target_grid, method='bilinear', unmapped_to_nan=True)
224+
glorys_rg = glorys_to_mom(glorys_ave)
225+
226+
logger.info("Glorys data processed successfully.")
227+
return glorys_rg, glorys_ave, glorys_lonc, glorys_latc

diagnostics/physics/sst_eval.py

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Uses whatever model data can be found within the directory pp_root,
44
and does not try to match the model and observed time periods.
55
How to use:
6-
python sst_eval.py /archive/acr/fre/NWA/2023_04/NWA12_COBALT_2023_04_kpo4-coastatten-physics/gfdl.ncrc5-intel22-prod
6+
python sst_eval.py -p /archive/acr/fre/NWA/2023_04/NWA12_COBALT_2023_04_kpo4-coastatten-physics/gfdl.ncrc5-intel22-prod -c config.yaml
77
"""
88

99
import cartopy.crs as ccrs
@@ -12,55 +12,61 @@
1212
from mpl_toolkits.axes_grid1 import AxesGrid
1313
import numpy as np
1414
import xarray
15-
import xesmf
15+
import logging
1616

17-
from plot_common import annotate_skill, autoextend_colorbar, corners, get_map_norm, open_var
17+
from plot_common import annotate_skill, autoextend_colorbar, get_map_norm, open_var, load_config, process_oisst, process_glorys
1818

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

20-
def plot_sst_eval(pp_root):
21-
model = open_var(pp_root, 'ocean_monthly', 'tos')
22-
model_grid = xarray.open_dataset('../data/geography/ocean_static.nc')
23-
target_grid = model_grid[['geolon', 'geolat', 'geolon_c', 'geolat_c']].rename({'geolon': 'lon', 'geolat': 'lat', 'geolon_c': 'lon_b', 'geolat_c': 'lat_b'})
23+
def plot_sst_eval(pp_root, config):
24+
25+
model = open_var(pp_root, config['domain'], 'tos')
26+
model_grid = xarray.open_dataset( config['model_grid'] )
27+
target_grid = model_grid[config['rename_map'].keys()].rename(config['rename_map'])
2428
model_ave = model.mean('time').load()
29+
logger.info("MODEL_GRID: %s",model_grid)
30+
logger.info("MODEL_AVE: %s",model_ave)
31+
logger.info("Successfully opened model grid and took mean over time")
2532

26-
glorys = xarray.open_dataset('/work/acr/mom6/diagnostics/glorys/glorys_sfc.nc')['thetao'] #.rename({'longitude': 'lon', 'latitude': 'lat'})
27-
glorys_lonc, glorys_latc = corners(glorys.lon, glorys.lat)
28-
glorys_ave = glorys.mean('time').load()
29-
glorys_to_mom = xesmf.Regridder(glorys_ave, target_grid, method='bilinear', unmapped_to_nan=True)
30-
glorys_rg = glorys_to_mom(glorys_ave)
33+
# Verify that xh/yh are set as coordinates, then make sure model coordinates match grid data
34+
model_grid = model_grid.assign_coords( {'xh':model_grid.xh, 'yh':model_grid.yh } )
35+
model_ave = xarray.align(model_grid, model_ave,join='override')[1]
36+
37+
glorys_rg, glorys_ave, glorys_lonc, glorys_latc = process_glorys(config, target_grid)
3138
delta_glorys = model_ave - glorys_rg
39+
logger.info("GLORYS_RG: %s",glorys_rg)
40+
logger.info("DELTA_GLORYS: %s",delta_glorys)
3241

33-
oisst = (
34-
xarray.open_mfdataset([f'/work/acr/oisstv2/sst.month.mean.{y}.nc' for y in range(1993, 2020)])
35-
.sst
36-
.sel(lat=slice(0, 60), lon=slice(360-100, 360-30))
37-
)
38-
oisst_lonc, oisst_latc = corners(oisst.lon, oisst.lat)
39-
oisst_lonc -= 360
40-
mom_to_oisst = xesmf.Regridder(
41-
target_grid,
42-
{'lat': oisst.lat, 'lon': oisst.lon, 'lat_b': oisst_latc, 'lon_b': oisst_lonc},
43-
method='conservative_normed',
44-
unmapped_to_nan=True
45-
)
46-
oisst_ave = oisst.mean('time').load()
47-
mom_rg = mom_to_oisst(model_ave)
42+
mom_rg, oisst_ave, oisst_lonc, oisst_latc = process_oisst(config, target_grid, model_ave)
4843
delta_oisst = mom_rg - oisst_ave
44+
logger.info("OISST_AVE: %s",oisst_ave)
45+
logger.info("DELTA_OISST: %s",delta_oisst)
46+
47+
fig = plt.figure(figsize=(config['fig_width'], config['fig_height']))
48+
49+
# Set projection of each grid in the plot
50+
# For now, sst_eval.py will only support a projection for the arctic and a projection for all other domains
51+
if config['projection_grid'] == 'NorthPolarStereo':
52+
p = ccrs.NorthPolarStereo()
53+
else:
54+
p = ccrs.PlateCarree()
4955

50-
fig = plt.figure(figsize=(11, 14))
5156
grid = AxesGrid(fig, 111,
52-
axes_class=(GeoAxes, dict(projection=ccrs.PlateCarree())),
57+
axes_class=(GeoAxes, dict( projection = p )),
5358
nrows_ncols=(2, 3),
5459
axes_pad=0.3,
5560
cbar_location='bottom',
5661
cbar_mode='edge',
5762
cbar_pad=0.2,
5863
cbar_size='15%',
59-
label_mode=''
64+
label_mode='keep'
6065
)
66+
logger.info("Successfully created grid")
6167

6268
# Discrete levels and colorbar for SST plots
63-
levels = np.arange(2, 31, 2)
69+
levels = np.arange(config['levels_min'], config['levels_max'], config['levels_step'])
6470
try:
6571
import cmcrameri.cm as cmc
6672
except ModuleNotFoundError:
@@ -70,55 +76,70 @@ def plot_sst_eval(pp_root):
7076
cmap, norm = get_map_norm(cmap, levels=levels)
7177

7278
# Discrete levels and colorbar for difference plots
73-
bias_levels = np.arange(-2, 2.1, 0.25)
79+
bias_levels = np.arange(config['bias_min'], config['bias_max'], config['bias_step'])
7480
bias_cmap, bias_norm = get_map_norm('coolwarm', levels=bias_levels)
7581
bias_common = dict(cmap=bias_cmap, norm=bias_norm)
7682

83+
# Set projection of input data files so that data is correctly tranformed when plotting
84+
# For now, sst_eval.py will only support a projection for the arctic and a projection for all other domains
85+
if config['projection_data'] == 'NorthPolarStereo':
86+
proj = ccrs.NorthPolarStereo()
87+
else:
88+
proj = ccrs.PlateCarree()
89+
7790
# Model
78-
p0 = grid[0].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, model_ave, cmap=cmap, norm=norm)
91+
p0 = grid[0].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, model_ave, cmap=cmap, norm=norm, transform=proj)
7992
grid[0].set_title('(a) Model')
8093
cbar1 = grid.cbar_axes[0].colorbar(p0)
8194
cbar1.ax.set_xlabel('Mean SST (°C)')
95+
logger.info("Successfully plotted model data")
8296

8397
# OISST
84-
grid[1].pcolormesh(oisst_lonc, oisst_latc, oisst_ave, cmap=cmap, norm=norm)
98+
grid[1].pcolormesh(oisst_lonc, oisst_latc, oisst_ave, cmap=cmap, norm=norm, transform=proj)
8599
grid[1].set_title('(b) OISST')
100+
logger.info("Successfully plotted oisst")
86101

87102
# Model - OISST
88-
grid[2].pcolormesh(oisst_lonc, oisst_latc, delta_oisst, **bias_common)
103+
grid[2].pcolormesh(oisst_lonc, oisst_latc, delta_oisst, transform=proj,**bias_common)
89104
grid[2].set_title('(c) Model - OISST')
90-
annotate_skill(mom_rg, oisst_ave, grid[2], dim=['lat', 'lon'])
105+
annotate_skill(mom_rg, oisst_ave, grid[2], dim=['lat', 'lon'], x0=config['text_x'], y0=config['text_y'], xint=config['text_xint'], plot_lat=config['plot_lat'])
106+
logger.info("Successfully plotted difference between model and oisst")
91107

92108
# GLORYS
93-
p1 = grid[4].pcolormesh(glorys_lonc, glorys_latc, glorys_ave, cmap=cmap, norm=norm)
109+
p1 = grid[4].pcolormesh(glorys_lonc, glorys_latc, glorys_ave, cmap=cmap, norm=norm, transform=proj)
94110
grid[4].set_title('(d) GLORYS12')
95111
cbar1 = autoextend_colorbar(grid.cbar_axes[1], p1)
96112
cbar1.ax.set_xlabel('Mean SST (°C)')
113+
logger.info("Successfully plotted glorys")
97114

98115
# Model - GLORYS
99-
p2 = grid[5].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, delta_glorys, **bias_common)
116+
p2 = grid[5].pcolormesh(model_grid.geolon_c, model_grid.geolat_c, delta_glorys, transform=proj, **bias_common)
100117
cbar2 = autoextend_colorbar(grid.cbar_axes[2], p2)
101118
cbar2.ax.set_xlabel('SST difference (°C)')
102119
cbar2.ax.set_xticks([-2, -1, 0, 1, 2])
103120
grid[5].set_title('(e) Model - GLORYS12')
104-
annotate_skill(model_ave, glorys_rg, grid[5], weights=model_grid.areacello)
121+
annotate_skill(model_ave, 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'])
122+
logger.info("Successfully plotted difference between glorys and model")
105123

106124
for ax in grid:
107-
ax.set_xlim(-99, -35)
108-
ax.set_ylim(4, 59)
125+
ax.set_extent([ config['x']['min'], config['x']['max'], config['y']['min'], config['y']['max'] ], crs=proj )
109126
ax.set_xticks([])
110127
ax.set_yticks([])
111128
ax.set_xticklabels([])
112129
ax.set_yticklabels([])
113130
for s in ax.spines.values():
114131
s.set_visible(False)
115-
116-
plt.savefig('figures/sst_eval.png', dpi=300, bbox_inches='tight')
132+
logger.info("Successfully set extent of each axis")
133+
134+
plt.savefig(config['figures_dir']+'sst_eval.png', dpi=300, bbox_inches='tight')
135+
logger.info("Successfully saved figure")
117136

118137

119138
if __name__ == '__main__':
120139
from argparse import ArgumentParser
121140
parser = ArgumentParser()
122-
parser.add_argument('pp_root', help='Path to postprocessed data (up to but not including /pp/)')
141+
parser.add_argument('-p','--pp_root', type=str, help='Path to postprocessed data (up to but not including /pp/)', required=True)
142+
parser.add_argument('-c','--config', type=str, help='Path to config.yaml file containing relevant paths for diagnostic scripts', required=True)
123143
args = parser.parse_args()
124-
plot_sst_eval(args.pp_root)
144+
config = load_config(args.config)
145+
plot_sst_eval(args.pp_root, config)

0 commit comments

Comments
 (0)