Skip to content

ENH: Support for Open-Meteo API in the Environment class - #1119

Open
Gui-FernandesBR wants to merge 7 commits into
developfrom
enh/520-open-meteo-api
Open

ENH: Support for Open-Meteo API in the Environment class#1119
Gui-FernandesBR wants to merge 7 commits into
developfrom
enh/520-open-meteo-api

Conversation

@Gui-FernandesBR

Copy link
Copy Markdown
Member

Closes #520

Pull request type

  • Code changes (bugfix, features)
  • ReadMe, Docs and GitHub updates

Checklist

  • Tests for the changes have been added (if needed)
  • Docs have been reviewed and added / updated
  • Lint (ruff check / ruff format) has passed locally
  • All tests (pytest tests -m slow --runslow) have passed locally
  • CHANGELOG.md — entries added manually as well

Current behavior

Reaching for reanalysis data means downloading files by hand from the Copernicus
Climate Data Store, and pulling a forecast means an OPeNDAP request through
netCDF4, which is heavy and slow. On top of that, the GEFS ensemble shortcut
is currently disabled because NOMADS OPeNDAP was deactivated, so there is no
working out-of-the-box ensemble source.

New behavior

Adds Open-Meteo as an atmospheric data source. It
serves pressure-level data as plain JSON over HTTPS, with no API key and no
netCDF4/OPeNDAP dependency (requests is already a RocketPy dependency, so
this adds none).

Two new model types:

env.set_atmospheric_model("open_meteo")                          # best_match
env.set_atmospheric_model("open_meteo", file="ecmwf_ifs025")     # specific model
env.set_atmospheric_model("open_meteo_ensemble", file="gfs05")   # 31 members
env.select_ensemble_member(10)

Past launches need no extra argument — a past date is transparently routed to
Open-Meteo's historical-forecast archive:

env.set_date([2024, 1, 10, 12])
env.set_atmospheric_model("open_meteo")   # historical-forecast API

The ensemble model stores every member, so select_ensemble_member() and
plots.ensemble_member_comparison() work exactly as they do for GEFS, with the
unperturbed control run exposed as member 0. Open-Meteo also reports the
elevation of the grid cell it answered for, so the launch site elevation is set
automatically.

Notable API findings

Each of the following returns HTTP 200, so none of them fail loudly. They were
found by probing the live API and they shaped the implementation:

  • Open-Meteo's ERA5 archive endpoint serves no pressure-level data at all
    nulls at every level and date tested. It is therefore not used; the
    historical-forecast API is the only archive that can feed a vertical profile.
  • The historical archive starts around March 2021 (2021-03-15 comes back
    empty, 2021-03-23 is complete). Earlier dates now raise a warning naming the
    cutoff and pointing at the reanalysis and sounding models, instead of failing
    with a generic "not enough pressure levels" error.
  • Only gfs05 (31 members) and ecmwf_ifs025 (51) publish complete
    pressure-level data.
    gem_global is the subtle one: it serves temperature
    and geopotential height but no pressure-level winds, so probing temperature
    alone makes it look usable. gfs025, icon_global and
    bom_access_global_ensemble return nulls everywhere. All of these are now
    rejected up front with an actionable message.
  • Open-Meteo defaults to km/h, so wind_speed_unit=ms is requested explicitly;
    dropping it would silently inflate wind speeds by 3.6x.

Coverage of pressure levels varies per model (gfs_seamless reaches 30 hPa,
ecmwf_ifs025 stops at 50 hPa), so levels a model does not resolve are dropped
while parsing.

Drive-by bug fix

The model-type gates in _EnvironmentPrints and _EnvironmentPlots compared
against capitalised literals ("Ensemble", "Forecast"), but
set_atmospheric_model documents type as case-insensitive and stores it as the
user spelled it. So type="ensemble" silently printed no time period and no
member count, and skipped the ensemble comparison plot. They now compare
case-insensitively.

Breaking change

  • No

Additional information

