diff --git a/swvo/io/solar_wind/read_solar_wind_from_multiple_models.py b/swvo/io/solar_wind/read_solar_wind_from_multiple_models.py index 9529384..24c9a9f 100644 --- a/swvo/io/solar_wind/read_solar_wind_from_multiple_models.py +++ b/swvo/io/solar_wind/read_solar_wind_from_multiple_models.py @@ -9,6 +9,7 @@ import logging from collections.abc import Sequence from datetime import datetime, timedelta, timezone +from pathlib import Path from typing import Literal, Optional import numpy as np @@ -96,7 +97,7 @@ def read_solar_wind_from_multiple_models( # noqa: PLR0913 if model_order is None: model_order = [SWOMNI(), DSCOVR(), SWACE(), SWSWIFTEnsemble()] - logger.warning("No model order specified, using default order: OMNI, ACE, SWIFT ensemble") + logger.warning("No model order specified, using default order: SWOMNI, DSCOVR, SWACE, SWSWIFTEnsemble") data_out = [pd.DataFrame()] swift_data_available = True @@ -104,18 +105,43 @@ def read_solar_wind_from_multiple_models( # noqa: PLR0913 for model in model_order: if not isinstance(model, SWModel): raise ModelError(f"Unknown or incompatible model: {type(model).__name__}") - data_one_model = _read_from_model( - model, - start_time, - end_time, - historical_data_cutoff_time, - reduce_ensemble, # ty: ignore[invalid-argument-type] - download=download, - do_interpolation=do_interpolation, - ) + active_model = model + try: + data_one_model = _read_from_model( + model, + start_time, + end_time, + historical_data_cutoff_time, + reduce_ensemble, # ty: ignore[invalid-argument-type] + download=download, + do_interpolation=do_interpolation, + ) + except ValueError as e: + if not isinstance(model, DSCOVR): + raise + + logger.warning(f"Failed to read DSCOVR data because: {e}. Falling back to ACE.") + # switch to SWACE if SWACE is already in the model_order, otherwise create a new instance of SWACE with "./data" as the data directory + # also log this fallback action + active_model = next((m for m in model_order if isinstance(m, SWACE)), None) + + if active_model is not None: + logger.info("Falling back to SWACE model in the model order.") + else: + active_model = SWACE(Path("./data")) + logger.info("Falling back to a new instance of SWACE model with default data directory './data'.") + data_one_model = _read_from_model( + active_model, + start_time, + end_time, + historical_data_cutoff_time, + reduce_ensemble, # ty: ignore[invalid-argument-type] + download=download, + do_interpolation=do_interpolation, + ) # Check if SWIFT ensemble returned empty data - if isinstance(model, SWSWIFTEnsemble): + if isinstance(active_model, SWSWIFTEnsemble): if (isinstance(data_one_model, list) and len(data_one_model) == 0) or ( isinstance(data_one_model, pd.DataFrame) and data_one_model.empty ): @@ -140,7 +166,7 @@ def read_solar_wind_from_multiple_models( # noqa: PLR0913 swift_data_available = False logger.info("SWIFT ensemble data contains only NaN values for future dates") - data_out = construct_updated_data_frame(data_out, data_one_model, model.LABEL) + data_out = construct_updated_data_frame(data_out, data_one_model, active_model.LABEL) if not any_nans(data_out): break diff --git a/tests/io/solar_wind/test_read_solar_wind_from_multiple_models.py b/tests/io/solar_wind/test_read_solar_wind_from_multiple_models.py index a92cd6d..d05a31f 100644 --- a/tests/io/solar_wind/test_read_solar_wind_from_multiple_models.py +++ b/tests/io/solar_wind/test_read_solar_wind_from_multiple_models.py @@ -174,6 +174,65 @@ def test_data_consistency(self, sample_times): for d1, d2 in zip(data1, data2): pd.testing.assert_frame_equal(d1, d2) + def test_dscovr_value_error_falls_back_to_ace(self, monkeypatch): + start_time = datetime(2026, 6, 29, 23, 58, tzinfo=timezone.utc) + end_time = datetime(2026, 6, 30, 0, 1, tzinfo=timezone.utc) + index = pd.date_range(start_time, end_time, freq="1min", tz="UTC") + ace_data = pd.DataFrame( + { + "speed": [400.0] * len(index), + "proton_density": [5.0] * len(index), + "bavg": [7.0] * len(index), + "temperature": [100000.0] * len(index), + "bx_gsm": [1.0] * len(index), + "by_gsm": [2.0] * len(index), + "bz_gsm": [-1.0] * len(index), + "file_name": ["ace_file"] * len(index), + }, + index=index, + ) + ace_calls = [] + + def raise_dscovr_value_error(self, *args, **kwargs): + raise ValueError("DSCOVR data is only available until 2026-06-29 23:59:59 UTC.") + + def read_ace(self, read_start, read_end, *, download=False, propagation=False): + ace_calls.append( + { + "model": self, + "start_time": read_start, + "end_time": read_end, + "download": download, + "propagation": propagation, + } + ) + return ace_data + + monkeypatch.setattr(DSCOVR, "read", raise_dscovr_value_error) + monkeypatch.setattr(SWACE, "read", read_ace) + + ace_model = SWACE() + data = read_solar_wind_from_multiple_models( + start_time=start_time, + end_time=end_time, + model_order=[DSCOVR(), ace_model], + historical_data_cutoff_time=end_time, + download=True, + ) + + assert len(ace_calls) == 1 + assert ace_calls[0] == { + "model": ace_model, + "start_time": start_time, + "end_time": end_time, + "download": True, + "propagation": True, + } + assert isinstance(data, pd.DataFrame) + assert (data["model"] == "ace").all() + assert (data["file_name"] == "ace_file").all() + assert data.loc[start_time, "speed"] == 400.0 + def test_model_check_with_wrong_class(self, sample_times): class FakeModel: pass