Ocean macronutrients


Overview

The availability of several macronutrients controls production in most of the ocean: nitrate, phosphate, and silicate. Here we take a look at maps and depth profiles of these nutrients, and compare them to an observational dataset.

  1. General setup

  2. Subsetting

  3. Processing - means in time and space

  4. Compare to World Ocean Atlas data

  5. Make depth profiles

Prerequisites

Concepts

Importance

Notes

Matplotlib

Necessary

Intro to Cartopy

Necessary

Dask Cookbook

Helpful

Intro to Xarray

Helpful

  • Time to learn: 30 min


Imports

import xarray as xr
import glob
import numpy as np
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
import pop_tools
from dask.distributed import LocalCluster
import s3fs
import netCDF4

from module import adjust_pop_grid

General setup (see intro notebooks for explanations)

Connect to cluster

cluster = LocalCluster()
client = cluster.get_client()
cluster.scale(20)
client

Client

Client-e8cd09a5-550a-11ef-8a5b-6045bdbbe7f0

Connection method: Cluster object Cluster type: distributed.LocalCluster
Dashboard: http://127.0.0.1:8787/status

Cluster Info

Bring in POP grid utilities

ds_grid = pop_tools.get_grid('POP_gx1v7')
lons = ds_grid.TLONG
lats = ds_grid.TLAT
depths = ds_grid.z_t * 0.01

Load the data

jetstream_url = 'https://js2.jetstream-cloud.org:8001/'

s3 = s3fs.S3FileSystem(anon=True, client_kwargs=dict(endpoint_url=jetstream_url))

# Generate a list of all files in CESM folder
s3path = 's3://pythia/ocean-bgc/cesm/g.e22.GOMIPECOIAF_JRA-1p4-2018.TL319_g17.4p2z.002branch/ocn/proc/tseries/month_1/*'
remote_files = s3.glob(s3path)

# Open all files from folder
fileset = [s3.open(file) for file in remote_files]

# Open with xarray
ds = xr.open_mfdataset(fileset, data_vars="minimal", coords='minimal', compat="override", parallel=True,
                       drop_variables=["transport_components", "transport_regions", 'moc_components'], decode_times=True)

Subsetting

Make our dataset smaller so it has just a couple of macronutrient variables we’re interested in.

variables =['PO4','NO3','SiO3']
keep_vars=['z_t','z_t_150m','dz','time_bound','time','TAREA','TLAT','TLONG'] + variables
ds = ds.drop_vars([v for v in ds.variables if v not in keep_vars])

Let’s take a quick look at nitrate to make sure that things look okay…