Implementation notes

  • Open-Meteo reports wind as speed/direction rather than u/v, so
    convert_wind_speed_direction_to_components was added to
    environment/tools.py. It converts the meteorological blows-from convention
    into RocketPy's East/North components, and is round-trip tested against
    calculate_wind_heading — getting that convention wrong would flip the wind
    by 180°.
  • Temperatures are converted from Celsius to Kelvin, pressure levels from hPa to
    Pa, and geopotential heights to geometric altitude.
  • to_dict/from_dict round-trip both new types, including every ensemble
    member.

Verification

  • 50 new unit tests, all offline: the fetchers are patched, and the suite was
    re-run with socket.connect blocked to confirm no network access.
  • 6 new integration tests marked slow, exercising the live API (forecast across
    4 models, the historical archive, and the ensemble with member selection).
  • The full unit suite passes: 1954 passed, 16 skipped.
  • The full tests/integration/environment suite passes with --runslow: 29
    passed, 1 skipped.
  • Every code block in the new docs page runs as part of the docs build; all five
    were executed against the live API.
  • End-to-end sanity check: a full Calisto flight using open_meteo at Spaceport
    America gives 3421 m AGL apogee at Mach 0.82, with the 1405 m site elevation
    picked up automatically from the API.

Docs

New page at docs/user/environment/1-atm-models/open_meteo.rst, cross-referenced
from the forecast, reanalysis and ensemble pages, since Open-Meteo is the lighter
alternative in each of those cases — and currently the only working ensemble
source while the GEFS shortcut is down.

🤖 Generated with Claude Code

Gui-FernandesBR and others added 6 commits August 9, 2026 23:38
Wraps the three Open-Meteo endpoints RocketPy needs to build atmospheric
profiles: the forecast API, the historical-forecast API (for past launch
dates) and the ensemble API. All of them serve pressure-level data as plain
JSON over HTTPS, with no API key and no netCDF/OPeNDAP dependency.

Note that Open-Meteo's ERA5 archive endpoint is deliberately not used: it
serves surface variables only and answers with nulls at every pressure
level, so the historical-forecast API (available from 2021 onwards) is the
only archive that can feed a vertical profile.

Ensemble models are restricted to the ones that actually publish
pressure-level data (gfs05, ecmwf_ifs025, gem_global); the others return
HTTP 200 with null values, which would otherwise surface as an opaque
failure much later in the parsing step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds two new atmospheric model types to set_atmospheric_model:

  env.set_atmospheric_model("open_meteo")                        # best_match
  env.set_atmospheric_model("open_meteo", file="ecmwf_ifs025")
  env.set_atmospheric_model("open_meteo_ensemble", file="gfs05")

Both build the usual pressure, temperature and wind profiles from
Open-Meteo pressure-level data, so no external files and no netCDF/OPeNDAP
libraries are involved. When the launch date is in the past, "open_meteo"
transparently queries Open-Meteo's historical-forecast archive instead of
the live forecast, which is what makes past-launch reconstruction work
without downloading reanalysis files by hand.

The ensemble processor stores every member, so select_ensemble_member() and
plots.ensemble_member_comparison() work exactly as they do for GEFS. The
unsuffixed control run is kept as member 0, matching the documented
convention that member 0 is the unperturbed control.

Open-Meteo reports wind as speed/direction rather than u/v components, so
convert_wind_speed_direction_to_components is added to environment.tools;
it converts the meteorological blows-from convention into RocketPy's
East/North components. Temperatures are converted from Celsius to Kelvin
and pressure levels from hPa to Pa.

The model-type gates in the prints and plots classes were comparing
capitalised literals ("Ensemble"), which never matched a lower-case type
even though set_atmospheric_model documents the argument as
case-insensitive. They now compare case-insensitively, so both the new
Open-Meteo types and a lower-case "ensemble" report their time period and
member count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds 44 offline unit tests (tests/unit/environment/test_open_meteo.py) and 6
live integration tests marked slow.

