Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 38 additions & 12 deletions swvo/io/solar_wind/read_solar_wind_from_multiple_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -96,26 +97,51 @@ 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

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.")
Comment on lines +119 to +123
# 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'.")
Comment on lines +130 to +132
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
):
Expand All @@ -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

Expand Down
59 changes: 59 additions & 0 deletions tests/io/solar_wind/test_read_solar_wind_from_multiple_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading