Skip to content

Commit 85b2a92

Browse files
MNT: simplify Meteomatics fetcher and profile assembly
Follow-up cleanups from the code review of the Meteomatics support: - Collapse the duplicated request/error ladder of the login and data endpoints into a single `_meteomatics_request_json` helper. - Have `_build_meteomatics_parameters` return a {parameter: (profile, height)} mapping so the response parser no longer regex-parses back the strings this module just built. Drops the regex constant and the variable-to-profile table. - Generalize `to_profile_array` to several profiles at once, so the wind u/v grid intersection reuses it instead of repeating it inline. - Reuse the existing `__validate_datetime` helper for the launch-date check, and simplify the model default to `model or "mix"`. - Normalize `atmospheric_model_type` once in `from_dict`. The type is stored as the user spelled it, so the previously case-sensitive `match` and `== "ensemble"` branches silently dropped the ensemble arrays for an Environment built with `type="Ensemble"`. - Trim the elevation warning and raise it before the API call, so it is shown even when the request later fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dcedbf6 commit 85b2a92

3 files changed

Lines changed: 140 additions & 139 deletions

File tree

rocketpy/environment/environment.py

Lines changed: 42 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,7 +1811,7 @@ def process_meteomatics_atmosphere( # pylint: disable=too-many-statements
18111811
If credentials are missing, if no launch date is set, or if the API
18121812
returns no usable data.
18131813
"""
1814-
model = model if isinstance(model, str) else "mix"
1814+
model = model or "mix"
18151815
username = username or os.environ.get("METEOMATICS_USERNAME")
18161816
password = password or os.environ.get("METEOMATICS_PASSWORD")
18171817
if not username or not password:
@@ -1821,10 +1821,16 @@ def process_meteomatics_atmosphere( # pylint: disable=too-many-statements
18211821
"or set the METEOMATICS_USERNAME and METEOMATICS_PASSWORD "
18221822
"environment variables."
18231823
)
1824-
if getattr(self, "datetime_date", None) is None:
1825-
raise ValueError(
1826-
"A launch date is required to use the Meteomatics atmospheric "
1827-
"model. Provide it when creating the Environment or via set_date()."
1824+
self.__validate_datetime()
1825+
1826+
if self.elevation == 0:
1827+
warnings.warn(
1828+
"The Environment elevation is 0 m (possibly unset), so Meteomatics "
1829+
"heights above ground level are being treated as heights above sea "
1830+
"level. Set the elevation before this call if the launch site is "
1831+
"not at sea level.",
1832+
UserWarning,
1833+
stacklevel=2,
18281834
)
18291835

18301836
profiles = fetch_atmospheric_data_from_meteomatics(
@@ -1841,39 +1847,34 @@ def process_meteomatics_atmosphere( # pylint: disable=too-many-statements
18411847
query_limit=query_limit,
18421848
)
18431849

1844-
if self.elevation == 0:
1845-
warnings.warn(
1846-
"The Environment elevation is 0 m (possibly unset), so "
1847-
"Meteomatics heights above ground level are being treated as "
1848-
"heights above sea level. If the launch site is not at sea "
1849-
"level, set the elevation (e.g. Environment(elevation=...) or "
1850-
"set_elevation('Open-Elevation')) before this call for an "
1851-
"accurate profile.",
1852-
UserWarning,
1853-
stacklevel=2,
1850+
def to_profile_array(*names):
1851+
"""Convert {AGL height: value} mappings into a sorted array whose
1852+
first column is the ASL height and the remaining ones the requested
1853+
profile values, keeping only the heights that carry a value in
1854+
every mapping."""
1855+
common_heights = set.intersection(*(set(profiles[n]) for n in names))
1856+
heights = sorted(
1857+
h
1858+
for h in common_heights
1859+
if all(profiles[n][h] is not None for n in names)
18541860
)
1855-
1856-
def to_profile_array(profile):
1857-
"""Convert an {AGL height: value} mapping into a sorted
1858-
(ASL height, value) array, dropping any missing value."""
1859-
heights = sorted(h for h, value in profile.items() if value is not None)
18601861
return np.array(
1861-
[(h + self.elevation, profile[h]) for h in heights], dtype=float
1862+
[
1863+
(h + self.elevation, *(profiles[n][h] for n in names))
1864+
for h in heights
1865+
],
1866+
dtype=float,
18621867
)
18631868

1864-
pressure_array = to_profile_array(profiles["pressure"])
1865-
temperature_array = to_profile_array(profiles["temperature"])
1866-
1869+
pressure_array = to_profile_array("pressure")
1870+
temperature_array = to_profile_array("temperature")
18671871
# Wind u and v share the same altitude grid; keep only common levels.
1868-
wind_heights = sorted(
1869-
h
1870-
for h in set(profiles["wind_u"]) & set(profiles["wind_v"])
1871-
if profiles["wind_u"][h] is not None and profiles["wind_v"][h] is not None
1872-
)
1872+
wind_array = to_profile_array("wind_u", "wind_v")
1873+
18731874
# Each profile needs at least two levels: a single-point Function cannot
18741875
# be evaluated at its own node (it raises IndexError downstream), so a
18751876
# collapsed grid must fail here with an actionable message instead.
1876-
if min(len(pressure_array), len(temperature_array), len(wind_heights)) < 2:
1877+
if min(len(pressure_array), len(temperature_array), len(wind_array)) < 2:
18771878
raise ValueError(
18781879
"Meteomatics did not return enough usable atmospheric data: at "
18791880
"least two valid altitude levels are required for pressure, "
@@ -1883,11 +1884,11 @@ def to_profile_array(profile):
18831884
"and your account permissions."
18841885
)
18851886

1886-
wind_u_values = np.array([profiles["wind_u"][h] for h in wind_heights])
1887-
wind_v_values = np.array([profiles["wind_v"][h] for h in wind_heights])
1888-
wind_asl_heights = np.array(wind_heights, dtype=float) + self.elevation
1889-
wind_u_array = np.column_stack((wind_asl_heights, wind_u_values))
1890-
wind_v_array = np.column_stack((wind_asl_heights, wind_v_values))
1887+
wind_asl_heights = wind_array[:, 0]
1888+
wind_u_values = wind_array[:, 1]
1889+
wind_v_values = wind_array[:, 2]
1890+
wind_u_array = wind_array[:, (0, 1)]
1891+
wind_v_array = wind_array[:, (0, 2)]
18911892

18921893
wind_speed_array = calculate_wind_speed(wind_u_values, wind_v_values)
18931894
wind_heading_array = calculate_wind_heading(wind_u_values, wind_v_values)
@@ -3181,8 +3182,11 @@ def from_dict(cls, data): # pylint: disable=too-many-statements
31813182
)
31823183
atmospheric_model = data["atmospheric_model_type"]
31833184
env.atmospheric_model_type = atmospheric_model
3185+
# set_atmospheric_model stores the type as the user spelled it (e.g.
3186+
# "Meteomatics"), so the dispatch below must be case-insensitive.
3187+
model_type = atmospheric_model.lower()
31843188

3185-
match atmospheric_model:
3189+
match model_type:
31863190
case "standard_atmosphere":
31873191
env.set_atmospheric_model("standard_atmosphere")
31883192
case "custom_atmosphere":
@@ -3204,13 +3208,7 @@ def from_dict(cls, data): # pylint: disable=too-many-statements
32043208
env.elevation = data["elevation"]
32053209
env.max_expected_height = data["max_expected_height"]
32063210

3207-
if isinstance(atmospheric_model, str) and atmospheric_model.lower() in (
3208-
"windy",
3209-
"meteomatics",
3210-
"forecast",
3211-
"reanalysis",
3212-
"ensemble",
3213-
):
3211+
if model_type in ("windy", "meteomatics", "forecast", "reanalysis", "ensemble"):
32143212
env.atmospheric_model_init_date = data["atmospheric_model_init_date"]
32153213
env.atmospheric_model_end_date = data["atmospheric_model_end_date"]
32163214
env.atmospheric_model_interval = data["atmospheric_model_interval"]
@@ -3219,7 +3217,7 @@ def from_dict(cls, data): # pylint: disable=too-many-statements
32193217
env.atmospheric_model_init_lon = data["atmospheric_model_init_lon"]
32203218
env.atmospheric_model_end_lon = data["atmospheric_model_end_lon"]
32213219

3222-
if atmospheric_model == "ensemble":
3220+
if model_type == "ensemble":
32233221
env.level_ensemble = data["level_ensemble"]
32243222
env.height_ensemble = data["height_ensemble"]
32253223
env.temperature_ensemble = data["temperature_ensemble"]

0 commit comments

Comments
 (0)