The unit tests patch the fetchers, so the whole module runs without network
access: verified by re-running the suite with socket.connect blocked, where
all 44 still pass. They cover the unit conversions (hPa to Pa, Celsius to
Kelvin, speed/direction to u/v), the nearest-hour selection, skipping levels
a model does not resolve, altitude sorting, the ensemble member layout with
the control run as member 0, the endpoint routing for past versus future
dates, error payload handling, and to_dict/from_dict round trips.

The wind-component conversion is tested against the four cardinal directions
and round-tripped through calculate_wind_heading, since getting that
convention wrong would silently flip the wind by 180 degrees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verifying the ensemble models against the live API showed gem_global cannot
feed a RocketPy profile: it publishes temperature and geopotential height at
pressure levels but no pressure-level winds at all (168/168 hours null for
wind_speed and wind_direction at every level, at three different launch
sites). The earlier check only probed temperature, which is why it looked
usable.

Accepting it meant "Open-Meteo returned fewer than two usable pressure
levels" at profile-build time instead of an actionable message naming the
model, so it is now rejected up front alongside gfs025, icon_global and
bom_access_global_ensemble. gfs05 (31 members) and ecmwf_ifs025 (51
members) are the two that publish the full set; both member counts are
confirmed against the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds docs/user/environment/1-atm-models/open_meteo.rst, covering the
forecast, past-launch and ensemble workflows, the model tables, and the
caveats worth knowing: coverage of pressure levels varies per model, the
historical archive only reaches back to 2021, and Open-Meteo's ERA5 endpoint
cannot be used because it serves no pressure-level data.

Cross-references were added from the forecast, reanalysis and ensemble pages,
since Open-Meteo is the lighter alternative in each of those cases -- notably
for ensembles, where the GEFS shortcut is currently unavailable.

Every code block in the new page runs as part of the docs build; all five
were executed against the live API to confirm they work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Open-Meteo answers historical requests for unsupported dates with HTTP 200
and null values at every pressure level, so a pre-archive launch date used to
surface only as a generic "fewer than two usable pressure levels" error, with
no hint that the date itself was the problem. Such dates now raise a warning
naming the archive start and pointing at the reanalysis and sounding models.

The cutoff was probed against the live API rather than assumed: 2021-03-15
comes back empty while 2021-03-23 is complete, so the archive starts in March
2021 and not in January as the previous constant implied. The constant is now
a date instead of a year, and the docs state March 2021.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Gui-FernandesBR
Gui-FernandesBR requested a review from a team as a code owner August 10, 2026 14:55
@github-actions

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.80%. Comparing base (e0ff281) to head (c1bd678).
⚠️ Report is 41 commits behind head on develop.

Files with missing lines Patch % Lines
...ocketpy/environment/fetchers/open_meteo_fetcher.py 85.24% 9 Missing ⚠️
rocketpy/environment/environment.py 99.29% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1119      +/-   ##
===========================================
+ Coverage    82.18%   82.80%   +0.62%     
===========================================
  Files          122      129       +7     
  Lines        16355    16812     +457     
===========================================
+ Hits         13441    13922     +481     
+ Misses        2914     2890      -24     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The CI lint job runs pylint, not just ruff, and flagged the new code:

- process_open_meteo_atmosphere and process_open_meteo_ensemble exceeded the
  statement limit. Rather than suppress it, the profile-storing and
  member-stacking blocks were extracted into helpers, mirroring the existing
  _store_meteomatics_* pattern. The two processors now read as a sequence of
  named steps.
- set_atmospheric_model exceeded the branch limit, since the two new model
  cases added to an already long match. The self-contained
  pressure_conversion_factor validation moved to a private validator next to
  the other validators, which also flattens its nested ifs.
- Unused-argument and missing-docstring warnings in the new tests, from fakes
  that deliberately accept the real signature.

Behaviour is unchanged: the pressure_conversion_factor error messages and the
Open-Meteo profiles were re-verified against the original, and the full unit
suite still passes (1954 passed, 16 skipped).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant