PISCES — chlorophyll seasonal cycle off the Iberian upwelling¶
Demonstrates the multi-variable subset pattern: pull two PISCES
biogeochem variables (chl chlorophyll-a, o2 dissolved oxygen) over a
coastal box off Iberia for a multi-year window, group by month, and plot
the seasonal cycle.
PISCES is cmems_mod_glo_bgc_my_0.25deg_P1D-m — daily, 1/4° grid,
global. Variables are 4-D (time, depth, lat, lon); we clip to the
surface 0-10 m to keep the request small.
Setup¶
Consolidate the imports up front. pyramids provides NetCDF (reading the
downloaded file); earthlens provides the unified EarthLens entry point and
the CMEMS Catalog. The monthly climatology below is a manual per-day/
per-month average over pandas/numpy, and matplotlib draws the final plot.
import os
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pyramids.netcdf import NetCDF
from earthlens.cmems import Catalog
from earthlens.core import EarthLens
The request¶
PISCES is cmems_mod_glo_bgc_my_0.25deg_P1D-m — daily, 1/4° grid, global. We
pull two biogeochem variables (chl chlorophyll-a, o2 dissolved oxygen) over
a coastal box off Iberia, and create the output directory.
OUT_DIR = Path('data/cmems-pisces')
OUT_DIR.mkdir(parents=True, exist_ok=True)
DATASET_ID = 'cmems_mod_glo_bgc_my_0.25deg_P1D-m'
VARIABLES = ['chl', 'o2']
Inspect the dataset metadata¶
The CMEMS Catalog resolves the dataset id to its cadence, domain, and per-
variable units / long names — a quick sanity check before downloading.
ds = Catalog().get_dataset(DATASET_ID)
print(ds)
print(f'domain: {ds.domain}')
for v in VARIABLES:
print(f' {ds.variables[v]}')
Download — three years of daily PISCES at the surface¶
Multi-year × multi-variable × surface-clipped, in one server-side subset call.
We build the EarthLens request first — variables are 4-D
(time, depth, lat, lon), so we clip to the surface 0-10 m to keep it small.
earthlens = EarthLens(
data_source='cmems',
start='2018-01-01',
end='2020-12-31',
cadence='daily',
dataset=DATASET_ID,
variables=VARIABLES,
aoi=[-12.0, 40.0, -8.0, 44.0], # Iberian upwelling box (W, S, E, N)
path=OUT_DIR,
minimum_depth=0.0,
maximum_depth=10.0,
service_username=os.environ.get('COPERNICUSMARINE_SERVICE_USERNAME'),
service_password=os.environ.get('COPERNICUSMARINE_SERVICE_PASSWORD'),
)
Run the subset download on its own line so the request build and the network call read as separate steps. It returns the list of written NetCDF paths.
paths = earthlens.download()
print(paths)
Compute the box-averaged monthly climatology for each variable¶
Read the NetCDF into pyramids¶
pd.to_datetime(nc.get_time_variable()) turns the CF "hours since 1950" time
axis into datetime64 timestamps, which the next cell groups into a monthly
climatology with a manual per-day/per-month average (not a groupby). The
cube itself is (time, depth, latitude, longitude), reshaped by hand from
the band-major layout read_array returns.
nc = NetCDF.read_file(paths[0], read_only=True)
print('variables :', nc.variable_names)
print('dimensions:', nc.dimension_sizes)
Reduce to a 12-month climatology¶
Average across (depth, lat, lon) per day, then group by calendar month across
the three years. Both arrays end up as 12-element vectors keyed on month-of-year
(1-12).
# A 4-D cube arrives as bands ordered time-major: band = time_index * n_depth + depth.
n_time = nc.dimension_sizes['time']
n_depth = nc.dimension_sizes['depth']
months = pd.to_datetime(nc.get_time_variable()).month
climatology = {}
for name in VARIABLES:
var = nc.get_variable(name)
cube = var.read_array(masked=True).reshape(n_time, n_depth, var.rows, var.columns)
per_day = cube.mean(axis=(1, 2, 3)) # surface box mean for each day
climatology[name] = np.array(
[float(per_day[months == m].mean()) for m in range(1, 13)]
)
nc.close()
for v in VARIABLES:
print(f'{v}: {climatology[v].round(4)}')
Plot the seasonal cycle¶
Iberian upwelling chlorophyll peaks in spring / late summer with the wind-driven upwelling events; dissolved oxygen tracks the same pattern inversely (warmer water holds less O2).
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
months = np.arange(1, 13)
ax1.bar(months, climatology['chl'], color='tab:green')
ax1.set_xlabel('Month')
ax1.set_ylabel('chl (mg m-3)')
ax1.set_title('PISCES chlorophyll-a climatology')
ax1.set_xticks(months)
ax2.bar(months, climatology['o2'], color='tab:blue')
ax2.set_xlabel('Month')
ax2.set_ylabel('o2 (mmol m-3)')
ax2.set_title('PISCES dissolved oxygen climatology')
ax2.set_xticks(months)
fig.suptitle('Iberian upwelling box, 2018-2020 surface (0-10 m) mean')
fig.tight_layout()