%load_ext autoreload
%autoreload 2
import xarray as xr
import xgcm
import matplotlib.pyplot as plt
Tutorial: analyzing finite-volume ocean model budgets with xbudget¶
This tutorial shows how to use xbudget to close and manipulate mass and tracer budgets in finite-volume.
Load example MOM6 dataset from Zenodo¶
from load_example_model_grid import load_MOM6_coarsened_diagnostics
grid = load_MOM6_coarsened_diagnostics()
File 'MOM6_global_example_diagnostics_zlevels_v0_0_6.nc' already exists at ../data/MOM6_global_example_diagnostics_zlevels_v0_0_6.nc. Skipping download.
Load budget metadata from preset MOM6 dictionary¶
import xbudget
recipe = xbudget.load_preset_budget(model="MOM6").copy()
We load in the file xbudget/recipes/MOM6.yaml, which comprehensively details the mass, heat, and salt budgets in MOM6, and how they can be evaluated using various combinations of diagnostics. In MOM6, most mass and tracer diagnostics are output as thickness tendencies and thickness-weighted tracer tendencies, respectively, and must be converted into density-weighted integrals over a finite-volume cell by multiplying by a seawater density and grid cell area.
For example, consider the structure of the heat budget:
xbudget.show_recipe(recipe, "heat")
heat
lhsΣ
Eulerian_tendency×
advectionΣ
lateral×
interfacial×
surface_ocean_flux_advective_negative_lhs×
rhsΣ
diffusionΣ
lateral×
interfacial×
surface_exchange_flux×Σ
×
Σ
nonadvectiveΣ
latent×
sensible×
longwave×
shortwave×
advective×
surface_ocean_flux_advective_negative_rhs×
bottom_flux×
frazil_ice×
The var: null entries in the dictionary denote where diagnostics are not directly available. In many cases, however, they can be reconstructed by taking the sum or product of available diagnostics.
We can use the xbudget.collect_budgets function to leverage the information in this metadata dictionary to fill in all of these gaps in the budget.
xbudget.collect_budgets(grid, recipe)
q = xbudget.BudgetQuery(grid, recipe)
Every reconstructed term is added to the original dataset (lazily), named after its path through the budget tree: the advection term under heat/lhs becomes heat_lhs_advection. Your metadata dictionary is left exactly as you wrote it.
BudgetQuery is how you ask what was built, without having to know the naming rule or search the dataset by eye.
q.aggregate()['heat']
{'lambda': 'thetao',
'surface_lambda': 'tos',
'lhs': {'Eulerian_tendency': 'heat_lhs_Eulerian_tendency',
'advection': 'heat_lhs_advection',
'surface_ocean_flux_advective_negative_lhs': 'heat_lhs_surface_ocean_flux_advective_negative_lhs'},
'rhs': {'diffusion': 'heat_rhs_diffusion',
'surface_exchange_flux': 'heat_rhs_surface_exchange_flux',
'surface_ocean_flux_advective_negative_rhs': 'heat_rhs_surface_ocean_flux_advective_negative_rhs',
'bottom_flux': 'heat_rhs_bottom_flux',
'frazil_ice': 'heat_rhs_frazil_ice'}}
aggregate gives the high-level terms of each budget. To go the other way – from a term to the variable holding it – use q.var:
grid._ds[q.var("heat_lhs_advection")]
<xarray.DataArray 'heat_lhs_advection' (time: 1, z_l: 35, yh: 180, xh: 240)> Size: 12MB
dask.array<add, shape=(1, 35, 180, 240), dtype=float64, chunksize=(1, 35, 100, 100), chunktype=numpy.ndarray>
Coordinates:
geolon (yh, xh) float64 346kB dask.array<chunksize=(100, 100), meta=np.ndarray>
lon (yh, xh) float64 346kB dask.array<chunksize=(100, 100), meta=np.ndarray>
geolat (yh, xh) float64 346kB dask.array<chunksize=(100, 100), meta=np.ndarray>
lat (yh, xh) float64 346kB dask.array<chunksize=(100, 100), meta=np.ndarray>
deptho (yh, xh) float32 173kB dask.array<chunksize=(100, 100), meta=np.ndarray>
wet (yh, xh) float32 173kB dask.array<chunksize=(100, 100), meta=np.ndarray>
* z_l (z_l) float64 280B 2.5 10.0 20.0 32.5 ... 5.5e+03 6e+03 6.5e+03
* time (time) object 8B 2000-07-01 00:00:00
areacello (yh, xh) float64 346kB dask.array<chunksize=(100, 100), meta=np.ndarray>
* xh (xh) int64 2kB 0 1 2 3 4 5 6 7 ... 233 234 235 236 237 238 239
* yh (yh) int64 1kB 0 1 2 3 4 5 6 7 ... 173 174 175 176 177 178 179
Attributes:
provenance: ['heat_lhs_advection_lateral', 'heat_lhs_advection_interfa...
xbudget_path: ['heat', 'lhs', 'advection']
xbudget_op: sumIf the variable is not immediately available as a diagnostic–but instead derived from other diagnostics–the provenance attribute points to the origin of the variable.
Note that q.var above and q.get_vars below are asking about the same variable: get_vars additionally reports the terms that feed into it. For variables that are the sum or product of other terms, that list is the decomposition:
q.get_vars("heat_lhs_advection")
{'var': 'heat_lhs_advection',
'sum': ['heat_lhs_advection_lateral', 'heat_lhs_advection_interfacial']}
The terms from which the variable is derived are not necessarily other xr.DataArray objects–they can also be constant numbers.
q.get_vars("heat_lhs_surface_ocean_flux_advective_negative_lhs")
{'var': 'heat_lhs_surface_ocean_flux_advective_negative_lhs',
'product': [-1.0,
3992.0,
'tos',
'boundary_forcing_h_tendency',
1035.0,
'areacello']}
get_vars takes either a single address or a list of them, and a raw diagnostic name works too (it reports which terms use it):
q.get_vars(["heat_lhs", "heat_rhs_diffusion", "boundary_forcing_heat_tendency"])
[{'var': 'heat_lhs',
'sum': ['heat_lhs_Eulerian_tendency',
'heat_lhs_advection',
'heat_lhs_surface_ocean_flux_advective_negative_lhs']},
{'var': 'heat_rhs_diffusion',
'sum': ['heat_rhs_diffusion_lateral', 'heat_rhs_diffusion_interfacial']},
{'var': 'boundary_forcing_heat_tendency',
'referenced_by': [('heat', 'rhs', 'surface_exchange_flux')]}]
Verification that the budgets close¶
We compare the left-hand-side (LHS) tendency terms against the sum of all of the right-hand-side (RHS) terms.
for eq, vmax in zip(["mass", "heat", "salt"], [1.e4, 1e12, 1.e5]):
plt.figure(figsize=(10, 4))
plt.subplot(1,2,1)
pc = grid._ds[q.var(f"{eq}_lhs")].isel(z_l = 0).isel(time=0).plot(vmin=-vmax, vmax=vmax, cmap="RdBu_r")
plt.title(f"LHS {eq} tendency")
plt.subplot(1,2,2)
pc = grid._ds[q.var(f"{eq}_rhs")].isel(z_l = 0).isel(time=0).plot(vmin=-vmax, vmax=vmax, cmap="RdBu_r")
plt.title(f"RHS {eq} tendency")
plt.tight_layout()
Decompose RHS budget terms with xbudget¶
Since the full budget is relatively clunky and can be decomposed in a number of different ways, we include helper functions for collecting all of the high-level terms and optionally dissaggreating terms of interest:
# Default high-level aggregate budget
simple_budgets = q.aggregate()
simple_budgets
{'mass': {'lambda': 'density',
'thickness': 'thkcello',
'lhs': {'Eulerian_tendency': 'mass_lhs_Eulerian_tendency'},
'rhs': {'advection': 'mass_rhs_advection',
'surface_exchange_flux': 'mass_rhs_surface_exchange_flux'}},
'heat': {'lambda': 'thetao',
'surface_lambda': 'tos',
'lhs': {'Eulerian_tendency': 'heat_lhs_Eulerian_tendency',
'advection': 'heat_lhs_advection',
'surface_ocean_flux_advective_negative_lhs': 'heat_lhs_surface_ocean_flux_advective_negative_lhs'},
'rhs': {'diffusion': 'heat_rhs_diffusion',
'surface_exchange_flux': 'heat_rhs_surface_exchange_flux',
'surface_ocean_flux_advective_negative_rhs': 'heat_rhs_surface_ocean_flux_advective_negative_rhs',
'bottom_flux': 'heat_rhs_bottom_flux',
'frazil_ice': 'heat_rhs_frazil_ice'}},
'salt': {'lambda': 'so',
'surface_lambda': 'sos',
'lhs': {'Eulerian_tendency': 'salt_lhs_Eulerian_tendency',
'advection': 'salt_lhs_advection',
'surface_ocean_flux_advective_negative_lhs': 'salt_lhs_surface_ocean_flux_advective_negative_lhs'},
'rhs': {'diffusion': 'salt_rhs_diffusion',
'surface_exchange_flux': 'salt_rhs_surface_exchange_flux',
'surface_ocean_flux_advective_negative_rhs': 'salt_rhs_surface_ocean_flux_advective_negative_rhs'}}}
vmax = 1.e-4
for eq, vmax in zip(["mass", "heat", "salt"], [1.e6, 1e12, 1e4]):
N = len(simple_budgets[eq]['rhs'])
plt.figure(figsize=(5*N, 4))
for i, (k,v) in enumerate(simple_budgets[eq]['rhs'].items(), start=1):
plt.subplot(1,N, i)
if "z_l" in grid._ds[v].dims:
grid._ds[v].isel(z_l = 0).isel(time=0).plot(vmin=-vmax, vmax=vmax, cmap="RdBu_r")
else:
grid._ds[v].isel(time=0).plot(vmin=-vmax, vmax=vmax, cmap="RdBu_r")
plt.title(f"{eq} {k}")
plt.tight_layout()
Custom budget decompositions¶
Suppose we want to have a better understanding of the processes driving both the surface_flux terms and interior diffusion terms. We can simply pass the list of these process names to the decompose keyword argument. We can also further breakdown the nonadvective part of the surface_flux term.
decomposed_budgets = q.aggregate(decompose=["surface_exchange_flux", "nonadvective", "diffusion"])
decomposed_budgets
{'mass': {'lambda': 'density',
'thickness': 'thkcello',
'lhs': {'Eulerian_tendency': 'mass_lhs_Eulerian_tendency'},
'rhs': {'advection': 'mass_rhs_advection',
'surface_exchange_flux_rain_and_ice': 'mass_rhs_surface_exchange_flux_rain_and_ice',
'surface_exchange_flux_snow': 'mass_rhs_surface_exchange_flux_snow',
'surface_exchange_flux_evaporation': 'mass_rhs_surface_exchange_flux_evaporation',
'surface_exchange_flux_rivers': 'mass_rhs_surface_exchange_flux_rivers',
'surface_exchange_flux_icebergs': 'mass_rhs_surface_exchange_flux_icebergs',
'surface_exchange_flux_sea_ice_melt': 'mass_rhs_surface_exchange_flux_sea_ice_melt',
'surface_exchange_flux_virtual_precip_restoring': 'mass_rhs_surface_exchange_flux_virtual_precip_restoring'}},
'heat': {'lambda': 'thetao',
'surface_lambda': 'tos',
'lhs': {'Eulerian_tendency': 'heat_lhs_Eulerian_tendency',
'advection': 'heat_lhs_advection',
'surface_ocean_flux_advective_negative_lhs': 'heat_lhs_surface_ocean_flux_advective_negative_lhs'},
'rhs': {'diffusion_lateral': 'heat_rhs_diffusion_lateral',
'diffusion_interfacial': 'heat_rhs_diffusion_interfacial',
'surface_exchange_flux_nonadvective_latent': 'heat_rhs_surface_exchange_flux_nonadvective_latent',
'surface_exchange_flux_nonadvective_sensible': 'heat_rhs_surface_exchange_flux_nonadvective_sensible',
'surface_exchange_flux_nonadvective_longwave': 'heat_rhs_surface_exchange_flux_nonadvective_longwave',
'surface_exchange_flux_nonadvective_shortwave': 'heat_rhs_surface_exchange_flux_nonadvective_shortwave',
'surface_exchange_flux_advective': 'heat_rhs_surface_exchange_flux_advective',
'surface_ocean_flux_advective_negative_rhs': 'heat_rhs_surface_ocean_flux_advective_negative_rhs',
'bottom_flux': 'heat_rhs_bottom_flux',
'frazil_ice': 'heat_rhs_frazil_ice'}},
'salt': {'lambda': 'so',
'surface_lambda': 'sos',
'lhs': {'Eulerian_tendency': 'salt_lhs_Eulerian_tendency',
'advection': 'salt_lhs_advection',
'surface_ocean_flux_advective_negative_lhs': 'salt_lhs_surface_ocean_flux_advective_negative_lhs'},
'rhs': {'diffusion_lateral': 'salt_rhs_diffusion_lateral',
'diffusion_interfacial': 'salt_rhs_diffusion_interfacial',
'surface_exchange_flux_nonadvective_basal': 'salt_rhs_surface_exchange_flux_nonadvective_basal',
'surface_exchange_flux_advective': 'salt_rhs_surface_exchange_flux_advective',
'surface_ocean_flux_advective_negative_rhs': 'salt_rhs_surface_ocean_flux_advective_negative_rhs'}}}
decomposed_budgets[eq]['rhs']
{'diffusion_lateral': 'salt_rhs_diffusion_lateral',
'diffusion_interfacial': 'salt_rhs_diffusion_interfacial',
'surface_exchange_flux_nonadvective_basal': 'salt_rhs_surface_exchange_flux_nonadvective_basal',
'surface_exchange_flux_advective': 'salt_rhs_surface_exchange_flux_advective',
'surface_ocean_flux_advective_negative_rhs': 'salt_rhs_surface_ocean_flux_advective_negative_rhs'}
vmax = 1.e-4
import numpy as np
for eq, vmax in zip(["mass", "heat", "salt"], [1e6, 1e12, 1e4]):
nrow = np.int64(np.ceil(len(decomposed_budgets[eq]['rhs'])/2))
plt.figure(figsize=(12, nrow*4))
for i, (k,v) in enumerate(decomposed_budgets[eq]['rhs'].items(), start=1):
plt.subplot(nrow, 2, i)
if "z_l" in grid._ds[v].dims:
grid._ds[v].isel(z_l = 0).isel(time=0).plot(vmin=-vmax, vmax=vmax, cmap="RdBu_r")
else:
grid._ds[v].isel(time=0).plot(vmin=-vmax, vmax=vmax, cmap="RdBu_r")
plt.title(f"{eq} {k}")
plt.tight_layout()
plt.show()