Skip to main content

waveray

Docs CI PyPI License

Fast last-stage nearshore spectral wave transformation: precomputed backward ray-traced linear transfer operators over local bathymetry, with parametric depth-limited wave breaking at the target.

📖 Documentation: https://oceanum.github.io/waveray/

Built to downscale directional wave spectra from SWAN (or WW3) hindcasts and nowcasts through the final nearshore transformation to a target site — decades of hourly spectra in seconds, no wave model in the runtime loop.

Install

pip install waveray                # core
pip install "waveray[datamesh]"    # + Oceanum Datamesh bathymetry/spectra access

Documentation

Full docs: https://oceanum.github.io/waveray/

Guide Read it for
Installation Install, extras, environment
Quickstart A working example in 20 lines
Concepts What the operator is, and the physics inside it
User guide Bathymetry, boundary points, tide, breaking, persistence, ray export
Oceanum Datamesh Tokens, catalog search, datasource ids, query recipes and gotchas
wavespectra interop Spectral conventions and the .spec accessor
Validation Measured skill against parent SWAN models
Limitations What waveray does not model
API reference Every public function and its arguments

The pages are plain Markdown in docs/ and render on GitHub too.

Method

Setup, once per site:

  1. Pull local bathymetry (e.g. from an Oceanum Datamesh datasource) into a LocalGrid on a local tangent plane.
  2. For every (frequency, direction) bin, trace ray bundles backward from the target over the 2D bathymetry (O'Reilly & Guza style spectral refraction). Each sub-ray records where and in what direction it exits the domain, or whether it runs aground (island/headland sheltering).
  3. Assemble a linear operator T[f, dir_t, bp, dir_b] mapping K boundary spectra (the SWAN output points around the domain perimeter — alongshore inhomogeneity is handled by exit-point interpolation) to the target spectrum. The per-ray coefficient is the exact linear invariant E(f, theta) * c * cg = const, which reproduces Snell refraction and energy-flux shoaling identically (validated against analytic plane-beach solutions in the test suite).

Along each ray path the coefficients also integrate the linear source terms: JONSWAP bottom friction (on by default) and, when a wind is supplied, SWAN's wind input — the Komen et al. (1984) exponential growth folded into the same path exponent, plus (optionally, agrow=True) the Cavaleri & Malanotte-Rizzoli (1981) linear growth accumulated into an additive spectrum that seeds locally generated wind sea. Wind can be a uniform (speed, direction) or a gridded snapshot, with u* from the Zijlema et al. (2012) drag law (SWAN's default).

Runtime, per hindcast:

  1. E_t = einsum(T, E_b) over all timesteps at once.
  2. Depth-limited breaking as a nonlinear post-step: transformed Hm0 is capped at a Miche-type (Battjes-Janssen) or gamma * d limit with optional tide-modulated depth; the cap scales the spectrum proportionally, matching how SWAN distributes surf-breaking dissipation over the spectrum.

The stationary, linear-propagation assumption (no temporal nonlinear evolution, no quadruplets/triads) is deliberate: over a last-mile nearshore domain the propagation time is minutes and the transformation is dominated by refraction, shoaling, sheltering and breaking.

Quickstart

import numpy as np
from waveray import SiteModel, fetch_datamesh_bathymetry

bathy = fetch_datamesh_bathymetry(
    "gebco_2025", bbox=(114.35, -28.95, 114.65, -28.60), positive="up"
)
model = SiteModel.build(
    bathy=bathy,
    target=(114.58, -28.77),                     # lon, lat
    boundary_points=[(114.35, -28.90), (114.35, -28.65)],  # SWAN output sites
    freqs=efth_boundary.freq.values,
    dirs=efth_boundary.dir.values,
)
efth_near = model.transform(efth_boundary, tide=tide_series)   # (time, freq, dir)
model.to_netcdf("geraldton_berth.nc")            # reuse without rebuilding

efth follows the wavespectra convention: dims (time, [site,] freq, dir), dir = coming-from nautical degrees, density per Hz per degree. The site dimension (K boundary points, same order as boundary_points) is required when K > 1.

Ray paths can be exported for inspection (QGIS, EIDOS, any web map) as a GeoJSON FeatureCollection — one MultiLineString per (freq, dir) bin, with period, direction, per-ray status and friction decay in the properties:

from waveray import ray_paths_geojson

x, y = bathy_grid.to_local(lon, lat)
ray_paths_geojson(bathy_grid, (x[0], y[0]), freqs=[0.06, 0.1],
                  dirs=np.arange(0, 360, 15), path="rays.geojson")

Notebooks

Executed, illustrated notebooks (plots included in the committed output):