ds.NO3.isel(time=0,z_t=0).plot(cmap="viridis")
/home/runner/miniconda3/envs/ocean-bgc-cookbook-dev/lib/python3.12/site-packages/distributed/client.py:3362: UserWarning: Sending large graph of size 10.01 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
 or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
  warnings.warn(
<matplotlib.collections.QuadMesh at 0x7f9e7c0d5070>
../_images/d605f0caefc082176d59817b8b9c993b3ba98508bcbbd80897eac0d20070453e.png

Transforming from monthly to annual data

We can’t just use xarray’s regular mean() function because months have different numbers of days in them, so we have to weight by that to ensure the annual mean is accurate. See this ESDS blog post for a more detailed explanation with examples!

def year_mean(ds):
    """
    Properly convert monthly data to annual means, taking into account month lengths.
    Source: https://ncar.github.io/esds/posts/2021/yearly-averages-xarray/
    """
    
    # Make a DataArray with the number of days in each month, size = len(time)
    month_length = ds.time.dt.days_in_month

    # Calculate the weights by grouping by 'time.year'
    weights = (
        month_length.groupby("time.year") / month_length.groupby("time.year").sum()
    )

    # Test that the sum of the year for each season is 1.0
    np.testing.assert_allclose(weights.groupby("time.year").sum().values, np.ones((len(ds.groupby("time.year")), )))

    # Calculate the weighted average
    return (ds * weights).groupby("time.year").sum(dim="time")
ds_annual = year_mean(ds)
ds_annual
<xarray.Dataset> Size: 2GB
Dimensions:   (year: 10, z_t: 60, nlat: 384, nlon: 320, z_t_150m: 15)
Coordinates:
    TLAT      (nlat, nlon) float64 983kB dask.array<chunksize=(384, 320), meta=np.ndarray>
    TLONG     (nlat, nlon) float64 983kB dask.array<chunksize=(384, 320), meta=np.ndarray>
  * z_t       (z_t) float32 240B 500.0 1.5e+03 2.5e+03 ... 5.125e+05 5.375e+05
  * z_t_150m  (z_t_150m) float32 60B 500.0 1.5e+03 2.5e+03 ... 1.35e+04 1.45e+04
  * year      (year) int64 80B 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019
Dimensions without coordinates: nlat, nlon
Data variables:
    NO3       (year, z_t, nlat, nlon) float64 590MB dask.array<chunksize=(1, 15, 96, 80), meta=np.ndarray>
    PO4       (year, z_t, nlat, nlon) float64 590MB dask.array<chunksize=(1, 15, 96, 80), meta=np.ndarray>
    SiO3      (year, z_t, nlat, nlon) float64 590MB dask.array<chunksize=(1, 15, 96, 80), meta=np.ndarray>

Note that our time coordinate is now called year instead, and has only years now. We can select specific years to plot:

ds_annual['NO3'].sel(year=2010).isel(z_t=0).plot()
/home/runner/miniconda3/envs/ocean-bgc-cookbook-dev/lib/python3.12/site-packages/distributed/client.py:3362: UserWarning: Sending large graph of size 10.01 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
 or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
  warnings.warn(
<matplotlib.collections.QuadMesh at 0x7f9e8c5e7950>
../_images/32e7e9fea1a15e6ac66e354e4b77e9b12e0cdbf38addf719d49859e5fa76bcca.png

Let’s make a nicer-looking map

fig = plt.figure(figsize=(8,6))

ax = fig.add_subplot(1,1,1, projection=ccrs.Robinson(central_longitude=305.0))
land = cartopy.feature.NaturalEarthFeature('physical', 'land', scale='110m', edgecolor='k', facecolor='white', linewidth=0.5)
ax.add_feature(land)

ax.set_title('CESM surface NO$_3$', fontsize=10)
lon, lat, field = adjust_pop_grid(lons, lats, ds_annual.NO3.sel(year=2010).isel(z_t=0))
pc1=ax.pcolormesh(lon, lat,field, vmin=0, vmax=20, cmap='Greens',
                 transform=ccrs.PlateCarree())

cbar1 = fig.colorbar(pc1, ax=ax,extend='max',label='NO$_3$ (mmol m$^{-3}$)')
../_images/9a7f244a3d5822c0265b014a7fe1555877266961dd31c1957f544d69360c9772.png

Compare long-term mean to World Ocean Atlas 2018

We’ve already regridded the WOA data to be on the same grid as the CESM data, so we don’t need to worry about that step. However, if you wanted to compare to a dataset that’s on a different grid, you’d need to go through the regridding process, which is beyond the scope of this cookbook.

This dataset has also already had a time mean taken, so there’s no time coordinate.

You might notice that there are three coordinates: z_t, z_w, and z_w_bot. Each of these are different versions of the same vertical coordinate - z_t represents the midpoint of a depth layer, z_w the top, and z_w_bot the bottom. We use z_t in this demonstration.

woa_file_path = 's3://pythia/ocean-bgc/obs/WOA2018_POPgrid.nc'

woa_file = s3.open(woa_file_path)

ds_woa = xr.load_dataset(woa_file, decode_times=False, decode_coords=False)
ds_woa['z_t'] = ds.z_t
ds_woa
<xarray.Dataset> Size: 214MB
Dimensions:      (z_t: 60, nlat: 384, nlon: 320, z_w: 60, z_w_bot: 60)
Coordinates:
  * z_t          (z_t) float32 240B 500.0 1.5e+03 ... 5.125e+05 5.375e+05
  * z_w          (z_w) float64 480B 0.0 1e+03 2e+03 ... 4.75e+05 5e+05 5.25e+05
  * z_w_bot      (z_w_bot) float64 480B 1e+03 2e+03 3e+03 ... 5.25e+05 5.5e+05
Dimensions without coordinates: nlat, nlon
Data variables: (12/17)
    TEMP         (z_t, nlat, nlon) float32 29MB nan nan nan nan ... nan nan nan
    SALT         (z_t, nlat, nlon) float32 29MB nan nan nan nan ... nan nan nan
    NO3          (z_t, nlat, nlon) float32 29MB nan nan nan nan ... nan nan nan
    O2           (z_t, nlat, nlon) float32 29MB nan nan nan nan ... nan nan nan
    SiO3         (z_t, nlat, nlon) float32 29MB nan nan nan nan ... nan nan nan
    PO4          (z_t, nlat, nlon) float32 29MB nan nan nan nan ... nan nan nan
    ...           ...
    DXT          (nlat, nlon) float64 983kB 2.339e+06 2.339e+06 ... 1.473e+06
    DYT          (nlat, nlon) float64 983kB 5.94e+06 5.94e+06 ... 5.046e+06
    TAREA        (nlat, nlon) float64 983kB 1.39e+13 1.39e+13 ... 7.432e+12
    KMT          (nlat, nlon) int32 492kB 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0
    REGION_MASK  (nlat, nlon) int32 492kB 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0
    dz           (z_t) float64 480B 1e+03 1e+03 1e+03 ... 2.5e+04 2.5e+04
Attributes:
    history:  created by kristen krumhardt on 2019-10-25

Now that we’re doing more involved calculations, we’re going to just take a mean over a couple years (2010-2011) to make the computational load a bit lighter. For a more accurate analysis, we’d want to include more years than this.

ds_annual_subset = ds_annual.sel(year=[2010,2011])
ds_mean = ds_annual_subset.mean("year").compute()
/home/runner/miniconda3/envs/ocean-bgc-cookbook-dev/lib/python3.12/site-packages/distributed/client.py:3362: UserWarning: Sending large graph of size 20.16 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
 or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
  warnings.warn(
NO3_diff = ds_mean.NO3 - ds_woa.NO3
PO4_diff = ds_mean.PO4 - ds_woa.PO4
SiO3_diff = ds_mean.SiO3 - ds_woa.SiO3

Surface comparison

We choose to set up a dictionary with some parameters for each plot we want to make, to cut down on repetition in the actual plotting code block. This could be condensed even further, but there’s a tradeoff between conciseness and readability! We specify the variables we want to plot (in this case different nutrients) and things like the colormaps and normalization. In addition to plotting each nutrient from the modeled data and observations, we also plot the bias, which is the difference between the two datasets. This helps us see how the model differs from observations.

ds_dict_surf = {'CESMNO3': {'title': 'CESM surface NO$_3$',
                       'label': 'NO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Greens',
                       'vmin': 0, 'vmax': 20,
                       'ds': ds_mean.NO3},
           'WOANO3':  {'title': 'WOA surface NO$_3$',
                       'label': 'NO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Greens',
                       'vmin': 0, 'vmax': 20,
                       'ds': ds_woa.NO3},
           'DIFFNO3': {'title': 'Surface NO$_3$ model bias',
                       'label': 'NO$_3$ bias (mmol m$^{-3}$)',
                       'cmap': 'bwr',
                       'vmin': -10, 'vmax': 10,
                       'ds': ds_mean.NO3 - ds_woa.NO3},
           'CESMPO4': {'title': 'CESM surface PO$_4$',
                       'label': 'PO$_4$ (mmol m$^{-3}$)',
                       'cmap': 'Oranges',
                       'vmin': 0, 'vmax': 2,
                       'ds': ds_mean.PO4},
           'WOAPO4':  {'title': 'WOA surface PO$_4$',
                       'label': 'PO$_4$ (mmol m$^{-3}$)',
                       'cmap': 'Oranges',
                       'vmin': 0, 'vmax': 2,
                       'ds': ds_woa.PO4},
           'DIFFPO4': {'title': 'Surface PO$_4$ model bias',
                       'label': 'PO$_4$ bias (mmol m$^{-3}$)',
                       'cmap': 'bwr',
                       'vmin': -1, 'vmax': 1,
                       'ds': ds_mean.PO4 - ds_woa.PO4},
           'CESMSiO3': {'title': 'CESM surface SiO$_3$',
                       'label': 'SiO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Blues',
                       'vmin': 0, 'vmax': 30,
                       'ds': ds_mean.SiO3},
           'WOASiO3':  {'title': 'WOA surface SiO$_3$',
                       'label': 'SiO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Blues',
                       'vmin': 0, 'vmax': 30,
                       'ds': ds_woa.SiO3},
           'DIFFSiO3': {'title': 'Surface SiO$_3$ model bias',
                       'label': 'SiO$_3$ bias (mmol m$^{-3}$)',
                       'cmap': 'bwr',
                       'vmin': -15, 'vmax': 15,
                       'ds': ds_mean.SiO3 - ds_woa.SiO3}
          }
                        

Here we pull from the above dictionary to actually make the plots.

fig = plt.figure(figsize=(18,10))


plot_count = 1
for key, item in ds_dict_surf.items():
    ax = fig.add_subplot(3,3,plot_count, projection=ccrs.Robinson(central_longitude=305.0))
    land = cartopy.feature.NaturalEarthFeature('physical', 'land', scale='110m', edgecolor='k', facecolor='white', linewidth=0.5)
    ax.add_feature(land)
    ax.set_title(item['title'], fontsize=10)
    lon, lat, field = adjust_pop_grid(lons, lats, item['ds'].isel(z_t=0))
    pc=ax.pcolormesh(lon, lat,field, vmin=item['vmin'], vmax=item['vmax'], cmap=item['cmap'],
                     transform=ccrs.PlateCarree())
    cbar1 = fig.colorbar(pc, ax=ax,label=item['label'])

    plot_count += 1
    
../_images/70d91cdd4e51ddefea849b093d04e6b38335f32c8889502b14741888a7a04c82.png

Comparison at 100m

Similar to above, but at a depth of 100m rather than at the surface.

ds_dict_100m = {'CESMNO3': {'title': 'CESM 100m NO$_3$',
                       'label': 'NO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Greens',
                       'vmin': 0, 'vmax': 20,
                       'ds': ds_mean.NO3},
           'WOANO3':  {'title': 'WOA 100m NO$_3$',
                       'label': 'NO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Greens',
                       'vmin': 0, 'vmax': 20,
                       'ds': ds_woa.NO3},
           'DIFFNO3': {'title': '100m NO$_3$ model bias',
                       'label': 'NO$_3$ bias (mmol m$^{-3}$)',
                       'cmap': 'bwr',
                       'vmin': -10, 'vmax': 10,
                       'ds': ds_mean.NO3 - ds_woa.NO3},
           'CESMPO4': {'title': 'CESM 100m PO$_4$',
                       'label': 'PO$_4$ (mmol m$^{-3}$)',
                       'cmap': 'Oranges',
                       'vmin': 0, 'vmax': 2,
                       'ds': ds_mean.PO4},
           'WOAPO4':  {'title': 'WOA 100m PO$_4$',
                       'label': 'PO$_4$ (mmol m$^{-3}$)',
                       'cmap': 'Oranges',
                       'vmin': 0, 'vmax': 2,
                       'ds': ds_woa.PO4},
           'DIFFPO4': {'title': '100m PO$_4$ model bias',
                       'label': 'PO$_4$ bias (mmol m$^{-3}$)',
                       'cmap': 'bwr',
                       'vmin': -1, 'vmax': 1,
                       'ds': ds_mean.PO4 - ds_woa.PO4},
           'CESMSiO3': {'title': 'CESM 100m SiO$_3$',
                       'label': 'SiO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Blues',
                       'vmin': 0, 'vmax': 30,
                       'ds': ds_mean.SiO3},
           'WOASiO3':  {'title': 'WOA 100m SiO$_3$',
                       'label': 'SiO$_3$ (mmol m$^{-3}$)',
                       'cmap': 'Blues',
                       'vmin': 0, 'vmax': 30,
                       'ds': ds_woa.SiO3},
           'DIFFSiO3': {'title': '100m SiO$_3$ model bias',
                       'label': 'SiO$_3$ bias (mmol m$^{-3}$)',
                       'cmap': 'bwr',
                       'vmin': -15, 'vmax': 15,
                       'ds': ds_mean.SiO3 - ds_woa.SiO3}
          }
                        
fig = plt.figure(figsize=(18,10))


plot_count = 1
for key, item in ds_dict_100m.items():
    ax = fig.add_subplot(3,3,plot_count, projection=ccrs.Robinson(central_longitude=305.0))
    land = cartopy.feature.NaturalEarthFeature('physical', 'land', scale='110m', edgecolor='k', facecolor='white', linewidth=0.5)
    ax.add_feature(land)
    ax.set_title(item['title'], fontsize=10)
    lon, lat, field = adjust_pop_grid(lons, lats, item['ds'].isel(z_t=10))
    pc=ax.pcolormesh(lon, lat,field, vmin=item['vmin'], vmax=item['vmax'], cmap=item['cmap'],
                     transform=ccrs.PlateCarree())
    cbar1 = fig.colorbar(pc, ax=ax,label=item['label'])

    plot_count += 1
../_images/f4d96310311781c0bb6bfaad0b02b32e5e99065543fff76a7ef1e6cdaea01aca.png

Global mean macronutrient profiles

Let’s write a function to take a global mean of the variables we’re interested in, so that we can look at some depth profiles rather than maps. Also remember that we already took a mean over the whole time range (and the WOA dataset already had this mean taken), so this is a mean in time as well. Like the above maps, we also plot a bias panel to directly compare the difference between the datasets.

def global_mean(ds, ds_grid, compute_vars, include_ms=False):
    """
    Compute the global mean on a POP dataset. 
    Return computed quantity in conventional units.
    """
    dso = xr.Dataset({v: ds_grid[v] for v in ['z_t']})
    
    for var in compute_vars:
        
        area_depth = np.full([384,320,60],np.nan)
        var_profile = np.full([60],np.nan)

        for z in np.arange(0,60,1):
            
            if include_ms: # marginal seas
                area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.KMT > 0).where(ds[var].isel(z_t=z) > 0)
            
            else: 
                area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.REGION_MASK > 0).where(ds[var].isel(z_t=z) > 0)  
            
        area_depth = xr.DataArray(area_depth,dims=('nlat','nlon','z_t'))
            
        for z in np.arange(0,60,1):
            
            var_profile[z] = (ds[var].isel(z_t=z) * area_depth.isel(z_t=z)).sum(dim=('nlon','nlat')) / area_depth.isel(z_t=z).sum(dim=('nlon','nlat'))
    
        dso[var] = var_profile
                
    return dso
ds_glb = global_mean(ds_mean, ds_grid, ['NO3','PO4','SiO3']).compute()
/tmp/ipykernel_2651/680694341.py:19: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments.
  area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.REGION_MASK > 0).where(ds[var].isel(z_t=z) > 0)
