diff --git a/swvo/io/solar_wind/midl.py b/swvo/io/solar_wind/midl.py index 318d927..57f608b 100644 --- a/swvo/io/solar_wind/midl.py +++ b/swvo/io/solar_wind/midl.py @@ -173,7 +173,7 @@ def download_and_process( start_time: datetime, end_time: datetime, *, - target: float | int | str = "L1", + target: float | int | str = 14, method: Literal["ballistic", "mhd"] = "ballistic", ) -> None: """ @@ -190,9 +190,10 @@ def download_and_process( end_time : datetime End time of the data to download. Must be timezone-aware. target : float | int | str, optional - Location at which MIDL returns the solar wind: ``"L1"`` (default) for unpropagated L1 - observations, or a distance in Earth radii along the Sun-Earth line for data MIDL has - already propagated. + Location at which MIDL returns the solar wind: a distance in Earth radii along the + Sun-Earth line for data MIDL has already propagated (default 14, the distance used in + the MIDL paper for comparison against OMNI), or ``"L1"`` for unpropagated L1 + observations. method : Literal["ballistic", "mhd"], optional MIDL propagation method, either ``"ballistic"`` (default) or ``"mhd"``. Only relevant if `target` is not ``"L1"``. MHD targets must be integer Earth radii in ``[-70, 70]``. @@ -383,7 +384,7 @@ def read( download: bool = False, propagation: bool = False, *, - target: float | int | str = "L1", + target: float | int | str = 14, method: Literal["ballistic", "mhd"] = "ballistic", ) -> pd.DataFrame: """ @@ -401,9 +402,10 @@ def read( Propagate the data from L1 to near-Earth, defaults to False. Ignored if `target` is not ``"L1"``, since MIDL has then already propagated the data. target : float | int | str, optional - Location at which MIDL returns the solar wind: ``"L1"`` (default) for unpropagated L1 - observations, or a distance in Earth radii along the Sun-Earth line for data MIDL has - already propagated. + Location at which MIDL returns the solar wind: a distance in Earth radii along the + Sun-Earth line for data MIDL has already propagated (default 14, the distance used in + the MIDL paper for comparison against OMNI), or ``"L1"`` for unpropagated L1 + observations. method : Literal["ballistic", "mhd"], optional MIDL propagation method, either ``"ballistic"`` (default) or ``"mhd"``. Only relevant if `target` is not ``"L1"``. MHD targets must be integer Earth radii in ``[-70, 70]``. @@ -433,7 +435,7 @@ def read( if propagation and target != "L1": logger.warning( - f"Ignoring `propagation` because MIDL data is already propagated to {target_token}", + f"MIDL data is already propagated with target={target_token}, `propagation` is not needed and is ignored", ) propagation = False 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 c090ffe..fadfe40 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 @@ -382,8 +382,12 @@ def _read_historical_model( """ logger.info(f"Reading {model.LABEL} from {start_time} to {end_time}") + # OMNI is already propagated, so no L1 propagation is applied. if isinstance(model, SWOMNI): data_one_model = model.read(start_time, end_time, download=download) + # MIDL is requested already propagated to 14 Re (comparable to OMNI), so `propagation` is not needed. + elif isinstance(model, SWMIDL): + data_one_model = model.read(start_time, end_time, download=download, target=14) else: data_one_model = model.read(start_time, end_time, download=download, propagation=True) diff --git a/tests/io/solar_wind/test_midl.py b/tests/io/solar_wind/test_midl.py index c31a73c..93fdb3a 100644 --- a/tests/io/solar_wind/test_midl.py +++ b/tests/io/solar_wind/test_midl.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: Apache-2.0 +import inspect import os import shutil from datetime import datetime, timezone @@ -90,8 +91,7 @@ def test_initialization_without_env_var(self): @pytest.mark.parametrize( "target,method,token", [ - ("L1", "ballistic", "L1"), - ("l1", "ballistic", "L1"), + (14, "ballistic", "14Re"), (32, "ballistic", "32Re"), (20.25, "ballistic", "20p25Re"), (30, "mhd", "mhd_030Re"), @@ -144,12 +144,12 @@ def test_get_processed_file_list(self, midl_instance): start_time = datetime(2020, 1, 1, tzinfo=timezone.utc) end_time = datetime(2020, 12, 31, tzinfo=timezone.utc) - file_paths, time_intervals = midl_instance._get_processed_file_list(start_time, end_time, "L1") + file_paths, time_intervals = midl_instance._get_processed_file_list(start_time, end_time, "14Re") assert len(file_paths) == 366 assert all(str(path).startswith(str(DATA_DIR)) for path in file_paths) - assert all(path.name.startswith("MIDL_SW_L1_") for path in file_paths) - assert file_paths[0] == DATA_DIR / "2020/01" / "MIDL_SW_L1_20200101.csv" + assert all(path.name.startswith("MIDL_SW_14Re_") for path in file_paths) + assert file_paths[0] == DATA_DIR / "2020/01" / "MIDL_SW_14Re_20200101.csv" assert len(time_intervals) == 366 assert all(isinstance(interval, tuple) for interval in time_intervals) @@ -157,13 +157,13 @@ def test_source_urls(self, midl_instance): urls = midl_instance._source_urls( datetime(2023, 12, 20, tzinfo=timezone.utc), datetime(2024, 2, 3, tzinfo=timezone.utc), - "L1", + "14Re", ) assert urls == [ - f"{SWMIDL.URL}/2023/12/202312_L1.csv", - f"{SWMIDL.URL}/2024/01/202401_L1.csv", - f"{SWMIDL.URL}/2024/02/202402_L1.csv", + f"{SWMIDL.URL}/2023/12/202312_14Re.csv", + f"{SWMIDL.URL}/2024/01/202401_14Re.csv", + f"{SWMIDL.URL}/2024/02/202402_14Re.csv", ] mhd_urls = midl_instance._source_urls( @@ -184,18 +184,24 @@ def test_download_and_process(self, midl_instance, sample_dataset): args, kwargs = mock_load.call_args # MIDL slices a naive index, so the UTC bounds must reach it without a timezone. - assert args == (start_time.replace(tzinfo=None), end_time.replace(tzinfo=None), "L1") + assert args == (start_time.replace(tzinfo=None), end_time.replace(tzinfo=None), 14) assert all(arg.tzinfo is None for arg in args[:2]) assert kwargs == {"method": "ballistic", "coords": "GSM", "orbital_motion": False} - expected_file = DATA_DIR / "2024/03" / "MIDL_SW_L1_20240315.csv" + expected_file = DATA_DIR / "2024/03" / "MIDL_SW_14Re_20240315.csv" assert expected_file.exists() data = pd.read_csv(expected_file) assert len(data) == 4 assert all(field in data.columns for field in EXPECTED_COLUMNS) assert not any(column in data.columns for column in ("X", "B_source", "B_interp")) - assert midl_instance.url == f"{SWMIDL.URL}/2024/03/202403_L1.csv" + assert midl_instance.url == f"{SWMIDL.URL}/2024/03/202403_14Re.csv" + + def test_default_target_is_14_re(self): + for method in (SWMIDL.download_and_process, SWMIDL.read): + defaults = inspect.signature(method).parameters + assert defaults["target"].default == 14 + assert defaults["method"].default == "ballistic" def test_download_and_process_splits_days(self, midl_instance): start_time = datetime(2024, 3, 15, 23, 58, tzinfo=timezone.utc) @@ -205,8 +211,8 @@ def test_download_and_process_splits_days(self, midl_instance): with patch("midl.load", return_value=dataset): midl_instance.download_and_process(start_time, end_time) - first_day = DATA_DIR / "2024/03" / "MIDL_SW_L1_20240315.csv" - second_day = DATA_DIR / "2024/03" / "MIDL_SW_L1_20240316.csv" + first_day = DATA_DIR / "2024/03" / "MIDL_SW_14Re_20240315.csv" + second_day = DATA_DIR / "2024/03" / "MIDL_SW_14Re_20240316.csv" assert len(pd.read_csv(first_day)) == 2 assert len(pd.read_csv(second_day)) == 2 @@ -220,7 +226,7 @@ def test_download_and_process_merges_existing(self, midl_instance): with patch("midl.load", return_value=make_dataset(second_start, 3)): midl_instance.download_and_process(second_start, second_start.replace(hour=2, minute=2)) - file_path = DATA_DIR / "2024/03" / "MIDL_SW_L1_20240315.csv" + file_path = DATA_DIR / "2024/03" / "MIDL_SW_14Re_20240315.csv" data = pd.read_csv(file_path) assert len(data) == 6 @@ -295,7 +301,7 @@ def test_read_with_existing_data(self, midl_instance): data = midl_instance.read(start_time, end_time) - file_path = DATA_DIR / "2024/03" / "MIDL_SW_L1_20240315.csv" + file_path = DATA_DIR / "2024/03" / "MIDL_SW_14Re_20240315.csv" assert isinstance(data, pd.DataFrame) assert len(data) == 1440 assert all(col in data.columns for col in EXPECTED_COLUMNS) @@ -314,27 +320,14 @@ def test_read_with_download(self, midl_instance, sample_dataset): assert mock_load.called assert data.loc[start_time, "bavg"] == pytest.approx(13.0) - def test_with_propagation(self, midl_instance): - start_time = datetime(2024, 3, 15, tzinfo=timezone.utc) - end_time = datetime(2024, 3, 16, tzinfo=timezone.utc) - - with patch("midl.load", return_value=make_dataset(start_time, 2 * 1440)): - midl_instance.download_and_process(start_time, end_time) - - data = midl_instance.read(start_time, end_time, propagation=True) - - assert isinstance(data, pd.DataFrame) - assert all(col in data.columns for col in EXPECTED_COLUMNS) - assert data.index.is_monotonic_increasing - assert any(data["file_name"] == "propagated from previous MIDL file") - def test_propagation_skipped_for_propagated_target(self, midl_instance, caplog): start_time = datetime(2024, 3, 15, tzinfo=timezone.utc) end_time = datetime(2024, 3, 16, tzinfo=timezone.utc) - data = midl_instance.read(start_time, end_time, target=32, propagation=True) + data = midl_instance.read(start_time, end_time, propagation=True) - assert "already propagated" in caplog.text + assert "already propagated with target=14Re" in caplog.text + assert "`propagation` is not needed" in caplog.text # No -1 day shift was applied either, so the output starts exactly at start_time # instead of the day before, and carries no propagation provenance. assert data.index[0] == start_time 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 11b1d43..1640f7c 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 @@ -372,17 +372,15 @@ def read_ace(self, read_start, read_end, *, download=False, propagation=False): def test_midl_is_read_as_a_historical_model(self, tmp_path, expected_columns): """SWMIDL passed in the model order is accepted and read like the other historical - models: propagated from L1 and labelled `midl` in the `model` column.""" + models: read at the default target (14 Re), without further propagation, and labelled `midl` + in the `model` column.""" start_time = datetime(2024, 11, 22, tzinfo=timezone.utc) end_time = datetime(2024, 11, 23, tzinfo=timezone.utc) midl_model = SWMIDL(tmp_path / "midl") - # One MIDL day file per day, plus the preceding day the propagation shift reaches back to. - # The speed ramps slightly across the window: a strictly constant speed would shift every - # sample by the same amount and `sw_mag_propagation` would round half the minutes onto - # each other, leaving an alternating grid that says nothing about the model wiring. - for day in pd.date_range(start_time - timedelta(days=1), end_time, freq="D"): + # One MIDL day file per day, already propagated, so no preceding day is needed. + for day in pd.date_range(start_time, end_time, freq="D"): index = pd.date_range(day, day + timedelta(hours=24) - timedelta(minutes=1), freq="1min", tz="UTC") index.name = "t" speed = np.linspace(400.0, 401.0, len(index)) @@ -399,7 +397,7 @@ def test_midl_is_read_as_a_historical_model(self, tmp_path, expected_columns): }, index=index, ) - path = midl_model.data_dir / day.strftime("%Y/%m") / midl_model._file_name(day, "L1") + path = midl_model.data_dir / day.strftime("%Y/%m") / midl_model._file_name(day, "14Re") path.parent.mkdir(parents=True, exist_ok=True) df.to_csv(path)