Notebook What it shows
notebooks/01_holland_downscaling.ipynb End-to-end downscaling of a SWAN 1 km hindcast to a point off Noordwijk aan Zee through storms Pia and Henk: bathymetry, ray geometry and the arrival cone, offshore vs nearshore Hs, the directional spectrum before/after, validation against SWAN (r ≈ 0.99), and what depth-limited breaking contributes.
notebooks/02_abrolhos_validation.ipynb Held-out validation on a reef-fronted WA coast, and an ablation isolating JONSWAP bottom friction (Hm0 bias +1.11 m → +0.18 m when friction is on).
notebooks/03_boundary_line_termination.ipynb Terminating rays on the line/ring through interior output sites (boundary_mode="line"/"ring") instead of the grid edge — plotted ray geometry and the shoaling-coefficient difference on a synthetic bay (no Datamesh token needed).
notebooks/04_swan_validation_idealised.ipynb Against stationary SWAN on idealised bathymetry, both models fed the identical boundary spectrum: plane beach shore-normal (Hs within 0.31%), 30° oblique refraction (direction within 0.91°), and a circular island where the deep lee is where a ray model and a spectral model legitimately part company.
notebooks/05_swan_validation_wind.ipynb Wind input against SWAN: fetch-limited growth from calm, swell + wind on a plane beach, and the runaway demonstration — unbalanced wind input spans 156 orders of magnitude across the frequency range before the max_growth ceiling contains it.
uv sync --extra datamesh --extra notebooks
DATAMESH_TOKEN=... uv run jupyter lab notebooks/

Limitations (v0)

  • No diffraction: accuracy degrades inside harbours / behind breakwaters, and island shadows are sharper than reality (rays block, they do not leak).
  • Island / headland sheltering is included — a ray grounding on any land carries no energy — but blocking is binary and only as good as the bathymetry: a feature smaller than ~2 grid cells is smoothed away by the bilinear depth sampling and will not shelter (GEBCO at ~450 m cannot shelter behind a small reef or islet). See tests/test_island.py.
  • Bottom friction IS included (JONSWAP, integrated along ray paths) and is ON by default with the SWAN swell coefficient cf_jonswap=0.038; pass cf_jonswap=None for pure refraction + shoaling. No triad interactions.
  • Wind input (SWAN formulations) is available but stationary: the wind is baked into the operator at build time, like the bathymetry — build operators per wind condition if the wind matters and varies. Only the wind input term is represented; its nonlinear counterweights (whitecapping, quadruplets) cannot live in a linear operator, so wind input is unbounded and ships with a max_growth ceiling that warns when it binds. This is not a ray-method artefact — SWAN with its sinks disabled grows a 2 m swell to 77.8 m over the same 15 km fetch. An optional wind-sea saturation closure (off by default, empirical rather than derived) caps the energy the wind adds — never the total, so swell is exempt by construction — bringing the measured error against full-physics SWAN to within 13 % with swell present and ~28 % worst case generating a sea across 8–18 m/s, against an uncapped worst of 466 %.
  • Breaking is an endpoint cap, not accumulated dissipation along the approach — appropriate at berths and outside the inner surf zone; tune gamma per site against observations.
  • The operator is built at a fixed water level; tide only modulates the breaking cap. For strongly tide-dependent refraction, build operators per tide stage.
  • Boundary spectra quality bounds achievable skill — nested-model literature consistently finds offshore directional accuracy, not nearshore resolution, is the limiting factor.

Development

uv sync                     # or: uv sync --extra datamesh
uv run pytest               # physics-validation suite
uv run ruff check . && uv run ruff format --check .

Comparison against a real SWAN run is opt-in — it pulls the official SWAN docker image and takes a few minutes:

docker pull delftwaves/swan:latest
uv run pytest -m swan -s    # stationary SWAN 41.51A vs waveray, same boundary spectra

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

waveray-0.3.0.tar.gz (39.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

waveray-0.3.0-py3-none-any.whl (46.5 kB view details)

Uploaded Python 3

File details

Details for the file waveray-0.3.0.tar.gz.

File metadata

  • Download URL: waveray-0.3.0.tar.gz
  • Upload date:
  • Size: 39.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for waveray-0.3.0.tar.gz
Algorithm Hash digest
SHA256 62c4b20729590fa72551920e3bebd794aa4cb04b254a5f0d88db01c709240b7a
MD5 3a759a06cf0955bd8ba5f0b472a7d114
BLAKE2b-256 86d68458011e8ed19dd010d2ec4d2b30b4c26ddfb2f87616de8e206ae23255c8

See more details on using hashes here.

Provenance

The following attestation bundles were made for waveray-0.3.0.tar.gz:

Publisher: publish.yml on oceanum/waveray

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file waveray-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: waveray-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 46.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for waveray-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b59be9d30613956a8231bbb23e004e5c7a7ef6ef0067778afe8b0b4493d03e9
MD5 9c4702dc71c4dc2762d4619531a621d5
BLAKE2b-256 f6026c04896e777c5abf642d58a9d51e22ed933fbcc15b50b1d88f09d3b5ab02

See more details on using hashes here.

Provenance

The following attestation bundles were made for waveray-0.3.0-py3-none-any.whl:

Publisher: publish.yml on oceanum/waveray

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page