/tmp/ipykernel_2651/680694341.py:19: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments.
  area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.REGION_MASK > 0).where(ds[var].isel(z_t=z) > 0)
/tmp/ipykernel_2651/680694341.py:19: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments.
  area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.REGION_MASK > 0).where(ds[var].isel(z_t=z) > 0)
ds_glb_woa = global_mean(ds_woa, ds_grid, ['NO3','PO4','SiO3']).compute()
/tmp/ipykernel_2651/680694341.py:19: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments.
  area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.REGION_MASK > 0).where(ds[var].isel(z_t=z) > 0)
/tmp/ipykernel_2651/680694341.py:19: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments.
  area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.REGION_MASK > 0).where(ds[var].isel(z_t=z) > 0)
/tmp/ipykernel_2651/680694341.py:19: DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments.
  area_depth[:,:,z] = ds_grid.TAREA.where(ds_grid.REGION_MASK > 0).where(ds[var].isel(z_t=z) > 0)

Rather than setting up a dictionary of parameters, here we choose to make the plots inline since there aren’t as many.

fig = plt.figure(figsize=(6,10))

plt.suptitle('Global mean macronutrient profiles', fontsize=14)

### Row 1  - NO3

ax = fig.add_subplot(3,2,1)
ax.set_title('Global mean NO$_3$')
ax.plot(ds_glb_woa['NO3'].values, depths, label='WOA', linewidth=3, color='lightgreen')
ax.plot(ds_glb['NO3'].values, depths, label='CESM', linewidth=3, color='green')
ax.legend()
ax.set(ylabel='depth (m)',xlabel='NO$_3$ (mmol m$^{-3}$)')
plt.gca().invert_yaxis()

