11import json
22import os
3+ from datetime import datetime
34
45import numpy as np
6+ import numpy .testing as npt
57import pytest
68import pytz
79
1012 find_longitude_index ,
1113 geodesic_to_lambert_conformal ,
1214 geodesic_to_utm ,
15+ get_final_date_from_time_array ,
16+ get_initial_date_from_time_array ,
1317 utm_to_geodesic ,
1418)
1519from rocketpy .environment .weather_model_mapping import WeatherModelMapping
@@ -24,6 +28,27 @@ class DummyLambertProjection:
2428 earth_radius = 6371229.0
2529
2630
31+ @pytest .mark .parametrize (
32+ "date_helper" , [get_initial_date_from_time_array , get_final_date_from_time_array ]
33+ )
34+ def test_time_array_date_helpers_convert_cftime_dates (
35+ monkeypatch , date_helper , dummy_time_array , dummy_cftime_date
36+ ):
37+ """Convert NetCDF/cftime date objects to JSON-serializable datetimes."""
38+
39+ # Arrange
40+ def fake_num2date (* _args , ** _kwargs ):
41+ return dummy_cftime_date
42+
43+ monkeypatch .setattr ("rocketpy.environment.tools.netCDF4.num2date" , fake_num2date )
44+
45+ # Act
46+ converted_date = date_helper (dummy_time_array )
47+
48+ # Assert
49+ assert converted_date == datetime (2023 , 6 , 24 , 9 , 30 , 15 , 123456 )
50+
51+
2752@pytest .mark .parametrize (
2853 "latitude, longitude" , [(- 21.960641 , - 47.482122 ), (0 , 0 ), (21.960641 , 47.482122 )]
2954)
@@ -306,6 +331,93 @@ def test_environment_export_environment_exports_valid_environment_json(
306331 os .remove ("environment.json" )
307332
308333
334+ @pytest .mark .parametrize (
335+ "atmospheric_model_type" , ["windy" , "forecast" , "reanalysis" , "ensemble" ]
336+ )
337+ def test_environment_to_dict_from_dict_round_trip_preserves_weather_metadata (
338+ example_plain_env , atmospheric_model_type
339+ ):
340+ """Round-trip weather-model environments without losing metadata.
341+
342+ Parameters
343+ ----------
344+ example_plain_env : rocketpy.Environment
345+ Baseline environment used to build the serialized state.
346+ atmospheric_model_type : str
347+ Weather-model label stored in the serialized payload.
348+ """
349+ # Arrange
350+ env = example_plain_env
351+
352+ weather_metadata = {
353+ "atmospheric_model_type" : atmospheric_model_type ,
354+ "atmospheric_model_file" : None ,
355+ "atmospheric_model_dict" : {"time" : "time" },
356+ "atmospheric_model_init_date" : datetime (2024 , 1 , 1 , 0 ),
357+ "atmospheric_model_end_date" : datetime (2024 , 1 , 1 , 6 ),
358+ "atmospheric_model_interval" : 6 ,
359+ "atmospheric_model_init_lat" : - 10.0 ,
360+ "atmospheric_model_end_lat" : 10.0 ,
361+ "atmospheric_model_init_lon" : - 20.0 ,
362+ "atmospheric_model_end_lon" : 20.0 ,
363+ }
364+
365+ ensemble_metadata = {
366+ "level_ensemble" : None ,
367+ "height_ensemble" : None ,
368+ "temperature_ensemble" : None ,
369+ "wind_u_ensemble" : None ,
370+ "wind_v_ensemble" : None ,
371+ "wind_heading_ensemble" : None ,
372+ "wind_direction_ensemble" : None ,
373+ "wind_speed_ensemble" : None ,
374+ "num_ensemble_members" : None ,
375+ }
376+
377+ if atmospheric_model_type == "ensemble" :
378+ ensemble_metadata .update (
379+ {
380+ "level_ensemble" : np .array ([1000.0 , 900.0 ]),
381+ "height_ensemble" : np .array ([[0.0 , 1000.0 ]]),
382+ "temperature_ensemble" : np .array ([[288.15 , 281.15 ]]),
383+ "wind_u_ensemble" : np .array ([[2.0 , 3.0 ]]),
384+ "wind_v_ensemble" : np .array ([[4.0 , 5.0 ]]),
385+ "wind_heading_ensemble" : np .array ([[26.565051 , 30.963757 ]]),
386+ "wind_direction_ensemble" : np .array ([[206.565051 , 210.963757 ]]),
387+ "wind_speed_ensemble" : np .array ([[4.472136 , 5.830952 ]]),
388+ "num_ensemble_members" : 1 ,
389+ }
390+ )
391+
392+ for metadata in (weather_metadata , ensemble_metadata ):
393+ for attribute , value in metadata .items ():
394+ setattr (env , attribute , value )
395+
396+ env_dict = env .to_dict ()
397+
398+ # The serialized payload should be self-contained and not depend on files.
399+ assert "atmospheric_model_file" not in env_dict
400+ assert "atmospheric_model_dict" not in env_dict
401+
402+ # Act
403+ restored_env = Environment .from_dict (env_dict )
404+
405+ # Assert
406+ assert restored_env .atmospheric_model_type == atmospheric_model_type
407+ assert restored_env .atmospheric_model_init_date == env .atmospheric_model_init_date
408+ assert restored_env .atmospheric_model_end_date == env .atmospheric_model_end_date
409+ assert restored_env .atmospheric_model_interval == env .atmospheric_model_interval
410+ assert restored_env .atmospheric_model_init_lat == env .atmospheric_model_init_lat
411+ assert restored_env .atmospheric_model_end_lat == env .atmospheric_model_end_lat
412+ assert restored_env .atmospheric_model_init_lon == env .atmospheric_model_init_lon
413+ assert restored_env .atmospheric_model_end_lon == env .atmospheric_model_end_lon
414+
415+ if atmospheric_model_type == "ensemble" :
416+ npt .assert_allclose (restored_env .level_ensemble , env .level_ensemble )
417+ npt .assert_allclose (restored_env .height_ensemble , env .height_ensemble )
418+ assert restored_env .num_ensemble_members == env .num_ensemble_members
419+
420+
309421class _DummyDataset :
310422 """Small test double that mimics a netCDF dataset variables mapping."""
311423
0 commit comments