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.
General setup
Subsetting
Processing - means in time and space
Compare to World Ocean Atlas data
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
from module import adjust_pop_grid/home/runner/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/pop_tools/__init__.py:4: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
from pkg_resources import DistributionNotFound, get_distribution
General setup (see intro notebooks for explanations)¶
Connect to cluster¶
cluster = LocalCluster()
client = cluster.get_client()/home/runner/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/distributed/node.py:187: UserWarning: Port 8787 is already in use.
Perhaps you already have a cluster running?
Hosting the HTTP server on port 42521 instead
warnings.warn(
cluster.scale(20)clientBring 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.01Downloading file 'inputdata/ocn/pop/gx1v7/grid/horiz_grid_20010402.ieeer8' from 'https://svn-ccsm-inputdata.cgd.ucar.edu/trunk/inputdata/ocn/pop/gx1v7/grid/horiz_grid_20010402.ieeer8' to '/home/runner/.pop_tools'.
---------------------------------------------------------------------------
ConnectionRefusedError Traceback (most recent call last)
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connection.py:198, in HTTPConnection._new_conn(self)
197 try:
--> 198 sock = connection.create_connection(
199 (self._dns_host, self.port),
200 self.timeout,
201 source_address=self.source_address,
202 socket_options=self.socket_options,
203 )
204 except socket.gaierror as e:
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/util/connection.py:85, in create_connection(address, timeout, source_address, socket_options)
84 try:
---> 85 raise err
86 finally:
87 # Break explicitly a reference cycle
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/util/connection.py:73, in create_connection(address, timeout, source_address, socket_options)
72 sock.bind(source_address)
---> 73 sock.connect(sa)
74 # Break explicitly a reference cycle
ConnectionRefusedError: [Errno 111] Connection refused
The above exception was the direct cause of the following exception:
NewConnectionError Traceback (most recent call last)
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connectionpool.py:787, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
786 # Make the request on the HTTPConnection object
--> 787 response = self._make_request(
788 conn,
789 method,
790 url,
791 timeout=timeout_obj,
792 body=body,
793 headers=headers,
794 chunked=chunked,
795 retries=retries,
796 response_conn=response_conn,
797 preload_content=preload_content,
798 decode_content=decode_content,
799 **response_kw,
800 )
802 # Everything went great!
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connectionpool.py:488, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
487 new_e = _wrap_proxy_error(new_e, conn.proxy.scheme)
--> 488 raise new_e
490 # conn.request() calls http.client.*.request, not the method in
491 # urllib3.request. It also calls makefile (recv) on the socket.
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connectionpool.py:464, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
463 try:
--> 464 self._validate_conn(conn)
465 except (SocketTimeout, BaseSSLError) as e:
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connectionpool.py:1093, in HTTPSConnectionPool._validate_conn(self, conn)
1092 if conn.is_closed:
-> 1093 conn.connect()
1095 # TODO revise this, see https://github.com/urllib3/urllib3/issues/2791
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connection.py:753, in HTTPSConnection.connect(self)
752 sock: socket.socket | ssl.SSLSocket
--> 753 self.sock = sock = self._new_conn()
754 server_hostname: str = self.host
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connection.py:213, in HTTPConnection._new_conn(self)
212 except OSError as e:
--> 213 raise NewConnectionError(
214 self, f"Failed to establish a new connection: {e}"
215 ) from e
217 sys.audit("http.client.connect", self, self.host, self.port)
NewConnectionError: <urllib3.connection.HTTPSConnection object at 0x7f78305ad550>: Failed to establish a new connection: [Errno 111] Connection refused
The above exception was the direct cause of the following exception:
MaxRetryError Traceback (most recent call last)
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/requests/adapters.py:644, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
643 try:
--> 644 resp = conn.urlopen(
645 method=request.method,
646 url=url,
647 body=request.body,
648 headers=request.headers,
649 redirect=False,
650 assert_same_host=False,
651 preload_content=False,
652 decode_content=False,
653 retries=self.max_retries,
654 timeout=timeout,
655 chunked=chunked,
656 )
658 except (ProtocolError, OSError) as err:
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/connectionpool.py:841, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
839 new_e = ProtocolError("Connection aborted.", new_e)
--> 841 retries = retries.increment(
842 method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
843 )
844 retries.sleep()
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/urllib3/util/retry.py:519, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
518 reason = error or ResponseError(cause)
--> 519 raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]
521 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)
MaxRetryError: HTTPSConnectionPool(host='svn-ccsm-inputdata.cgd.ucar.edu', port=443): Max retries exceeded with url: /trunk/inputdata/ocn/pop/gx1v7/grid/horiz_grid_20010402.ieeer8 (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f78305ad550>: Failed to establish a new connection: [Errno 111] Connection refused'))
During handling of the above exception, another exception occurred:
ConnectionError Traceback (most recent call last)
Cell In[5], line 1
----> 1 ds_grid = pop_tools.get_grid('POP_gx1v7')
2 lons = ds_grid.TLONG
3 lats = ds_grid.TLAT
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/pop_tools/grid.py:137, in get_grid(grid_name, scrip)
134 nlon = grid_attrs['lateral_dims'][1]
136 # read horizontal grid
--> 137 horiz_grid_fname = INPUTDATA.fetch(grid_attrs['horiz_grid_fname'], downloader=downloader)
138 grid_file_data = np.fromfile(horiz_grid_fname, dtype='>f8', count=-1)
139 grid_file_data = grid_file_data.reshape((7, nlat, nlon))
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/pop_tools/grid.py:92, in fetch(self, fname, processor, downloader)
89 if downloader is None:
90 downloader = pooch.downloaders.choose_downloader(url)
---> 92 pooch.core.stream_download(url, full_path, known_hash, downloader, pooch=self)
94 if processor is not None:
95 return processor(str(full_path), action, self)
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/pooch/core.py:807, in stream_download(url, fname, known_hash, downloader, pooch, retry_if_failed)
803 try:
804 # Stream the file to a temporary so that we can safely check its
805 # hash before overwriting the original.
806 with temporary_file(path=str(fname.parent)) as tmp:
--> 807 downloader(url, tmp, pooch)
808 hash_matches(tmp, known_hash, strict=True, source=str(fname.name))
809 shutil.move(tmp, str(fname))
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/pooch/downloaders.py:220, in HTTPDownloader.__call__(self, url, output_file, pooch, check_only)
218 # pylint: enable=consider-using-with
219 try:
--> 220 response = requests.get(url, timeout=timeout, **kwargs)
221 response.raise_for_status()
222 content = response.iter_content(chunk_size=self.chunk_size)
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/requests/api.py:73, in get(url, params, **kwargs)
62 def get(url, params=None, **kwargs):
63 r"""Sends a GET request.
64
65 :param url: URL for the new :class:`Request` object.
(...) 70 :rtype: requests.Response
71 """
---> 73 return request("get", url, params=params, **kwargs)
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/requests/api.py:59, in request(method, url, **kwargs)
55 # By using the 'with' statement we are sure the session is closed, thus we
56 # avoid leaving sockets open which can trigger a ResourceWarning in some
57 # cases, and look like a memory leak in others.
58 with sessions.Session() as session:
---> 59 return session.request(method=method, url=url, **kwargs)
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/requests/sessions.py:589, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
584 send_kwargs = {
585 "timeout": timeout,
586 "allow_redirects": allow_redirects,
587 }
588 send_kwargs.update(settings)
--> 589 resp = self.send(prep, **send_kwargs)
591 return resp
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/requests/sessions.py:703, in Session.send(self, request, **kwargs)
700 start = preferred_clock()
702 # Send the request
--> 703 r = adapter.send(request, **kwargs)
705 # Total elapsed time of the request (approximately)
706 elapsed = preferred_clock() - start
File ~/micromamba/envs/ocean-bgc-cookbook-dev/lib/python3.13/site-packages/requests/adapters.py:677, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
673 if isinstance(e.reason, _SSLError):
674 # This branch is for urllib3 v1.22 and later.
675 raise SSLError(e, request=request)
--> 677 raise ConnectionError(e, request=request)
679 except ClosedPoolError as e:
680 raise ConnectionError(e, request=request)
ConnectionError: HTTPSConnectionPool(host='svn-ccsm-inputdata.cgd.ucar.edu', port=443): Max retries exceeded with url: /trunk/inputdata/ocn/pop/gx1v7/grid/horiz_grid_20010402.ieeer8 (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f78305ad550>: Failed to establish a new connection: [Errno 111] Connection refused'))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)
s3.invalidate_cache()
# 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")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_annualNote 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()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}$)')Compare long-term mean to World Ocean Atlas 2018¶
About the World Ocean Atlas
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_woaNow 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()NO3_diff = ds_mean.NO3 - ds_woa.NO3
PO4_diff = ds_mean.PO4 - ds_woa.PO4
SiO3_diff = ds_mean.SiO3 - ds_woa.SiO3Surface 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
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 += 1Global 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 dsods_glb = global_mean(ds_mean, ds_grid, ['NO3','PO4','SiO3']).compute()ds_glb_woa = global_mean(ds_woa, ds_grid, ['NO3','PO4','SiO3']).compute()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()
And close the Dask cluster we spun up at the beginning.
cluster.close()Summary¶
You’ve learned how to plot and evaluate the distribution of some key ocean nutrients in CESM output.
Resources and references¶
- (2013). In Ocean Biogeochemical Dynamics (pp. 102–172). Princeton University Press. 10.2307/j.ctt3fgxqx.7