| name | ocean-data |
| description | Download and analyze oceanographic data from Copernicus Marine Service and Argo floats using copernicusmarine, gsw, and xarray. |
| tags | ["oceanography","copernicus","argo","xarray","earth-science"] |
| version | 1.0.0 |
| authors | [{"name":"Rosetta Skills Contributors","github":"@xjtulyc"}] |
| license | MIT |
| platforms | ["claude-code","codex","gemini-cli","cursor"] |
| dependencies | ["copernicusmarine>=1.0","gsw>=3.6","xarray>=2023.6","pandas>=2.0","matplotlib>=3.7","numpy>=1.24","cartopy>=0.22"] |
| last_updated | 2026-03-17 |
| status | stable |
Ocean Data Analysis with Copernicus Marine Service and Argo Floats
Retrieve, process, and visualize ocean physical and biogeochemical data using
the Copernicus Marine Service (CMEMS) Python client, Argo float profiles, and
the TEOS-10 Gibbs SeaWater (GSW) toolbox.
When to Use This Skill
- You need sea surface temperature, salinity, or current fields for a specific
region and time period from the Copernicus Marine Service catalog.
- You want to download individual Argo float profiles and compute derived
quantities such as mixed layer depth (MLD) or potential density.
- You need to produce T-S diagrams, eddy kinetic energy (EKE) maps, or
mixed-layer climatologies for research or operational purposes.
- You are building ocean-climate pipelines that combine model output with
in-situ observations.
Background & Key Concepts
Copernicus Marine Service (CMEMS)
The Copernicus Marine Environment Monitoring Service provides free,
quality-controlled ocean datasets spanning physical, biogeochemical, and sea-ice
variables. Products include reanalysis, near-real-time, and forecast datasets.
The copernicusmarine Python package (v1+) replaced the legacy motuclient
and ftplib workflows.
Argo Float Program
Argo is a global array of ~4000 autonomous profiling floats that measure
temperature and salinity from 2000 m to the surface every ~10 days. Data are
freely available via GDAC servers (Ifremer / BODC) and through the
copernicusmarine catalog.
TEOS-10 / GSW Toolbox
The Thermodynamic Equation of Seawater 2010 (TEOS-10) defines Absolute
Salinity (SA) and Conservative Temperature (CT) as the preferred variables.
The gsw Python library converts Practical Salinity and in-situ temperature to
TEOS-10 variables and computes derived quantities such as potential density,
buoyancy frequency, and mixed layer depth.
Mixed Layer Depth (MLD)
MLD is commonly estimated using a density threshold criterion: the depth at
which potential density exceeds the surface value by 0.03 kg/m³ (de Boyer
Montégut et al., 2004).
Eddy Kinetic Energy (EKE)
EKE quantifies mesoscale variability: EKE = 0.5 * (u'^2 + v'^2), where u' and
v' are anomalies of eastward and northward geostrophic currents relative to a
long-term mean.
Environment Setup
Install dependencies
pip install "copernicusmarine>=1.0" "gsw>=3.6" "xarray>=2023.6" \
"pandas>=2.0" "matplotlib>=3.7" "numpy>=1.24" "cartopy>=0.22"
Authenticate with Copernicus Marine Service
Register for a free account at https://marine.copernicus.eu/ then store your
credentials:
copernicusmarine login
export COPERNICUSMARINE_USERNAME="<your-username>"
export COPERNICUSMARINE_PASSWORD=$(cat ~/.cmems_passwd)
import os
import copernicusmarine as cm
username = os.getenv("COPERNICUSMARINE_USERNAME", "")
password = os.getenv("COPERNICUSMARINE_PASSWORD", "")
cm.login(username=username, password=password, overwrite_configuration_file=True)
Core Workflow
Step 1 – Browse the Catalog and Download SST Data
import copernicusmarine as cm
import xarray as xr
catalog = cm.describe(contains=["SST", "Mediterranean"])
for entry in catalog.products[:5]:
print(entry.product_id, "-", entry.title)
ds = cm.open_dataset(
dataset_id="cmems_obs-sst_glo_phy_l4_my_0.25deg",
variables=["analysed_sst", "analysis_error"],
minimum_longitude=-20.0,
maximum_longitude=40.0,
minimum_latitude=25.0,
maximum_latitude=50.0,
start_datetime="2023-01-01T00:00:00",
end_datetime="2023-03-31T23:59:59",
)
print(ds)
ds["sst_celsius"] = ds["analysed_sst"] - 273.15
ds["sst_celsius"].attrs["units"] = "degC"
Step 2 – Download Argo Float Profiles and Compute MLD
import gsw
import numpy as np
import xarray as xr
import copernicusmarine as cm
argo = cm.open_dataset(
dataset_id="cmems_obs-ins_glo_phy-temp-sal_nrt_argo_P1D-m",
variables=["TEMP", "PSAL", "PRES", "LATITUDE", "LONGITUDE", "TIME"],
minimum_longitude=-40.0,
maximum_longitude=-20.0,
minimum_latitude=40.0,
maximum_latitude=60.0,
start_datetime="2023-06-01T00:00:00",
end_datetime="2023-08-31T23:59:59",
)
def compute_mld(temp, psal, pres, lat, threshold=0.03):
"""
Compute mixed layer depth using a density threshold criterion.
Parameters
----------
temp : array-like, in-situ temperature (ITS-90, °C)
psal : array-like, Practical Salinity (PSS-78)
pres : array-like, sea pressure (dbar)
lat : float, latitude (degrees N)
threshold : float, density difference criterion (kg/m³), default 0.03
Returns
-------
mld : float, mixed layer depth (m)
"""
SA = gsw.SA_from_SP(psal, pres, 0.0, lat)
CT = gsw.CT_from_t(SA, temp, pres)
sigma0 = gsw.sigma0(SA, CT)
valid = np.isfinite(sigma0)
if valid.sum() < 3:
return np.nan
ref_density = sigma0[valid][0]
exceeds = np.where((sigma0 - ref_density) > threshold)[0]
if len(exceeds) == 0:
return float(pres[valid][-1])
return float(pres[exceeds[0]])
mld_values = []
for i in range(min(50, argo.dims.get("N_PROF", 0))):
profile = argo.isel(N_PROF=i)
mld = compute_mld(
profile["TEMP"].values,
profile["PSAL"].values,
profile["PRES"].values,
float(profile["LATITUDE"].values),
)
mld_values.append(mld)
print(f"Mean MLD (N=50 profiles): {np.nanmean(mld_values):.1f} dbar")
Step 3 – Compute Eddy Kinetic Energy from Altimetry
import copernicusmarine as cm
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
alt = cm.open_dataset(
dataset_id="cmems_obs-sl_glo_phy-ssh_my_allsat-l4-duacs-0.25deg_P1D",
variables=["ugosa", "vgosa"],
minimum_longitude=-80.0,
maximum_longitude=-40.0,
minimum_latitude=25.0,
maximum_latitude=55.0,
start_datetime="2023-01-01T00:00:00",
end_datetime="2023-12-31T23:59:59",
)
eke = 0.5 * (alt["ugosa"] ** 2 + alt["vgosa"] ** 2)
eke_mean = eke.mean(dim="time")
fig, ax = plt.subplots(
subplot_kw={"projection": ccrs.PlateCarree()}, figsize=(10, 7)
)
ax.add_feature(cfeature.LAND, facecolor="lightgray")
ax.add_feature(cfeature.COASTLINE, linewidth=0.5)
ax.gridlines(draw_labels=True, linewidth=0.3, linestyle="--")
pcm = ax.pcolormesh(
eke_mean.longitude,
eke_mean.latitude,
eke_mean.values * 1e4,
cmap="plasma",
transform=ccrs.PlateCarree(),
vmin=0,
vmax=500,
)
plt.colorbar(pcm, ax=ax, label="EKE (cm²/s²)", shrink=0.8)
ax.set_title("Annual Mean Eddy Kinetic Energy – North Atlantic 2023")
plt.tight_layout()
plt.savefig("eke_north_atlantic_2023.png", dpi=150)
plt.show()
Advanced Usage
T-S Diagram with Density Contours
import gsw
import numpy as np
import matplotlib.pyplot as plt
def ts_diagram(temp_array, psal_array, pres_array, lat, color_by=None,
title="T-S Diagram"):
"""
Plot a Temperature-Salinity diagram with TEOS-10 potential density contours.
Parameters
----------
temp_array : 2-D array (N_PROF, N_LEVELS)
psal_array : 2-D array (N_PROF, N_LEVELS)
pres_array : 2-D array (N_PROF, N_LEVELS)
lat : float, representative latitude
color_by : 1-D array (N_PROF,) used to color-code profiles, optional
title : str
"""
SA_all = gsw.SA_from_SP(psal_array, pres_array, 0.0, lat)
CT_all = gsw.CT_from_t(SA_all, temp_array, pres_array)
sa_grid = np.linspace(SA_all[np.isfinite(SA_all)].min() - 0.5,
SA_all[np.isfinite(SA_all)].max() + 0.5, 100)
ct_grid = np.linspace(CT_all[np.isfinite(CT_all)].min() - 1,
CT_all[np.isfinite(CT_all)].max() + 1, 100)
SA_g, CT_g = np.meshgrid(sa_grid, ct_grid)
sigma0_grid = gsw.sigma0(SA_g, CT_g)
fig, ax = plt.subplots(figsize=(8, 7))
cs = ax.contour(sa_grid, ct_grid, sigma0_grid,
levels=np.arange(22, 30, 0.5), colors="gray",
linewidths=0.6, linestyles="--")
ax.clabel(cs, fmt="%.1f", fontsize=8)
sc = ax.scatter(SA_all.ravel(), CT_all.ravel(),
c=color_by if color_by is not None else "steelblue",
s=1, alpha=0.4, cmap="viridis")
if color_by is not None:
plt.colorbar(sc, ax=ax, label="Depth (m)")
ax.set_xlabel("Absolute Salinity SA (g/kg)")
ax.set_ylabel("Conservative Temperature CT (°C)")
ax.set_title(title)
plt.tight_layout()
plt.savefig("ts_diagram.png", dpi=150)
plt.show()
rng = np.random.default_rng(42)
n_prof, n_lev = 100, 50
pres_syn = np.tile(np.linspace(5, 500, n_lev), (n_prof, 1))
psal_syn = 35.0 + rng.normal(0, 0.3, (n_prof, n_lev))
temp_syn = 15.0 - pres_syn / 50 + rng.normal(0, 0.5, (n_prof, n_lev))
ts_diagram(temp_syn, psal_syn, pres_syn, lat=45.0,
color_by=np.repeat(np.arange(n_prof), n_lev),
title="Synthetic T-S Diagram – North Atlantic")
Seasonal SST Cycle from Reanalysis
import copernicusmarine as cm
import xarray as xr
import matplotlib.pyplot as plt
import numpy as np
ds = cm.open_dataset(
dataset_id="cmems_mod_glo_phy_my_0.083deg_P1D-m",
variables=["thetao"],
minimum_longitude=-10.0,
maximum_longitude=36.0,
minimum_latitude=30.0,
maximum_latitude=46.0,
minimum_depth=0.0,
maximum_depth=1.0,
start_datetime="2010-01-01T00:00:00",
end_datetime="2020-12-31T23:59:59",
)
sst = ds["thetao"].isel(depth=0)
sst_clim = sst.groupby("time.month").mean(dim="time")
sst_basin = sst_clim.mean(dim=["latitude", "longitude"])
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(np.arange(1, 13), sst_basin.values, "o-", color="firebrick", lw=2)
ax.set_xticks(np.arange(1, 13))
ax.set_xticklabels(["J","F","M","A","M","J","J","A","S","O","N","D"])
ax.set_xlabel("Month")
ax.set_ylabel("SST (°C)")
ax.set_title("Mediterranean Sea – 2010–2020 SST Climatological Cycle")
ax.grid(True, linestyle="--", alpha=0.5)
plt.tight_layout()
plt.savefig("mediterranean_sst_climatology.png", dpi=150)
plt.show()
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|
Missing credentials error | COPERNICUSMARINE_USERNAME / PASSWORD not set | Run copernicusmarine login or export env vars |
TimeoutError on large downloads | Requesting too large a spatial/temporal domain | Split into smaller chunks; use cm.subset() to download files locally |
KeyError: 'N_PROF' | Argo dataset dimension name differs by product | Inspect ds.dims and adapt index variable |
gsw returns NaN arrays | Pressure must be in dbar and non-negative; salinity must be > 0 | Filter out fill values (typically 99999) before calling gsw functions |
| Cartopy install fails on Windows | Binary wheel not available | Use conda install -c conda-forge cartopy instead of pip |
404 Not Found for dataset_id | Product retired or renamed in CMEMS catalog | Run cm.describe(contains=["keyword"]) to find current product ID |
External Resources
Examples
Example 1 – Black Sea Surface Salinity Trend (2015–2023)
import copernicusmarine as cm
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
from scipy import stats
ds = cm.open_dataset(
dataset_id="cmems_mod_blk_phy_my_2.5km_P1M-m",
variables=["so"],
minimum_depth=0.0,
maximum_depth=1.0,
start_datetime="2015-01-01T00:00:00",
end_datetime="2023-12-31T23:59:59",
)
sal = ds["so"].isel(depth=0)
basin_mean = sal.mean(dim=["latitude", "longitude"])
times = basin_mean["time"].values
decimal_years = [
float(str(t)[:4]) + float(str(t)[5:7]) / 12
for t in basin_mean["time"].dt.strftime("%Y-%m").values
]
slope, intercept, r, p, se = stats.linregress(decimal_years, basin_mean.values)
trend_line = slope * np.array(decimal_years) + intercept
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(decimal_years, basin_mean.values, color="steelblue", lw=1.5,
label="Monthly mean salinity")
ax.plot(decimal_years, trend_line, "r--", lw=2,
label=f"Trend: {slope*10:.3f} PSU/decade (p={p:.3f})")
ax.set_xlabel("Year")
ax.set_ylabel("Surface Salinity (PSU)")
ax.set_title("Black Sea Basin-Mean Surface Salinity 2015–2023")
ax.legend()
ax.grid(True, linestyle="--", alpha=0.4)
plt.tight_layout()
plt.savefig("black_sea_salinity_trend.png", dpi=150)
plt.show()
print(f"Trend: {slope*10:.4f} PSU/decade, R²={r**2:.3f}, p={p:.4f}")
Example 2 – Argo Float Profile Visualization with MLD Overlay
import gsw
import numpy as np
import matplotlib.pyplot as plt
import copernicusmarine as cm
argo = cm.open_dataset(
dataset_id="cmems_obs-ins_glo_phy-temp-sal_nrt_argo_P1D-m",
variables=["TEMP", "PSAL", "PRES", "TIME", "LATITUDE", "LONGITUDE"],
minimum_longitude=-35.0,
maximum_longitude=-30.0,
minimum_latitude=45.0,
maximum_latitude=50.0,
start_datetime="2023-07-01T00:00:00",
end_datetime="2023-09-30T23:59:59",
)
i_prof = 0
pres = argo["PRES"].isel(N_PROF=i_prof).values
temp = argo["TEMP"].isel(N_PROF=i_prof).values
psal = argo["PSAL"].isel(N_PROF=i_prof).values
lat = float(argo["LATITUDE"].isel(N_PROF=i_prof).values)
valid = (pres > 0) & (psal > 0) & np.isfinite(temp)
pres, temp, psal = pres[valid], temp[valid], psal[valid]
SA = gsw.SA_from_SP(psal, pres, 0.0, lat)
CT = gsw.CT_from_t(SA, temp, pres)
sigma0 = gsw.sigma0(SA, CT)
ref = sigma0[0]
mld_idx = np.where(sigma0 - ref > 0.03)[0]
mld = pres[mld_idx[0]] if len(mld_idx) > 0 else pres[-1]
fig, axes = plt.subplots(1, 3, figsize=(12, 6), sharey=True)
ax_t, ax_s, ax_d = axes
ax_t.plot(CT, pres, "b-", lw=1.5)
ax_t.axhline(mld, color="red", ls="--", label=f"MLD={mld:.0f} m")
ax_t.set_xlabel("Conservative Temp CT (°C)")
ax_t.set_ylabel("Pressure (dbar)")
ax_t.invert_yaxis()
ax_t.legend(fontsize=9)
ax_t.grid(True, linestyle="--", alpha=0.4)
ax_t.set_title("Temperature")
ax_s.plot(SA, pres, "g-", lw=1.5)
ax_s.axhline(mld, color="red", ls="--")
ax_s.set_xlabel("Absolute Salinity SA (g/kg)")
ax_s.grid(True, linestyle="--", alpha=0.4)
ax_s.set_title("Salinity")
ax_d.plot(sigma0, pres, "k-", lw=1.5)
ax_d.axhline(mld, color="red", ls="--")
ax_d.set_xlabel("Potential Density σ₀ (kg/m³)")
ax_d.grid(True, linestyle="--", alpha=0.4)
ax_d.set_title("Potential Density")
plt.suptitle(f"Argo Profile – Lat {lat:.2f}°N", fontsize=12)
plt.tight_layout()
plt.savefig("argo_profile_mld.png", dpi=150)
plt.show()