# Bias plot

ax = fig.add_subplot(3,2,2)
ax.plot(ds_glb['NO3'].values - ds_glb_woa['NO3'].values, depths, label='bias', linewidth=3, color='black')
ax.legend()
ax.set(ylabel='depth (m)',xlabel='NO$_3$ bias (mmol m$^{-3}$)')
ax.axvline(x=0,color='black',linestyle='--',linewidth=0.5)
plt.gca().invert_yaxis()

### Row 2  - PO4

ax = fig.add_subplot(3,2,3)
ax.set_title('Global mean PO$_4$')
ax.plot(ds_glb_woa['PO4'].values, depths, label='WOA', linewidth=3, color='peachpuff')
ax.plot(ds_glb['PO4'].values, depths, label='CESM', linewidth=3, color='orange')
ax.legend()
ax.set(ylabel='depth (m)',xlabel='PO$_4$ (mmol m$^{-3}$)')
plt.gca().invert_yaxis()

# Bias plot

ax = fig.add_subplot(3,2,4)
ax.plot(ds_glb['PO4'].values - ds_glb_woa['PO4'].values, depths, label='bias', linewidth=3, color='black')
ax.legend()
ax.set(ylabel='depth (m)',xlabel='PO$_4$ bias (mmol m$^{-3}$)')
ax.axvline(x=0,color='black',linestyle='--',linewidth=0.5)
plt.gca().invert_yaxis()

