From 04bf7643cb8bff618d13233db9ea6b9b1775ed6c Mon Sep 17 00:00:00 2001 From: monta Date: Wed, 26 Nov 2025 11:30:43 +0100 Subject: [PATCH 1/6] created new branch --- monte_carlo_test.errors.txt | 0 monte_carlo_test.inputs.txt | 0 monte_carlo_test.outputs.txt | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 monte_carlo_test.errors.txt create mode 100644 monte_carlo_test.inputs.txt create mode 100644 monte_carlo_test.outputs.txt diff --git a/monte_carlo_test.errors.txt b/monte_carlo_test.errors.txt new file mode 100644 index 000000000..e69de29bb diff --git a/monte_carlo_test.inputs.txt b/monte_carlo_test.inputs.txt new file mode 100644 index 000000000..e69de29bb diff --git a/monte_carlo_test.outputs.txt b/monte_carlo_test.outputs.txt new file mode 100644 index 000000000..e69de29bb From 1b7d2877cac1b1e14d3aecc81779b7d920b5a584 Mon Sep 17 00:00:00 2001 From: monta Date: Wed, 26 Nov 2025 18:16:26 +0100 Subject: [PATCH 2/6] ENH: Add MERRA-2 compatibility with unit conversion and documentation. --- rocketpy/environment/environment.py | 17 +++- rocketpy/environment/weather_model_mapping.py | 14 +++ rocketpy/mathutils/function.py | 2 +- tests/integration/test_environment.py | 96 +++++++++++++++++++ .../integration/test_environment_analysis.py | 1 + 5 files changed, 127 insertions(+), 3 deletions(-) diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index a0a8c4238..09de4a77e 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -588,7 +588,7 @@ def __reset_wind_direction_function(self): def __validate_dictionary(self, file, dictionary): # removed CMC until it is fixed. - available_models = ["GFS", "NAM", "RAP", "HIRESW", "GEFS", "ERA5"] + available_models = ["GFS", "NAM", "RAP", "HIRESW", "GEFS", "ERA5", "MERRA2"] if isinstance(dictionary, str): dictionary = self.__weather_model_map.get(dictionary) elif file in available_models: @@ -1874,10 +1874,23 @@ def process_forecast_reanalysis(self, file, dictionary): # pylint: disable=too- self._max_expected_height = max(height[0], height[-1]) # Get elevation data from file - if dictionary["surface_geopotential_height"] is not None: + if dictionary.get("surface_geopotential_height") is not None: self.elevation = get_elevation_data_from_dataset( dictionary, data, time_index, lat_index, lon_index, x, y, x1, x2, y1, y2 ) + # 2. If not found, try Geopotential (m^2/s^2) and convert + elif dictionary.get("surface_geopotential") is not None: + # Create a temporary dictionary to trick the helper function into reading PHIS + temp_dict = dictionary.copy() + temp_dict["surface_geopotential_height"] = dictionary[ + "surface_geopotential" + ] + + geopotential_value = get_elevation_data_from_dataset( + temp_dict, data, time_index, lat_index, lon_index, x, y, x1, x2, y1, y2 + ) + # Perform the conversion: Height = Geopotential / Gravity + self.elevation = geopotential_value / self.standard_g # Compute info data self.atmospheric_model_init_date = get_initial_date_from_time_array(time_array) diff --git a/rocketpy/environment/weather_model_mapping.py b/rocketpy/environment/weather_model_mapping.py index 59c6e2fd9..21b97f46c 100644 --- a/rocketpy/environment/weather_model_mapping.py +++ b/rocketpy/environment/weather_model_mapping.py @@ -112,6 +112,19 @@ class WeatherModelMapping: "u_wind": "ugrdprs", "v_wind": "vgrdprs", } + MERRA2 = { + "time": "time", + "latitude": "lat", + "longitude": "lon", + "level": "lev", + "temperature": "T", + "surface_geopotential_height": None, # + "surface_geopotential": "PHIS", # special key for Geopotential (m^2/s^2) + "geopotential_height": "H", + "geopotential": None, + "u_wind": "U", + "v_wind": "V", + } def __init__(self): """Initialize the class, creates a dictionary with all the weather models @@ -127,6 +140,7 @@ def __init__(self): "CMC": self.CMC, "GEFS": self.GEFS, "HIRESW": self.HIRESW, + "MERRA2": self.MERRA2, } def get(self, model): diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index e60b39286..221974ca8 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -10,10 +10,10 @@ from bisect import bisect_left from collections.abc import Iterable from copy import deepcopy +from enum import Enum from functools import cached_property from inspect import signature from pathlib import Path -from enum import Enum import matplotlib.pyplot as plt import numpy as np diff --git a/tests/integration/test_environment.py b/tests/integration/test_environment.py index e4c6b07f5..ce1ba64c8 100644 --- a/tests/integration/test_environment.py +++ b/tests/integration/test_environment.py @@ -2,8 +2,12 @@ from datetime import date, datetime, timezone from unittest.mock import patch +import netCDF4 +import numpy as np import pytest +from rocketpy import Environment + @pytest.mark.parametrize( "lat, lon, theoretical_elevation", @@ -258,3 +262,95 @@ def test_cmc_atmosphere(mock_show, example_spaceport_env): # pylint: disable=un """ example_spaceport_env.set_atmospheric_model(type="Ensemble", file="CMC") assert example_spaceport_env.all_info() is None + + +@pytest.fixture +def merra2_file_path(tmp_path): # pylint: disable=too-many-statements + """ + Generates a temporary NetCDF file that STRICTLY mimics the structure of a + NASA MERRA-2 'inst3_3d_asm_Np' file (Assimilated Meteorological Fields). + """ + file_path = tmp_path / "MERRA2_300.inst3_3d_asm_Np.20230620.nc4" + + with netCDF4.Dataset(file_path, "w", format="NETCDF4") as nc: + # Define Dimensions + nc.createDimension("lon", 5) + nc.createDimension("lat", 5) + nc.createDimension("lev", 5) + nc.createDimension("time", None) + + # Define Coordinates + lon = nc.createVariable("lon", "f8", ("lon",)) + lon.units = "degrees_east" + lon[:] = np.linspace(-180, 180, 5) + + lat = nc.createVariable("lat", "f8", ("lat",)) + lat.units = "degrees_north" + lat[:] = np.linspace(-90, 90, 5) + + lev = nc.createVariable("lev", "f8", ("lev",)) + lev.units = "hPa" + lev[:] = np.linspace(1000, 100, 5) + + time = nc.createVariable("time", "i4", ("time",)) + time.units = "minutes since 2023-06-20 00:00:00" + time[:] = [720] + + # Define Data Variables + # Note: Python variables are snake_case (t_var), + # but NetCDF names are uppercase ("T") to match NASA specs. + + t_var = nc.createVariable("T", "f4", ("time", "lev", "lat", "lon")) + t_var.units = "K" + t_var[:] = np.full((1, 5, 5, 5), 300.0) + + u_var = nc.createVariable("U", "f4", ("time", "lev", "lat", "lon")) + u_var.units = "m s-1" + u_var[:] = np.full((1, 5, 5, 5), 10.0) + + v_var = nc.createVariable("V", "f4", ("time", "lev", "lat", "lon")) + v_var.units = "m s-1" + v_var[:] = np.full((1, 5, 5, 5), 5.0) + + h_var = nc.createVariable("H", "f4", ("time", "lev", "lat", "lon")) + h_var.units = "m" + h_var[:] = np.linspace(0, 10000, 5).reshape(1, 5, 1, 1) * np.ones((1, 5, 5, 5)) + + # PHIS: Surface Geopotential Height [m2 s-2] + # Fixed: Variable name is now 'phis' (lowercase) to satisfy Pylint + phis = nc.createVariable("PHIS", "f4", ("time", "lat", "lon")) + phis.units = "m2 s-2" + + # We set PHIS to 9806.65 (Energy). + # We expect the code to divide by ~9.8 and get ~1000.0 (Height). + phis[:] = np.full((1, 5, 5), 9806.65) + + return str(file_path) + + +def test_merra2_full_specification_compliance(merra2_file_path): + """ + Tests that RocketPy loads a file complying with NASA BOSILOVICH785 specs. + """ + # 1. Initialize Environment + env = Environment(date=(2023, 6, 20, 12), latitude=0, longitude=0) + + # 2. Force standard gravity to a known constant for precise math checking + env.standard_g = 9.80665 + + # 3. Load the Atmospheric Model (Using the file generated above) + env.set_atmospheric_model( + type="Reanalysis", file=merra2_file_path, dictionary="MERRA2" + ) + + # 4. Verify Unit Conversion (Energy -> Height) + # Input: 9806.65 m2/s2 + # Expected: 1000.0 m + print(f"Calculated Elevation: {env.elevation} m") + assert abs(env.elevation - 1000.0) < 0.01, ( + f"Failed to convert PHIS (m2/s2) to meters. Got {env.elevation}, expected 1000.0" + ) + + # 5. Verify Variable Mapping + assert env.temperature(0) == 300.0 + assert env.wind_speed(0) > 0 diff --git a/tests/integration/test_environment_analysis.py b/tests/integration/test_environment_analysis.py index e6043c85a..2b12a2057 100644 --- a/tests/integration/test_environment_analysis.py +++ b/tests/integration/test_environment_analysis.py @@ -4,6 +4,7 @@ import matplotlib as plt import pytest + from rocketpy import Environment plt.rcParams.update({"figure.max_open_warning": 0}) From f46f4ede9b44b93f4507caf6be26ad30f1f6a1bc Mon Sep 17 00:00:00 2001 From: monta Date: Wed, 26 Nov 2025 18:44:46 +0100 Subject: [PATCH 3/6] ENH: Finalize MERRA-2 support (Resolve conflicts, Update Docs) --- CHANGELOG.md | 1 + .../environment/1-atm-models/reanalysis.rst | 19 +++++++++++++++ rocketpy/environment/environment.py | 2 +- tests/integration/test_environment.py | 24 +++++++++++-------- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28586a1b7..a7aa128e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Attention: The newest changes should be on top --> ### Added +ENH: Compatibility with MERRA-2 atmosphere reanalysis files [#825](https://github.com/RocketPy-Team/RocketPy/pull/825) ### Changed diff --git a/docs/user/environment/1-atm-models/reanalysis.rst b/docs/user/environment/1-atm-models/reanalysis.rst index 0019d1e7c..f55a596f7 100644 --- a/docs/user/environment/1-atm-models/reanalysis.rst +++ b/docs/user/environment/1-atm-models/reanalysis.rst @@ -46,6 +46,25 @@ ERA5 data can be downloaded from the processed by RocketPy. It is recommended that you download only the \ necessary data. +MERRA-2 +------- + +The Modern-Era Retrospective analysis for Research and Applications, Version 2 (MERRA-2) is a NASA atmospheric reanalysis for the satellite era using the Goddard Earth Observing System, Version 5 (GEOS-5) with its Atmospheric Data Assimilation System (ADAS). + +To use MERRA-2 data in RocketPy, you generally need the **Assimilated Meteorological Fields** collection (specifically the 3D Pressure Level data, usually named ``inst3_3d_asm_Np``). + +You can load these files using the ``dictionary="MERRA2"`` argument: + +.. code-block:: python + + env.set_atmospheric_model( + type="Reanalysis", + file="MERRA2_400.inst3_3d_asm_Np.20230620.nc4", + dictionary="MERRA2" + ) + +RocketPy automatically handles the unit conversion for MERRA-2's surface geopotential (energy) to geometric height (meters). + Setting the Environment ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index 09de4a77e..71d075d57 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -1167,7 +1167,7 @@ def set_atmospheric_model( # pylint: disable=too-many-statements ``Reanalysis`` or ``Ensemble``. It specifies the dictionary to be used when reading ``netCDF`` and ``OPeNDAP`` files, allowing the correct retrieval of data. Acceptable values include ``ECMWF``, - ``NOAA`` and ``UCAR`` for default dictionaries which can generally + ``NOAA``, ``UCAR`` and ``MERRA2`` for default dictionaries which can generally be used to read datasets from these institutes. Alternatively, a dictionary structure can also be given, specifying the short names used for time, latitude, longitude, pressure levels, temperature diff --git a/tests/integration/test_environment.py b/tests/integration/test_environment.py index ce1ba64c8..f214df33f 100644 --- a/tests/integration/test_environment.py +++ b/tests/integration/test_environment.py @@ -1,7 +1,6 @@ import time from datetime import date, datetime, timezone from unittest.mock import patch - import netCDF4 import numpy as np import pytest @@ -268,7 +267,9 @@ def test_cmc_atmosphere(mock_show, example_spaceport_env): # pylint: disable=un def merra2_file_path(tmp_path): # pylint: disable=too-many-statements """ Generates a temporary NetCDF file that STRICTLY mimics the structure of a - NASA MERRA-2 'inst3_3d_asm_Np' file (Assimilated Meteorological Fields). + NASA MERRA-2 'inst3_3d_asm_Np' file (Assimilated Meteorological Fields) + because MERRA-2 files are too large. + """ file_path = tmp_path / "MERRA2_300.inst3_3d_asm_Np.20230620.nc4" @@ -297,8 +298,7 @@ def merra2_file_path(tmp_path): # pylint: disable=too-many-statements time[:] = [720] # Define Data Variables - # Note: Python variables are snake_case (t_var), - # but NetCDF names are uppercase ("T") to match NASA specs. + # NetCDF names are uppercase ("T") to match NASA specs. t_var = nc.createVariable("T", "f4", ("time", "lev", "lat", "lon")) t_var.units = "K" @@ -317,7 +317,6 @@ def merra2_file_path(tmp_path): # pylint: disable=too-many-statements h_var[:] = np.linspace(0, 10000, 5).reshape(1, 5, 1, 1) * np.ones((1, 5, 5, 5)) # PHIS: Surface Geopotential Height [m2 s-2] - # Fixed: Variable name is now 'phis' (lowercase) to satisfy Pylint phis = nc.createVariable("PHIS", "f4", ("time", "lat", "lon")) phis.units = "m2 s-2" @@ -330,26 +329,31 @@ def merra2_file_path(tmp_path): # pylint: disable=too-many-statements def test_merra2_full_specification_compliance(merra2_file_path): """ - Tests that RocketPy loads a file complying with NASA BOSILOVICH785 specs. + Tests that RocketPy loads a file complying with NASA MERRA-2 file specs. """ # 1. Initialize Environment - env = Environment(date=(2023, 6, 20, 12), latitude=0, longitude=0) + env = Environment( + date=(2023, 6, 20, 12), + latitude=0, + longitude=0 + ) # 2. Force standard gravity to a known constant for precise math checking env.standard_g = 9.80665 # 3. Load the Atmospheric Model (Using the file generated above) env.set_atmospheric_model( - type="Reanalysis", file=merra2_file_path, dictionary="MERRA2" + type="Reanalysis", + file=merra2_file_path, + dictionary="MERRA2" ) # 4. Verify Unit Conversion (Energy -> Height) # Input: 9806.65 m2/s2 # Expected: 1000.0 m print(f"Calculated Elevation: {env.elevation} m") - assert abs(env.elevation - 1000.0) < 0.01, ( + assert abs(env.elevation - 1000.0) < 0.01, \ f"Failed to convert PHIS (m2/s2) to meters. Got {env.elevation}, expected 1000.0" - ) # 5. Verify Variable Mapping assert env.temperature(0) == 300.0 From 4a45b5ab325a5cc4759595b29e9527800537f898 Mon Sep 17 00:00:00 2001 From: monta Date: Wed, 26 Nov 2025 19:51:00 +0100 Subject: [PATCH 4/6] Style: Fix ruff formatting errors --- rocketpy/environment/weather_model_mapping.py | 2 +- tests/integration/environment/test_environment.py | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/rocketpy/environment/weather_model_mapping.py b/rocketpy/environment/weather_model_mapping.py index 957694fee..75089f577 100644 --- a/rocketpy/environment/weather_model_mapping.py +++ b/rocketpy/environment/weather_model_mapping.py @@ -120,7 +120,7 @@ class WeatherModelMapping: "longitude": "lon", "level": "lev", "temperature": "T", - "surface_geopotential_height": None, + "surface_geopotential_height": None, "surface_geopotential": "PHIS", # special key for Geopotential (m^2/s^2) "geopotential_height": "H", "geopotential": None, diff --git a/tests/integration/environment/test_environment.py b/tests/integration/environment/test_environment.py index f214df33f..7f737d7cf 100644 --- a/tests/integration/environment/test_environment.py +++ b/tests/integration/environment/test_environment.py @@ -332,28 +332,23 @@ def test_merra2_full_specification_compliance(merra2_file_path): Tests that RocketPy loads a file complying with NASA MERRA-2 file specs. """ # 1. Initialize Environment - env = Environment( - date=(2023, 6, 20, 12), - latitude=0, - longitude=0 - ) + env = Environment(date=(2023, 6, 20, 12), latitude=0, longitude=0) # 2. Force standard gravity to a known constant for precise math checking env.standard_g = 9.80665 # 3. Load the Atmospheric Model (Using the file generated above) env.set_atmospheric_model( - type="Reanalysis", - file=merra2_file_path, - dictionary="MERRA2" + type="Reanalysis", file=merra2_file_path, dictionary="MERRA2" ) # 4. Verify Unit Conversion (Energy -> Height) # Input: 9806.65 m2/s2 # Expected: 1000.0 m print(f"Calculated Elevation: {env.elevation} m") - assert abs(env.elevation - 1000.0) < 0.01, \ + assert abs(env.elevation - 1000.0) < 0.01, ( f"Failed to convert PHIS (m2/s2) to meters. Got {env.elevation}, expected 1000.0" + ) # 5. Verify Variable Mapping assert env.temperature(0) == 300.0 From 0b06bc7463bd64aba8288b221d025d50831c1512 Mon Sep 17 00:00:00 2001 From: monta Date: Wed, 26 Nov 2025 20:28:46 +0100 Subject: [PATCH 5/6] REV: Address all code review comments --- .../environment/1-atm-models/reanalysis.rst | 4 +- monte_carlo_test.errors.txt | 0 monte_carlo_test.inputs.txt | 0 monte_carlo_test.outputs.txt | 0 rocketpy/environment/environment.py | 7 +- tests/conftest.py | 66 ++++++++++++++++ .../environment/test_environment.py | 79 ++----------------- 7 files changed, 79 insertions(+), 77 deletions(-) delete mode 100644 monte_carlo_test.errors.txt delete mode 100644 monte_carlo_test.inputs.txt delete mode 100644 monte_carlo_test.outputs.txt diff --git a/docs/user/environment/1-atm-models/reanalysis.rst b/docs/user/environment/1-atm-models/reanalysis.rst index f55a596f7..c24bec458 100644 --- a/docs/user/environment/1-atm-models/reanalysis.rst +++ b/docs/user/environment/1-atm-models/reanalysis.rst @@ -51,7 +51,9 @@ MERRA-2 The Modern-Era Retrospective analysis for Research and Applications, Version 2 (MERRA-2) is a NASA atmospheric reanalysis for the satellite era using the Goddard Earth Observing System, Version 5 (GEOS-5) with its Atmospheric Data Assimilation System (ADAS). -To use MERRA-2 data in RocketPy, you generally need the **Assimilated Meteorological Fields** collection (specifically the 3D Pressure Level data, usually named ``inst3_3d_asm_Np``). +You can download these files from the `NASA GES DISC `_. + +To use MERRA-2 data in RocketPy, you generally need the **Assimilated Meteorological Fields** collection (specifically the 3D Pressure Level data, usually named ``inst3_3d_asm_Np``). Note that MERRA-2 files typically use the ``.nc4`` extension (NetCDF-4), which is fully supported by RocketPy. You can load these files using the ``dictionary="MERRA2"`` argument: diff --git a/monte_carlo_test.errors.txt b/monte_carlo_test.errors.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/monte_carlo_test.inputs.txt b/monte_carlo_test.inputs.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/monte_carlo_test.outputs.txt b/monte_carlo_test.outputs.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/rocketpy/environment/environment.py b/rocketpy/environment/environment.py index e6d49daac..851ad3764 100644 --- a/rocketpy/environment/environment.py +++ b/rocketpy/environment/environment.py @@ -1899,17 +1899,14 @@ def process_forecast_reanalysis(self, file, dictionary): # pylint: disable=too- ) # 2. If not found, try Geopotential (m^2/s^2) and convert elif dictionary.get("surface_geopotential") is not None: - # Create a temporary dictionary to trick the helper function into reading PHIS temp_dict = dictionary.copy() temp_dict["surface_geopotential_height"] = dictionary[ "surface_geopotential" ] - - geopotential_value = get_elevation_data_from_dataset( + surface_geopotential_value = get_elevation_data_from_dataset( temp_dict, data, time_index, lat_index, lon_index, x, y, x1, x2, y1, y2 ) - # Perform the conversion: Height = Geopotential / Gravity - self.elevation = geopotential_value / self.standard_g + self.elevation = surface_geopotential_value / self.standard_g # Compute info data self.atmospheric_model_init_date = get_initial_date_from_time_array(time_array) diff --git a/tests/conftest.py b/tests/conftest.py index 2f6124900..eae54a88f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +import netCDF4 +import numpy as np import pytest # Pytest configuration @@ -68,3 +70,67 @@ def pytest_collection_modifyitems(config, items): for item in items: if "slow" in item.keywords: item.add_marker(skip_slow) + + +@pytest.fixture +def merra2_file_path(tmp_path): # pylint: disable=too-many-statements + """ + Generates a temporary NetCDF file that STRICTLY mimics the structure of a + NASA MERRA-2 'inst3_3d_asm_Np' file (Assimilated Meteorological Fields) + because MERRA-2 files are too large. + + """ + file_path = tmp_path / "MERRA2_300.inst3_3d_asm_Np.20230620.nc4" + + with netCDF4.Dataset(file_path, "w", format="NETCDF4") as nc: + # Define Dimensions + nc.createDimension("lon", 5) + nc.createDimension("lat", 5) + nc.createDimension("lev", 5) + nc.createDimension("time", None) + + # Define Coordinates + lon = nc.createVariable("lon", "f8", ("lon",)) + lon.units = "degrees_east" + lon[:] = np.linspace(-180, 180, 5) + + lat = nc.createVariable("lat", "f8", ("lat",)) + lat.units = "degrees_north" + lat[:] = np.linspace(-90, 90, 5) + + lev = nc.createVariable("lev", "f8", ("lev",)) + lev.units = "hPa" + lev[:] = np.linspace(1000, 100, 5) + + time = nc.createVariable("time", "i4", ("time",)) + time.units = "minutes since 2023-06-20 00:00:00" + time[:] = [720] + + # Define Data Variables + # NetCDF names are uppercase ("T") to match NASA specs. + + t_var = nc.createVariable("T", "f4", ("time", "lev", "lat", "lon")) + t_var.units = "K" + t_var[:] = np.full((1, 5, 5, 5), 300.0) + + u_var = nc.createVariable("U", "f4", ("time", "lev", "lat", "lon")) + u_var.units = "m s-1" + u_var[:] = np.full((1, 5, 5, 5), 10.0) + + v_var = nc.createVariable("V", "f4", ("time", "lev", "lat", "lon")) + v_var.units = "m s-1" + v_var[:] = np.full((1, 5, 5, 5), 5.0) + + h_var = nc.createVariable("H", "f4", ("time", "lev", "lat", "lon")) + h_var.units = "m" + h_var[:] = np.linspace(0, 10000, 5).reshape(1, 5, 1, 1) * np.ones((1, 5, 5, 5)) + + # PHIS: Surface Geopotential Height [m2 s-2] + phis = nc.createVariable("PHIS", "f4", ("time", "lat", "lon")) + phis.units = "m2 s-2" + + # We set PHIS to 9806.65 (Energy). + # We expect the code to divide by ~9.8 and get ~1000.0 (Height). + phis[:] = np.full((1, 5, 5), 9806.65) + + return str(file_path) diff --git a/tests/integration/environment/test_environment.py b/tests/integration/environment/test_environment.py index 7f737d7cf..1b09625b1 100644 --- a/tests/integration/environment/test_environment.py +++ b/tests/integration/environment/test_environment.py @@ -1,11 +1,10 @@ import time from datetime import date, datetime, timezone from unittest.mock import patch -import netCDF4 -import numpy as np + import pytest -from rocketpy import Environment + @pytest.mark.parametrize( @@ -263,76 +262,14 @@ def test_cmc_atmosphere(mock_show, example_spaceport_env): # pylint: disable=un assert example_spaceport_env.all_info() is None -@pytest.fixture -def merra2_file_path(tmp_path): # pylint: disable=too-many-statements - """ - Generates a temporary NetCDF file that STRICTLY mimics the structure of a - NASA MERRA-2 'inst3_3d_asm_Np' file (Assimilated Meteorological Fields) - because MERRA-2 files are too large. - - """ - file_path = tmp_path / "MERRA2_300.inst3_3d_asm_Np.20230620.nc4" - - with netCDF4.Dataset(file_path, "w", format="NETCDF4") as nc: - # Define Dimensions - nc.createDimension("lon", 5) - nc.createDimension("lat", 5) - nc.createDimension("lev", 5) - nc.createDimension("time", None) - - # Define Coordinates - lon = nc.createVariable("lon", "f8", ("lon",)) - lon.units = "degrees_east" - lon[:] = np.linspace(-180, 180, 5) - - lat = nc.createVariable("lat", "f8", ("lat",)) - lat.units = "degrees_north" - lat[:] = np.linspace(-90, 90, 5) - - lev = nc.createVariable("lev", "f8", ("lev",)) - lev.units = "hPa" - lev[:] = np.linspace(1000, 100, 5) - - time = nc.createVariable("time", "i4", ("time",)) - time.units = "minutes since 2023-06-20 00:00:00" - time[:] = [720] - - # Define Data Variables - # NetCDF names are uppercase ("T") to match NASA specs. - - t_var = nc.createVariable("T", "f4", ("time", "lev", "lat", "lon")) - t_var.units = "K" - t_var[:] = np.full((1, 5, 5, 5), 300.0) - - u_var = nc.createVariable("U", "f4", ("time", "lev", "lat", "lon")) - u_var.units = "m s-1" - u_var[:] = np.full((1, 5, 5, 5), 10.0) - - v_var = nc.createVariable("V", "f4", ("time", "lev", "lat", "lon")) - v_var.units = "m s-1" - v_var[:] = np.full((1, 5, 5, 5), 5.0) - - h_var = nc.createVariable("H", "f4", ("time", "lev", "lat", "lon")) - h_var.units = "m" - h_var[:] = np.linspace(0, 10000, 5).reshape(1, 5, 1, 1) * np.ones((1, 5, 5, 5)) - - # PHIS: Surface Geopotential Height [m2 s-2] - phis = nc.createVariable("PHIS", "f4", ("time", "lat", "lon")) - phis.units = "m2 s-2" - - # We set PHIS to 9806.65 (Energy). - # We expect the code to divide by ~9.8 and get ~1000.0 (Height). - phis[:] = np.full((1, 5, 5), 9806.65) - - return str(file_path) - - -def test_merra2_full_specification_compliance(merra2_file_path): +def test_merra2_full_specification_compliance(merra2_file_path, example_plain_env): """ Tests that RocketPy loads a file complying with NASA MERRA-2 file specs. """ # 1. Initialize Environment - env = Environment(date=(2023, 6, 20, 12), latitude=0, longitude=0) + env = example_plain_env + env.set_date((2023, 6, 20, 12)) + env.set_location(latitude=0, longitude=0) # 2. Force standard gravity to a known constant for precise math checking env.standard_g = 9.80665 @@ -346,10 +283,10 @@ def test_merra2_full_specification_compliance(merra2_file_path): # Input: 9806.65 m2/s2 # Expected: 1000.0 m print(f"Calculated Elevation: {env.elevation} m") - assert abs(env.elevation - 1000.0) < 0.01, ( + assert abs(env.elevation - 1000.0) < 1e-6, ( f"Failed to convert PHIS (m2/s2) to meters. Got {env.elevation}, expected 1000.0" ) # 5. Verify Variable Mapping - assert env.temperature(0) == 300.0 + assert abs(env.temperature(0) - 300.0) < 1e-6 assert env.wind_speed(0) > 0 From b2662b3ca404b5da48d0544283f812bbaa927d4c Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:23:23 -0300 Subject: [PATCH 6/6] Apply suggestion from @Gui-FernandesBR --- tests/integration/environment/test_environment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/environment/test_environment.py b/tests/integration/environment/test_environment.py index 1b09625b1..ef8f56497 100644 --- a/tests/integration/environment/test_environment.py +++ b/tests/integration/environment/test_environment.py @@ -5,8 +5,6 @@ import pytest - - @pytest.mark.parametrize( "lat, lon, theoretical_elevation", [