Skip to content

Commit 642e307

Browse files
authored
MNT: introduce pressure unit conversion when using forecast/reanalysis/ensemble data (#955)
* MNT: remove conversion from mbar to Pa in netcdf4 fetched data * ENH: introducing pressure conversion factor when using forecast or reanalysis * fix: adding conversion factor argument to ensemble as well
1 parent 7f85c07 commit 642e307

8 files changed

Lines changed: 93 additions & 11 deletions

File tree

rocketpy/environment/environment.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,7 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
11201120
temperature=None,
11211121
wind_u=0,
11221122
wind_v=0,
1123+
pressure_conversion_factor="Pa",
11231124
):
11241125
"""Define the atmospheric model for this Environment.
11251126
@@ -1216,6 +1217,12 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
12161217
m/s). Finally, a callable or function is also accepted. The function
12171218
should take one argument, the height above sea level in meters and
12181219
return a corresponding wind-v in m/s.
1220+
pressure_conversion_factor : string, int, float
1221+
This defines the pressure conversion factor to Pa when type is
1222+
``forecast`` or ``reanalysis``. The pressure unit from the data may
1223+
not be in Pascal, so the correction is necessary. Valid strings are
1224+
("mbar", "hPa", "Pa"), or a strictly positive number if using a
1225+
custom pressure unit.
12191226
12201227
Returns
12211228
-------
@@ -1265,6 +1272,28 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
12651272
case "windy":
12661273
self.process_windy_atmosphere(file)
12671274
case "forecast" | "reanalysis" | "ensemble":
1275+
conversion_factor = 1
1276+
if not isinstance(pressure_conversion_factor, (float, int, str)):
1277+
raise ValueError(
1278+
"Argument 'pressure_conversion_factor' must be numeric or a standard pressure unit ('mbar', 'hPa', 'Pa')!"
1279+
)
1280+
if isinstance(pressure_conversion_factor, (float, int)):
1281+
if pressure_conversion_factor <= 0:
1282+
raise ValueError(
1283+
"Argument 'pressure_conversion_factor' must be strictly positive!"
1284+
)
1285+
else:
1286+
conversion_factor = pressure_conversion_factor
1287+
if isinstance(pressure_conversion_factor, str):
1288+
if pressure_conversion_factor.lower() in ("mbar", "hpa"):
1289+
conversion_factor = 100
1290+
elif pressure_conversion_factor.lower() == "pa":
1291+
conversion_factor = 1
1292+
else:
1293+
raise ValueError(
1294+
"Argument 'pressure_conversion_factor' unit must be a standard pressure unit ('mbar', 'hPa', 'Pa')!"
1295+
)
1296+
12681297
if isinstance(file, str):
12691298
shortcut_map = self.__atm_type_file_to_function_map.get(type, {})
12701299
matching_shortcut = next(
@@ -1305,9 +1334,11 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
13051334
dataset = fetch_function() if fetch_function is not None else file
13061335

13071336
if type in ["forecast", "reanalysis"]:
1308-
self.process_forecast_reanalysis(dataset, dictionary)
1337+
self.process_forecast_reanalysis(
1338+
dataset, dictionary, conversion_factor=conversion_factor
1339+
)
13091340
else:
1310-
self.process_ensemble(dataset, dictionary)
1341+
self.process_ensemble(dataset, dictionary, conversion_factor)
13111342
case _: # pragma: no cover
13121343
raise ValueError(f"Unknown model type '{type}'.")
13131344

@@ -1686,7 +1717,7 @@ def process_wyoming_sounding(self, file): # pylint: disable=too-many-statements
16861717
# Save maximum expected height
16871718
self._max_expected_height = data_array[-1, 1]
16881719

1689-
def process_forecast_reanalysis(self, file, dictionary): # pylint: disable=too-many-locals,too-many-statements
1720+
def process_forecast_reanalysis(self, file, dictionary, conversion_factor): # pylint: disable=too-many-locals,too-many-statements
16901721
"""Import and process atmospheric data from weather forecasts
16911722
and reanalysis given as ``netCDF`` or ``OPeNDAP`` files.
16921723
Sets pressure, temperature, wind-u and wind-v
@@ -1738,6 +1769,9 @@ def process_forecast_reanalysis(self, file, dictionary): # pylint: disable=too-
17381769
"u_wind": "ugrdprs",
17391770
"v_wind": "vgrdprs",
17401771
}
1772+
conversion_factor : float, int
1773+
Specifies the factor by which the pressure will be multiplied
1774+
in order to transform it to Pascal.
17411775
17421776
Returns
17431777
-------
@@ -1790,7 +1824,7 @@ def process_forecast_reanalysis(self, file, dictionary): # pylint: disable=too-
17901824
_, lat_index = find_latitude_index(target_lat, lat_array)
17911825

