Working with Oceanum Datamesh¶
Datamesh is Oceanum's data platform. waveray needs two things from it — bathymetry for the local grid, and offshore spectra for the boundary condition — and both come from the same Python client.
Datamesh is optional. waveray works with any xarray bathymetry and any
wavespectra-readable spectrum from any source. This page is for when you do use
it.
Setup¶
pip install "waveray[datamesh]" # pulls in the `oceanum` package
export DATAMESH_TOKEN="..." # your Datamesh token
Get a token from your Oceanum account.
Every client call reads DATAMESH_TOKEN from the environment; you can also pass
one explicitly (Connector(token=...), or fetch_datamesh_bathymetry(..., token=...)).
Warning
Treat the token as a credential. Keep it in the environment or a secrets manager — never commit it, and never leave it in a notebook cell output.
from oceanum.datamesh import Connector
conn = Connector() # reads $DATAMESH_TOKEN, talks to https://datamesh.oceanum.io
Finding data¶
Browse datasets.oceanum.io or the Datamesh UI, or search the catalog from Python:
cat = conn.get_catalog(search="wave hindcast spectra")
for ds in cat:
print(ds.id, "-", ds.name)
meta = conn.get_datasource("oceanum_wave_dutch_era5_v1_spec")
print(meta.tstart, meta.tend, meta.coordinates, meta.geom)
get_catalog(search=..., timefilter=..., geofilter=..., limit=...) filters the
catalog; get_datasource(id) returns the full metadata record (id, name,
description, geom, tstart, tend, tags, labels, coordinates,
dataschema, details, …). Check coordinates before querying — it tells you
what the time, x/y, frequency and direction dimensions are called.
Datasources used in this project¶
These are the ones the bundled examples and notebooks actually query.
| Datasource | What it is |
|---|---|
gebco_2023, gebco_2025 |
Global 15-arcsecond bathymetry. Elevation, positive up. |
oceanum_wave_dutch_era5_v1_spec |
SWAN 1 km Dutch waters, hourly spectra, 1979–present |
oceanum_wave_ec_abrol500m_spec_nowcast |
SWAN 500 m Abrolhos (WA) spectra, nowcast |
oceanum_wave_weuro_era5_v1_spec |
SWAN 5 km Western Europe spectra |
oceanum_wave_waddenzee_nora3_v1_spec |
SWAN ~500 m Waddenzee spectra |
oceanum_wave_glob05_era5_v1_spec |
Global WW3 0.5° spectra, 26,962 sites, 1979–present |
oceanum_wave_glob05_gfs_spec_nowcast |
Global WW3 0.5° spectra, GFS-forced nowcast |
Regional SWAN spectra make the best boundary condition, because they already contain the shelf-scale transformation. A global WW3 datasource works when nothing regional exists, but see Limitations — the directional accuracy of your boundary spectra bounds your nearshore skill.
Bathymetry¶
fetch_datamesh_bathymetry is a thin convenience wrapper: it issues a bbox
query, takes the first data variable unless you name one, and returns a
LocalGrid.
from waveray import fetch_datamesh_bathymetry
grid = fetch_datamesh_bathymetry(
"gebco_2023",
bbox=(3.85, 52.00, 4.55, 52.50), # (west, south, east, north) degrees
positive="up", # GEBCO is elevation: seabed negative
)
print(grid.x.size, "x", grid.y.size, "nodes at ~", grid.spacing[0], "m")
Important
positive="up" for elevation sources (GEBCO), positive="down" for
depth sources. Getting it backwards turns your ocean into a mountain and
every ray will immediately ground. Sanity check with
grid.depth[~grid.land].max().
Equivalent by hand, if you want the raw Dataset:
ds = conn.query({
"datasource": "gebco_2023",
"geofilter": {"type": "bbox", "geom": [3.85, 52.00, 4.55, 52.50]},
})
grid = LocalGrid.from_dataarray(ds["elevation"], positive="up")
GEBCO is ~450 m at these latitudes. That is fine for open-coast refraction and too coarse to resolve a reef, an islet or a breakwater — see Limitations. Where sheltering matters, query a survey-resolution regional grid instead.
Spectra¶
A spectral query needs a geofilter (which sites) and a timefilter (which times).
ds = conn.query({
"datasource": "oceanum_wave_dutch_era5_v1_spec",
"geofilter": {"type": "bbox", "geom": [3.85, 52.00, 4.55, 52.50]},
"timefilter": {"times": ["2023-12-15T00:00:00Z", "2024-01-15T00:00:00Z"]},
})
efth = ds["efth"] # (time, site, freq, dir), wavespectra convention
The result already speaks waveray's language: efth with freq in Hz and dir
as coming-from nautical degrees. Pass it straight to
SiteModel.transform.
Query reference¶
The query dict is validated against oceanum.datamesh.query.Query. The fields
that matter here:
| Field | Purpose |
|---|---|
datasource |
the datasource id (required) |
geofilter |
{"type": "bbox", "geom": [w, s, e, n]} or {"type": "feature", "geom": <GeoJSON Feature>} |
timefilter |
{"times": [start, end]} — also accepts ISO-8601 durations, e.g. "-P7D" for "seven days ago" |
variables |
list of variables to return, to trim the payload |
limit |
cap the number of records |
crs |
reproject the result |
Geofilter types are bbox and feature; the interp option is nearest
(default) or linear. Timefilter types are range, series and trajectory.
conn.query(..., use_dask=True) returns a lazily-loaded Dataset, and
cache_timeout=<seconds> caches the response locally.
Full details: Datamesh Python client and the oceanum-python API reference.
Selecting boundary points¶
waveray needs the boundary spectra in the same order as the
boundary_points you pass to SiteModel.build. A robust recipe — probe one
timestep to find the sites, choose an offshore arc, then fetch only those sites:
import numpy as np
# 1. one cheap timestep to discover the site positions
probe = conn.query({
"datasource": SPEC,
"geofilter": {"type": "bbox", "geom": list(BBOX)},
"timefilter": {"times": [T0, T0]},
})
lons = np.atleast_1d(probe["lon"].values)
lats = np.atleast_1d(probe["lat"].values)
# 2. keep sites deep enough to sit outside the last-stage transformation,
# spread by azimuth around the target
xs, ys = grid.to_local(lons, lats)
deep = np.flatnonzero(grid.sample_depth(xs, ys) >= 16.0)
az = np.arctan2(ys[deep] - ty, xs[deep] - tx)
picks = deep[np.argsort(az)][np.linspace(0, deep.size - 1, 6).astype(int)]
bpts = [(float(lons[i]), float(lats[i])) for i in picks]
# 3. fetch the full time series for just those sites, one small query each
import xarray as xr
parts = []
for lon, lat in bpts:
d = conn.query({
"datasource": SPEC,
"geofilter": {"type": "bbox",
"geom": [lon - 0.004, lat - 0.004, lon + 0.004, lat + 0.004]},
"timefilter": {"times": TIMES},
})
sd = next(x for x in d["efth"].dims if x not in ("time", "freq", "dir"))
parts.append(d["efth"].isel({sd: 0}).rename({sd: "site"}))
efth_b = xr.concat(parts, dim="site").transpose("time", "site", "freq", "dir")
Several small per-site queries beat one big bbox query: you get exactly the sites you asked for, in the order you asked for them, and you do not download a month of spectra for sites you will discard.
If the spectra carry lon/lat coordinates, transform verifies they
match the boundary points used at build time and raises rather than silently
mixing sites up.
Gotchas seen in real Datamesh data¶
These are not hypothetical — each one cost time in this project.
Warning
The dir coordinate is not monotonic. oceanum_wave_dutch_era5_v1_spec
ships directions running 265° → 5°, then wrapping 355° → 275°. waveray
sorts internally with circular interpolation and returns the spectrum on your
original axis, so this is invisible to you — unless you plot it yourself
with pcolormesh on polar axes, which requires a monotonic theta and will
otherwise smear energy across the circle. Sort before plotting.
The site dimension is not always called site. Discover it rather than
assume it:
Don't assume coordinate shapes. A one-site bbox still returns length-1
arrays and keeps the site dimension (verified on the Dutch and Abrolhos
spectra), but np.atleast_1d(ds["lon"].values) is a cheap guard that keeps the
same code working if a driver ever collapses the dimension.
Spectral sites near a bbox edge may fall outside your bathymetry grid.
GEBCO coordinates are cell centres, so the grid's usable extent is slightly
inside the bbox you requested. Keep a margin (~2 km) when picking interior sites,
or LocalGrid will reject the target as outside its bounds.
Datasource ids encode the model, forcing and product. Read them as
oceanum_<var>_<domain><res>_<forcing>_<grid|spec>_<nowcast|forecast>; a
_spec datasource holds 2-D spectra at sites, a _grid datasource holds
integrated parameters on a grid. You want _spec for waveray's input.
A complete example¶
examples/demo_holland.py
does all of the above end to end — bathymetry, site discovery, boundary
selection, transformation and validation against SWAN's own nearshore answer.
The same material with plots is in
notebooks/01_holland_downscaling.ipynb.
Run it with:
Reference¶
| Resource | Link |
|---|---|
| Datamesh overview | https://docs.oceanum.io/datamesh/overview |
| Datamesh Python client | https://docs.oceanum.io/datamesh/integrations/python |
| Datamesh UI | https://docs.oceanum.io/datamesh/ui |
| Getting an account and token | https://docs.oceanum.io/get-started/account |
| Select-and-export tutorial | https://docs.oceanum.io/tutorials/select-and-export |
| Dataset catalogue | https://datasets.oceanum.io/ |
oceanum Python package |
https://pypi.org/project/oceanum/ · source |
oceanum-python API docs |
https://oceanum-python.readthedocs.io/en/latest/api.html |