### Row 3  - SiO3

ax = fig.add_subplot(3,2,5)
ax.set_title('Global mean SiO$_3$')
ax.plot(ds_glb_woa['SiO3'].values, depths, label='WOA', linewidth=3, color='lightblue')
ax.plot(ds_glb['SiO3'].values, depths, label='CESM', linewidth=3, color='blue')
ax.legend()
ax.set(ylabel='depth (m)',xlabel='SiO$_3$ (mmol m$^{-3}$)')
plt.gca().invert_yaxis()

# Bias plot

ax = fig.add_subplot(3,2,6)
ax.plot(ds_glb['SiO3'].values - ds_glb_woa['SiO3'].values, depths, label='bias', linewidth=3, color='black')
ax.legend()
ax.set(ylabel='depth (m)',xlabel='SiO$_3$ bias (mmol m$^{-3}$)')
ax.axvline(x=0,color='black',linestyle='--',linewidth=0.5)
plt.gca().invert_yaxis()

fig.tight_layout()
../_images/62694116e54a73063f550beb6f06311ec3afd5032cfa79a79a0b0bdbd4975eb1.png

And close the Dask cluster we spun up at the beginning.

cluster.close()
2024-08-07 22:20:01,054 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,055 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,055 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,056 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,057 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,058 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,059 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,061 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,062 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,065 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,066 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,073 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,077 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,080 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,081 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,085 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,093 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,093 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,101 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing
2024-08-07 22:20:01,112 - distributed.nanny - WARNING - Worker process still alive after 4.0 seconds, killing

Summary

You’ve learned how to plot and evaluate the distribution of some key ocean nutrients in CESM output.

Resources and references