17921826
# Get pressure level data from file
1793-
levels = get_pressure_levels_from_file(data, dictionary)
1827+
levels = get_pressure_levels_from_file(data, dictionary, conversion_factor)
17941828

17951829
# Get geopotential data from file
17961830
try:
@@ -1991,7 +2025,7 @@ def process_forecast_reanalysis(self, file, dictionary): # pylint: disable=too-
19912025
# Close weather data
19922026
data.close()
19932027

1994-
def process_ensemble(self, file, dictionary): # pylint: disable=too-many-locals,too-many-statements
2028+
def process_ensemble(self, file, dictionary, conversion_factor): # pylint: disable=too-many-locals,too-many-statements
19952029
"""Import and process atmospheric data from weather ensembles
19962030
given as ``netCDF`` or ``OPeNDAP`` files. Sets pressure, temperature,
19972031
wind-u and wind-v profiles and surface elevation obtained from a weather
@@ -2042,6 +2076,9 @@ def process_ensemble(self, file, dictionary): # pylint: disable=too-many-locals
20422076
"u_wind": "ugrdprs",
20432077
"v_wind": "vgrdprs",
20442078
}
2079+
conversion_factor : float, int
2080+
Specifies the factor by which the pressure will be multiplied
2081+
in order to transform it to Pascal.
20452082
20462083
See also
20472084
--------
@@ -2106,7 +2143,7 @@ class for some dictionary examples.
21062143
num_members = 1
21072144

21082145
# Get pressure level data from file
2109-
levels = get_pressure_levels_from_file(data, dictionary)
2146+
levels = get_pressure_levels_from_file(data, dictionary, conversion_factor)
21102147

21112148
inverse_dictionary = {v: k for k, v in dictionary.items()}
21122149
param_dictionary = {

rocketpy/environment/tools.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def geodesic_to_lambert_conformal(lat, lon, projection_variable, x_units="m"):
169169
## These functions are meant to be used with netcdf4 datasets
170170

171171

172-
def get_pressure_levels_from_file(data, dictionary):
172+
def get_pressure_levels_from_file(data, dictionary, conversion_factor):
173173
"""Extracts pressure levels from a netCDF4 dataset and converts them to Pa.
174174
175175
Parameters
@@ -178,6 +178,9 @@ def get_pressure_levels_from_file(data, dictionary):
178178
The netCDF4 dataset containing the pressure level data.
179179
dictionary : dict
180180
A dictionary mapping variable names to dataset keys.
181+
conversion_factor : float, int
182+
Specifies the factor by which the pressure will be multiplied
183+
in order to transform it to Pascal.
181184
182185
Returns
183186
-------
@@ -190,8 +193,7 @@ def get_pressure_levels_from_file(data, dictionary):
190193
If the pressure levels cannot be read from the file.
191194
"""
192195
try:
193-
# Convert mbar to Pa
194-
levels = 100 * data.variables[dictionary["level"]][:]
196+
levels = conversion_factor * data.variables[dictionary["level"]][:]
195197
except KeyError as e:
196198
raise ValueError(
197199
"Unable to read pressure levels from file. Check file and dictionary."

tests/acceptance/test_bella_lui_rocket.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def test_bella_lui_rocket_data_asserts_acceptance():
6767
type="Reanalysis",
6868
file="data/weather/bella_lui_weather_data_ERA5.nc",
6969
dictionary="ECMWF",
70+
pressure_conversion_factor="hPa",
7071
)
7172
env.max_expected_height = 2000
7273

tests/acceptance/test_ndrt_2020_rocket.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def test_ndrt_2020_rocket_data_asserts_acceptance(env_file):
8383
type="Reanalysis",
8484
file=env_file,
8585
dictionary="ECMWF",
86+
pressure_conversion_factor="hPa",
8687
)
8788
env.max_expected_height = 2000
8889

tests/fixtures/environment/environment_fixtures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def environment_spaceport_america_2023():
111111
type="Reanalysis",
112112
file="data/weather/spaceport_america_pressure_levels_2023_hourly.nc",
113113
dictionary="ECMWF",
114+
pressure_conversion_factor="hPa",
114115
)
115116

116117
env.max_expected_height = 6000

tests/integration/environment/test_environment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def test_era5_atmosphere(mock_show, example_spaceport_env): # pylint: disable=u
4545
type="Reanalysis",
4646
file="data/weather/SpaceportAmerica_2018_ERA-5.nc",
4747
dictionary="ECMWF",
48+
pressure_conversion_factor="hPa",
4849
)
4950
assert example_spaceport_env.all_info() is None
5051

tests/unit/environment/test_environment.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,9 +577,10 @@ def test_set_atmospheric_model_normalizes_shortcut_case_for_forecast(example_pla
577577

578578
called_arguments = {}
579579

580-
def fake_process_forecast_reanalysis(dataset, dictionary):
580+
def fake_process_forecast_reanalysis(dataset, dictionary, conversion_factor):
581581
called_arguments["dataset"] = dataset
582582
called_arguments["dictionary"] = dictionary
583+
called_arguments["conversion_factr"] = conversion_factor
583584

584585
environment.process_forecast_reanalysis = fake_process_forecast_reanalysis
585586

@@ -618,9 +619,10 @@ def test_forecast_shortcut_and_dictionary_are_case_insensitive(
618619

619620
captured = {}
620621

621-
def fake_process_forecast_reanalysis(file, dictionary):
622+
def fake_process_forecast_reanalysis(file, dictionary, conversion_factor):
622623
captured["file"] = file
623624
captured["dictionary"] = dictionary
625+
captured["conversion_factor"] = conversion_factor
624626

625627
monkeypatch.setattr(
626628
env, "process_forecast_reanalysis", fake_process_forecast_reanalysis

tests/unit/test_tools.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
import pytest
33

4+
from rocketpy import Environment
45
from rocketpy.tools import (
56
calculate_cubic_hermite_coefficients,
67
euler313_to_quaternions,
@@ -100,3 +101,39 @@ def test_tuple_handler(input_value, expected_output):
100101
def test_tuple_handler_exceptions(input_value, expected_exception):
101102
with pytest.raises(expected_exception):
102103
tuple_handler(input_value)
104+
105+
106+
@pytest.mark.parametrize("pressure_conversion_factor", ["hPa", "mbar", "Pa", 100])
107+
def test_valid_pressure_conversion_factor(pressure_conversion_factor):
108+
env = Environment(
109+
gravity=9.81,
110+
latitude=47.213476,
111+
longitude=9.003336,
112+
date=(2020, 2, 22, 13),
113+
elevation=407,
114+
)
115+
env.set_atmospheric_model(
116+
type="Reanalysis",
117+
file="data/weather/bella_lui_weather_data_ERA5.nc",
118+
dictionary="ECMWF",
119+
pressure_conversion_factor=pressure_conversion_factor,
120+
)
121+
122+
123+
@pytest.mark.parametrize("pressure_conversion_factor", [-1, "mPa"])
124+
def test_invalid_pressure_conversion_factor(pressure_conversion_factor):
125+
env = Environment(
126+
gravity=9.81,
127+
latitude=47.213476,
128+
longitude=9.003336,
129+
date=(2020, 2, 22, 13),
130+
elevation=407,
131+
)
132+
133+
with pytest.raises(ValueError):
134+
env.set_atmospheric_model(
135+
type="Reanalysis",
136+
file="data/weather/bella_lui_weather_data_ERA5.nc",
137+
dictionary="ECMWF",
138+
pressure_conversion_factor=pressure_conversion_factor,
139+
)

0 commit comments

Comments
 (0)