Reproducible extraction of ERA5 marine wind and wave fields from the ECMWF Climate Data Store, plus the unit and direction conversions that maritime work actually needs.
from era5_marine import Era5MarineClient
client = Era5MarineClient(output_dir="data")
client.retrieve(
"marine-full",
bbox=(-10, 35, 40, 60), # (min_lon, min_lat, max_lon, max_lat)
start="2025-01-01",
end="2025-01-03",
)The CDS request format is a nested dictionary of strings whose valid values differ per dataset, are validated only server-side, and fail after the request has been queued. A misspelled variable name can sit in a queue for twenty minutes before telling you. This package validates locally first, and encodes three things that otherwise cost people an afternoon each:
ERA5 is a reanalysis, not a forecast. It runs about five days behind, and the final stream lags by months. Ask for yesterday and you get an empty result, not an error explaining why. Requests past the latency window are rejected here with a message that says so:
ERA5 is a reanalysis with roughly 6 days of latency; the latest reliably
available date is 2026-07-27, but 2030-01-02 was requested. For anything more
recent you need a forecast product, not ERA5.
Area order. CDS wants [north, west, south, east]. Every other tool in the
geospatial world wants (min_lon, min_lat, max_lon, max_lat). The conversion
happens here so you never have to remember which is which — and so a
transposed box fails as a validation error rather than as a silently wrong
region.
Requests are queued server-side. Large area/time requests can take hours.
--dry-run prints exactly what would be submitted so you can check before
committing.
A live capture, same North Atlantic region as the companion CMEMS package's
current field: the download came back as a ZIP, not a plain NetCDF —
wind/pressure and wave data live in separate ECMWF "streams" on different
native grids (0.25° vs 0.5° here), and CDS does not merge them. Variables also
come back under GRIB short names (u10, not 10m_u_component_of_wind).
Neither is a bug in this package; both would silently break code that assumed
a single NetCDF with the requested variable names. open_capture() handles
it:
from era5_marine import open_capture
result = open_capture("your_download.nc") # zip-safe, despite the extension
result.keys() # dict_keys(['oper', 'wave'])
result["oper"].u10 # GRIB short name — see SHORT_NAME_MAP for the mappingAt one real point, wind (4.7 kt, Beaufort 2, from the east) and waves (1.0 m,
Douglas 3, from the west-southwest) combine into a following sea relative
to a 42.5° course — computed with this package's own encounter_angle(), not
asserted. Full write-up: docs/ANALYSIS.md
pip install git+https://github.com/kavianpour/era5-marine-fetch- Register free at https://cds.climate.copernicus.eu/
- Copy your Personal Access Token from the profile page
- Write
~/.cdsapirc:
url: https://cds.climate.copernicus.eu/api
key: <YOUR-PERSONAL-ACCESS-TOKEN>
- Accept the licence for each dataset once, in the web interface. Skipping this rejects every request, and the error message does not make the reason obvious.
era5-marine check| Key | Contents |
|---|---|
wind |
10 m u/v wind components |
waves |
significant height, mean direction, mean and peak period |
swell |
swell partition separated from local wind sea |
marine-full |
wind, sea state, swell and mean sea level pressure |
pressure |
mean sea level pressure |
era5-marine setsAll draw on reanalysis-era5-single-levels.
era5-marine fetch --variable-set waves \
--bbox -10 35 40 60 --start 2025-01-01 --end 2025-01-03 --dry-run
era5-marine fetch --variable-set marine-full \
--bbox -10 35 40 60 --start 2025-01-01 --end 2025-01-03 \
--hours 0 6 12 18 --grid 0.5 --out-dir dataThe default time step is 3-hourly, which is usually enough for route planning and a quarter of the volume of hourly.
from era5_marine import wind_speed_direction, encounter_angle, beaufort, douglas_sea_state
w = wind_speed_direction(u_ms=10.0, v_ms=0.0)
# {'speed_ms': 10.0, 'speed_kts': 19.44, 'direction_deg': 270.0}Wind is stored as u/v components, not speed and direction, and the
meteorological conversion is not atan2(v, u) — it is rotated and reversed
relative to the mathematical one. A wind blowing toward the east is a
westerly, reported as 270°.
Directions are "coming-from"; ship courses are "going-to". Mixing them puts
a headwind on your stern. encounter_angle handles it:
encounter_angle(course_deg=0.0, from_direction_deg=0.0) # 0 head seas
encounter_angle(course_deg=0.0, from_direction_deg=90.0) # 90 beam seas
encounter_angle(course_deg=0.0, from_direction_deg=180.0) # 180 following seasReturns 0–180 with correct wraparound. This is the quantity every added- resistance formulation is parameterised on.
Two operational scales are included because charter-party weather clauses and operability thresholds are written in them, not in m/s:
beaufort(37) # 8
douglas_sea_state(2.0) # 4 (moderate)Note that Douglas state 0 means a flat, glassy sea and is reserved for exactly zero — 0–0.1 m is state 1. Getting that boundary wrong shifts every state by one, which matters when a clause reads "sea state 5 or above".
Every retrieval writes a .provenance.json sidecar with the dataset name, the
full request dictionary, the variable set, the request timestamp, and the
SHA-256 of the downloaded file.
ERA5 is distributed under the Copernicus licence: free reuse, redistribution and commercial use, conditional on attribution to the Copernicus Climate Change Service and ECMWF. Cite the specific dataset name, version and access date. This package's code is MIT.
MIT