From c7869df68aa108f4002ce91e57231fd23ad4da2a Mon Sep 17 00:00:00 2001 From: Sahil Jhawar Date: Thu, 9 Jul 2026 11:40:34 +0200 Subject: [PATCH] feat: update the dscovr api and restrict the end time --- swvo/io/solar_wind/dscovr.py | 206 +++++++++++++++-------------- tests/io/solar_wind/test_dscovr.py | 202 +++++++++++++++++----------- 2 files changed, 231 insertions(+), 177 deletions(-) diff --git a/swvo/io/solar_wind/dscovr.py b/swvo/io/solar_wind/dscovr.py index 8a424c1..3456256 100644 --- a/swvo/io/solar_wind/dscovr.py +++ b/swvo/io/solar_wind/dscovr.py @@ -6,6 +6,7 @@ Module for handling DSCOVR Solar Wind data. """ +import json import logging import warnings from datetime import datetime, timedelta, timezone @@ -46,48 +47,64 @@ class DSCOVR(BaseIO): ENV_VAR_NAME = "SW_DSCOVR_STREAM_DIR" - URL = "https://services.swpc.noaa.gov/products/solar-wind/" - NAME_MAG = "mag-1-day.json" - NAME_SWEPAM = "plasma-1-day.json" + URL = "https://www.ncei.noaa.gov/cloud-access/space-weather-portal/api/v1/values" + NAME_DATA = "dscovr.json" SWEPAM_FIELDS = ["speed", "proton_density", "temperature"] MAG_FIELDS = ["bx_gsm", "by_gsm", "bz_gsm", "bavg"] + MAG_PARAMETER_FIELDS = { + "bavg": "bt", + "bx_gsm": "bx_gse", + "by_gsm": "by_gse", + "bz_gsm": "bz_gse", + } + SWEPAM_PARAMETER_FIELDS = { + "speed": "proton_speed", + "proton_density": "proton_density", + "temperature": "proton_temperature", + } LABEL = "dscovr" - def download_and_process(self, request_time: datetime) -> None: + def download_and_process(self, start_time: datetime, end_time: datetime) -> None: """ Download and process DSCOVR data, splitting data across midnight into appropriate day files. Parameters ---------- - request_time : datetime - The time for which the data is requested. Must be in the past and within the last two hours. + start_time : datetime + Start time of the data to download. Must be timezone-aware. If `end_time` is not provided, this is treated + as a nowcast request time and the previous 24 hours are downloaded. + end_time : datetime | None + End time of the data to download. Must be timezone-aware. Raises ------ AssertionError - If the request_time is in the future. + If the request_time is in the future or if start_time is after end_time. FileNotFoundError If the downloaded files are empty. + ValueError + If the end_time is after 2026-06-30, since DSCOVR data is only available until that date. Returns ------- None """ - current_time = datetime.now(timezone.utc) - assert request_time < current_time, ( - f"Request time: {request_time} cannot be in the future. Current time: {current_time}!" - ) - if current_time - request_time > timedelta(hours=24): - logger.debug("We can only download DSCOVR data for the last 23 hours and a hour in past!") - return + start_time = enforce_utc_timezone(start_time) + end_time = enforce_utc_timezone(end_time) + + if end_time > datetime(2026, 6, 29, 23, 59, 59, tzinfo=timezone.utc): + raise ValueError( + "DSCOVR data is only available until 2026-06-29 23:59:59 UTC. Please choose an earlier end_time." + ) + + assert start_time < end_time, "Start time must be before end time!" temporary_dir = Path("./temp_sw_dscovr_wget") temporary_dir.mkdir(exist_ok=True, parents=True) - self._download(temporary_dir, self.NAME_MAG) - self._download(temporary_dir, self.NAME_SWEPAM) + self._download(temporary_dir, start_time, end_time) logger.debug("Processing file ...") processed_df = self._process_single_file(temporary_dir) @@ -118,21 +135,23 @@ def download_and_process(self, request_time: datetime) -> None: except Exception as e: logger.error(f"Failed to process file for {date}: {e}") - if tmp_path.exists(): - tmp_path.unlink() + # if tmp_path.exists(): + # tmp_path.unlink() continue rmtree(temporary_dir, ignore_errors=True) - def _download(self, temporary_dir: Path, file_name: str) -> None: - """Download a file from DSCOVR server. + def _download(self, temporary_dir: Path, start_time: datetime, end_time: datetime) -> None: + """Download a DSCOVR data file from the NOAA NCEI Space Weather Portal API. Parameters ---------- temporary_dir : Path Temporary directory to store the downloaded file. - file_name : str - Name of the file to download. + start_time : datetime + Start time of the data to download. + end_time : datetime + End time of the data to download. Raises ------ @@ -141,15 +160,28 @@ def _download(self, temporary_dir: Path, file_name: str) -> None: FileNotFoundError If the downloaded file is empty. """ - logger.debug(f"Downloading file {self.URL + file_name} ...") - response = requests.get(self.URL + file_name, timeout=10) + params = { + "start_time": start_time.strftime("%Y-%m-%dT%H:%M:%S"), + "end_time": end_time.strftime("%Y-%m-%dT%H:%M:%S"), + "parameters": ";".join(self._portal_parameters()), + "format": "json", + "time_format": "iso", + } + logger.debug(f"Downloading DSCOVR data from {self.URL} for {start_time} - {end_time} ...") + response = requests.get(self.URL, params=params, timeout=30) response.raise_for_status() - with open(temporary_dir / file_name, "wb") as f: + with open(temporary_dir / self.NAME_DATA, "wb") as f: f.write(response.content) - if (temporary_dir / file_name).stat().st_size == 0: - raise FileNotFoundError(f"Error while downloading file: {self.URL + file_name}!") + if (temporary_dir / self.NAME_DATA).stat().st_size == 0: + raise FileNotFoundError(f"Error while downloading file: {self.URL}!") + + def _portal_parameters(self) -> list[str]: + """Build the parameter list for the NCEI portal values API.""" + mag_parameters = [f"DSCOVR:m1m_dscovr:{field}" for field in self.MAG_PARAMETER_FIELDS.values()] + swepam_parameters = [f"DSCOVR:f1m_dscovr:{field}" for field in self.SWEPAM_PARAMETER_FIELDS.values()] + return mag_parameters + swepam_parameters def read( self, @@ -183,19 +215,15 @@ def read( Raises ------ AssertionError - Raises `AssertionError` if the start time is before the end time. + Raises `AssertionError` if the end time is before the start time. """ - if start_time > end_time: - msg = "start_time must be before end_time" - logger.error(msg) - raise ValueError(msg) - start_time = enforce_utc_timezone(start_time) end_time = enforce_utc_timezone(end_time) if propagation: - logger.info("Shiting start day by -1 day to account for propagation") + logger.info("Shifting start day by -1 day to account for propagation") start_time = start_time - timedelta(days=1) + assert start_time < end_time, "Start time must be before end time!" file_paths, _ = self._get_processed_file_list(start_time, end_time) @@ -220,14 +248,13 @@ def read( }, ) - for file_path in file_paths: - if not file_path.exists() and download: - file_date = enforce_utc_timezone(datetime.strptime(file_path.stem.split("_")[-1], "%Y%m%d")) - try: - self.download_and_process(file_date) - except AssertionError as e: - logger.error(f"`download_and_process` failed because: {e}") + if download and any(not file_path.exists() for file_path in file_paths): + try: + self.download_and_process(start_time, end_time) + except AssertionError as e: + logger.error(f"`download_and_process` failed because: {e}") + for file_path in file_paths: if not file_path.exists(): warnings.warn(f"File {file_path} not found") continue @@ -306,17 +333,38 @@ def _read_single_file(self, file_path) -> pd.DataFrame: return df def _process_single_file(self, temporary_dir: Path) -> pd.DataFrame: - """Process mag and swepam DSCOVR file to a DataFrame. + """Process combined MAG and SWEPAM DSCOVR data to a DataFrame. Returns ------- pd.DataFrame DSCOVR data. """ - data_mag = self._process_mag_file(temporary_dir) - data_swepam = self._process_swepam_file(temporary_dir) + with open(temporary_dir / self.NAME_DATA, "r") as file: + payload = json.load(file) + + status_code = payload.get("status", {}).get("code") + if status_code is not None and status_code != 200: + msg = payload.get("status", {}).get("message", f"NOAA DSCOVR API returned status code {status_code}.") + logger.error(msg) + raise FileNotFoundError(msg) - data = pd.concat([data_swepam, data_mag], axis=1) + data = pd.DataFrame(payload.get("data", {})) + + if len(data["time"]) == 0: + raise FileNotFoundError("No data found in the downloaded DSCOVR file.") + + data["t"] = pd.to_datetime(data["time"], utc=True) + data.index = data["t"] + data.drop(["time", "t"], axis=1, inplace=True) + + data.rename(columns=self._portal_column_map(), inplace=True) + expected_columns = [*self.MAG_FIELDS, *self.SWEPAM_FIELDS] + for column in expected_columns: + if column not in data.columns: + data[column] = np.nan + data = data[expected_columns].apply(pd.to_numeric, errors="coerce") + self._replace_invalid_values(data) start_time = data.index.min() end_time = data.index.max() @@ -324,65 +372,21 @@ def _process_single_file(self, temporary_dir: Path) -> pd.DataFrame: data = data.reindex(complete_range) data.index.name = "t" + data["pdyn"] = 2e-6 * data["proton_density"].values * data["speed"].values ** 2 return data - def _process_mag_file(self, temporary_dir: Path) -> pd.DataFrame: - """ - Reads magnetic instrument last available real time DSCOVR data. - - Returns - ------- - - pd.DataFrame - Dataframe with magnetic field components and timestamp sampled every minute. - """ - - data_mag = pd.read_json(temporary_dir / self.NAME_MAG) - data_mag.columns = data_mag.iloc[0] - data_mag = data_mag.iloc[1:].reset_index(drop=True) - data_mag["t"] = pd.to_datetime(data_mag["time_tag"]) - data_mag.index = data_mag["t"] - data_mag.index = enforce_utc_timezone(data_mag.index) - data_mag.drop( - ["lon_gsm", "lat_gsm", "time_tag", "t"], - axis=1, - inplace=True, - ) - - data_mag.rename(columns={"bt": "bavg"}, inplace=True) - - return data_mag - - def _process_swepam_file(self, temporary_dir: Path) -> pd.DataFrame: - """ - This method reads faraday cup SWEPAM instrument daily file from DSCOVR original data. - - - Returns - ------- - - pd.DataFrame - Dataframe with solar wind speed, proton density, temperature and timestamp, sampled every minute. - """ - - data_plasma = pd.read_json(temporary_dir / self.NAME_SWEPAM) - data_plasma.columns = data_plasma.iloc[0] - data_plasma = data_plasma.iloc[1:].reset_index(drop=True) - data_plasma["t"] = data_plasma["time_tag"] - data_plasma.index = pd.to_datetime(data_plasma["t"]) - data_plasma.index = enforce_utc_timezone(data_plasma.index) - data_plasma.drop( - ["time_tag", "t"], - axis=1, - inplace=True, - ) - - data_plasma.rename(columns={"bt": "bavg", "density": "proton_density"}, inplace=True) - data_plasma = data_plasma.astype(float) - data_plasma["pdyn"] = 2e-6 * data_plasma["proton_density"].values * data_plasma["speed"].values ** 2 - - return data_plasma + def _portal_column_map(self) -> dict[str, str]: + """Map NCEI portal response columns to SWVO DSCOVR columns.""" + mag_map = {f"m1m_dscovr.{source}": target for target, source in self.MAG_PARAMETER_FIELDS.items()} + swepam_map = {f"f1m_dscovr.{source}": target for target, source in self.SWEPAM_PARAMETER_FIELDS.items()} + return mag_map | swepam_map + + def _replace_invalid_values(self, data: pd.DataFrame) -> None: + """Replace known DSCOVR missing-value sentinels with NaN.""" + for k in [*self.MAG_FIELDS, *self.SWEPAM_FIELDS]: + mask = (data[k] < -99999.0) | (data[k] == 0.0) + data.loc[mask, k] = np.nan def _update_filename(self, row: pd.Series) -> str: """Update the filename in the row. diff --git a/tests/io/solar_wind/test_dscovr.py b/tests/io/solar_wind/test_dscovr.py index 1ab0e39..1c2cf13 100644 --- a/tests/io/solar_wind/test_dscovr.py +++ b/tests/io/solar_wind/test_dscovr.py @@ -2,21 +2,22 @@ # # SPDX-License-Identifier: Apache-2.0 +import json import os import shutil from datetime import datetime, timezone from pathlib import Path -from unittest.mock import patch +from unittest.mock import Mock, patch import numpy as np import pandas as pd import pytest -import requests from swvo.io.solar_wind import DSCOVR TEST_DIR = Path("test_data") DATA_DIR = TEST_DIR / "mock_dscovr" +EXPECTED_COLUMNS = [*DSCOVR.MAG_FIELDS, *DSCOVR.SWEPAM_FIELDS, "pdyn"] class TestDSCOVR: @@ -31,20 +32,40 @@ def setup_and_cleanup(self): shutil.rmtree(TEST_DIR, ignore_errors=True) @pytest.fixture - def swace_instance(self): + def dscovr_instance(self): with patch.dict("os.environ", {DSCOVR.ENV_VAR_NAME: str(DATA_DIR)}): instance = DSCOVR() return instance + @pytest.fixture + def sample_portal_payload(self): + return { + "status": {"code": 200}, + "data": { + "time": [ + "2024-03-15T01:00:00Z", + "2024-03-15T01:02:00Z", + "2024-03-15T01:03:00Z", + ], + "m1m_dscovr.bt": [5.5, 0.0, 7.1], + "m1m_dscovr.bx_gse": [1.1, -100000.1, 3.3], + "m1m_dscovr.by_gse": [2.2, 4.4, 5.5], + "m1m_dscovr.bz_gse": [-1.2, -2.3, -3.4], + "f1m_dscovr.proton_density": [4.2, 5.0, 0.0], + "f1m_dscovr.proton_speed": [380.0, 0.0, 400.0], + "f1m_dscovr.proton_temperature": [145000.0, 150000.0, -100000.1], + }, + } + def test_initialization_with_env_var(self): with patch.dict("os.environ", {DSCOVR.ENV_VAR_NAME: str(DATA_DIR)}): - swace = DSCOVR() - assert swace.data_dir == DATA_DIR + dscovr = DSCOVR() + assert dscovr.data_dir == DATA_DIR def test_initialization_with_explicit_path(self): explicit_path = DATA_DIR / "explicit" - swace = DSCOVR(data_dir=explicit_path) - assert swace.data_dir == explicit_path + dscovr = DSCOVR(data_dir=explicit_path) + assert dscovr.data_dir == explicit_path def test_initialization_without_env_var(self): if DSCOVR.ENV_VAR_NAME in os.environ: @@ -52,11 +73,11 @@ def test_initialization_without_env_var(self): with pytest.raises(ValueError): DSCOVR() - def test_get_processed_file_list(self, swace_instance): + def test_get_processed_file_list(self, dscovr_instance): start_time = datetime(2020, 1, 1, tzinfo=timezone.utc) end_time = datetime(2020, 12, 31, tzinfo=timezone.utc) - file_paths, time_intervals = swace_instance._get_processed_file_list(start_time, end_time) + file_paths, time_intervals = dscovr_instance._get_processed_file_list(start_time, end_time) assert len(file_paths) == 366 assert all(str(path).startswith(str(DATA_DIR)) for path in file_paths) @@ -64,66 +85,97 @@ def test_get_processed_file_list(self, swace_instance): assert len(time_intervals) == 366 assert all(isinstance(interval, tuple) for interval in time_intervals) - def test_download_and_process(self, swace_instance): - current_time = datetime.now(timezone.utc) - swace_instance.download_and_process(current_time) + def test_download_and_process(self, dscovr_instance, sample_portal_payload): + start_time = datetime(2024, 3, 15, 1, 0, tzinfo=timezone.utc) + end_time = datetime(2024, 3, 15, 1, 3, tzinfo=timezone.utc) + mock_response = Mock() + mock_response.content = json.dumps(sample_portal_payload).encode() + mock_response.raise_for_status = Mock() - expected_file = ( - DATA_DIR / current_time.strftime("%Y/%m") / f"DSCOVR_SW_NOWCAST_{current_time.strftime('%Y%m%d')}.csv" - ) + with patch("requests.get", return_value=mock_response) as mock_get: + dscovr_instance.download_and_process(start_time, end_time) + + expected_file = DATA_DIR / "2024/03" / "DSCOVR_SW_NOWCAST_20240315.csv" assert expected_file.exists() data = pd.read_csv(expected_file) - assert len(data) > 0 - assert all(field in data.columns for field in DSCOVR.MAG_FIELDS + DSCOVR.SWEPAM_FIELDS) - - def test_process_mag_file(self, swace_instance): - test_file = DATA_DIR / DSCOVR.NAME_MAG - test_file.parent.mkdir(exist_ok=True) - - response = requests.get(DSCOVR.URL + DSCOVR.NAME_MAG, timeout=10) - response.raise_for_status() - with open(test_file, "wb") as f: - f.write(response.content) - - data = swace_instance._process_mag_file(DATA_DIR) + assert len(data) == 4 + assert all(field in data.columns for field in EXPECTED_COLUMNS) + assert data["pdyn"].iloc[0] == pytest.approx(2e-6 * 4.2 * 380.0**2) + assert not Path("./temp_sw_dscovr_wget").exists() + + _, kwargs = mock_get.call_args + assert kwargs["params"]["start_time"] == "2024-03-15T01:00:00" + assert kwargs["params"]["end_time"] == "2024-03-15T01:03:00" + assert kwargs["params"]["parameters"] == ";".join(dscovr_instance._portal_parameters()) + assert kwargs["params"]["format"] == "json" + assert kwargs["params"]["time_format"] == "iso" + assert kwargs["timeout"] == 30 + + def test_portal_parameters(self, dscovr_instance): + parameters = dscovr_instance._portal_parameters() + + assert parameters == [ + "DSCOVR:m1m_dscovr:bt", + "DSCOVR:m1m_dscovr:bx_gse", + "DSCOVR:m1m_dscovr:by_gse", + "DSCOVR:m1m_dscovr:bz_gse", + "DSCOVR:f1m_dscovr:proton_speed", + "DSCOVR:f1m_dscovr:proton_density", + "DSCOVR:f1m_dscovr:proton_temperature", + ] + + def test_process_single_file(self, dscovr_instance, sample_portal_payload): + test_file = DATA_DIR / DSCOVR.NAME_DATA + + with open(test_file, "w") as f: + json.dump(sample_portal_payload, f) + + data = dscovr_instance._process_single_file(DATA_DIR) assert isinstance(data, pd.DataFrame) - assert all(field in data.columns for field in DSCOVR.MAG_FIELDS) - - def test_process_swepam_file(self, swace_instance): - test_file = DATA_DIR / DSCOVR.NAME_SWEPAM - test_file.parent.mkdir(exist_ok=True) - - response = requests.get(DSCOVR.URL + DSCOVR.NAME_SWEPAM, timeout=10) - response.raise_for_status() - with open(test_file, "wb") as f: - f.write(response.content) - - data = swace_instance._process_swepam_file(DATA_DIR) - - assert isinstance(data, pd.DataFrame) - assert all(field in data.columns for field in DSCOVR.SWEPAM_FIELDS) - - def test_read_with_no_data(self, swace_instance): + assert list(data.columns) == EXPECTED_COLUMNS + assert len(data) == 4 + assert data.index[0] == pd.Timestamp("2024-03-15T01:00:00Z") + assert data.index[-1] == pd.Timestamp("2024-03-15T01:03:00Z") + assert data.loc["2024-03-15 01:00:00+00:00", "bavg"] == 5.5 + assert data.loc["2024-03-15 01:00:00+00:00", "bx_gsm"] == 1.1 + assert data.loc["2024-03-15 01:01:00+00:00"].isna().all() + assert np.isnan(data.loc["2024-03-15 01:02:00+00:00", "bx_gsm"]) + assert np.isnan(data.loc["2024-03-15 01:02:00+00:00", "speed"]) + assert np.isnan(data.loc["2024-03-15 01:03:00+00:00", "proton_density"]) + assert np.isnan(data.loc["2024-03-15 01:03:00+00:00", "temperature"]) + assert data.loc["2024-03-15 01:00:00+00:00", "pdyn"] == pytest.approx(2e-6 * 4.2 * 380.0**2) + + def test_process_single_file_api_error(self, dscovr_instance): + test_file = DATA_DIR / DSCOVR.NAME_DATA + payload = {"status": {"code": 404, "message": "No matching data"}, "data": {"time": []}} + + with open(test_file, "w") as f: + json.dump(payload, f) + + with pytest.raises(FileNotFoundError, match="No matching data"): + dscovr_instance._process_single_file(DATA_DIR) + + def test_read_with_no_data(self, dscovr_instance): start_time = datetime(2020, 1, 1, tzinfo=timezone.utc) end_time = datetime(2020, 1, 2, tzinfo=timezone.utc) - data = swace_instance.read(start_time, end_time, download=False) + data = dscovr_instance.read(start_time, end_time, download=False) assert isinstance(data, pd.DataFrame) assert len(data) > 0 - assert all(col in data.columns for col in DSCOVR.MAG_FIELDS + DSCOVR.SWEPAM_FIELDS) + assert all(col in data.columns for col in EXPECTED_COLUMNS) assert data.isna().all().all() - def test_read_invalid_time_range(self, swace_instance): + def test_read_invalid_time_range(self, dscovr_instance): start_time = datetime(2020, 12, 31, tzinfo=timezone.utc) end_time = datetime(2020, 1, 1, tzinfo=timezone.utc) - with pytest.raises(ValueError): - swace_instance.read(start_time, end_time) + with pytest.raises(AssertionError, match="Start time must be before end time"): + dscovr_instance.read(start_time, end_time) - def test_read_with_existing_data(self, swace_instance): + def test_read_with_existing_data(self, dscovr_instance): start_time = datetime(2024, 3, 15, tzinfo=timezone.utc) end_time = datetime(2024, 3, 15, 23, 59, 59, tzinfo=timezone.utc) @@ -132,45 +184,43 @@ def test_read_with_existing_data(self, swace_instance): sample_data = pd.DataFrame( { "t": t, - "bavg": np.random.random(len(t)), - "bx_gsm": np.random.random(len(t)), - "by_gsm": np.random.random(len(t)), - "bz_gsm": np.random.random(len(t)), - "proton_density": np.random.random(len(t)), - "speed": np.random.random(len(t)), - "temperature": np.random.random(len(t)), + "bavg": np.full(len(t), 5.0), + "bx_gsm": np.full(len(t), 1.0), + "by_gsm": np.full(len(t), 2.0), + "bz_gsm": np.full(len(t), -1.0), + "proton_density": np.full(len(t), 4.2), + "speed": np.full(len(t), 380.0), + "temperature": np.full(len(t), 145000.0), + "pdyn": np.full(len(t), 2e-6 * 4.2 * 380.0**2), } ) - file_path = DATA_DIR / f"DSCOVR_SW_NOWCAST_{start_time.strftime('%Y%m%d')}.csv" + file_path = DATA_DIR / start_time.strftime("%Y/%m") / f"DSCOVR_SW_NOWCAST_{start_time.strftime('%Y%m%d')}.csv" + file_path.parent.mkdir(parents=True) sample_data.to_csv(file_path, index=False) - data = swace_instance.read(start_time, end_time) + data = dscovr_instance.read(start_time, end_time) assert isinstance(data, pd.DataFrame) assert len(data) == 1440 - assert all(col in data.columns for col in DSCOVR.MAG_FIELDS + DSCOVR.SWEPAM_FIELDS) + assert all(col in data.columns for col in EXPECTED_COLUMNS) + assert data.loc[start_time, "bavg"] == pytest.approx(sample_data["bavg"].iloc[0]) + assert data.loc[start_time, "file_name"] == file_path @pytest.mark.parametrize( "field,invalid_value,expected", [ - ("bx_gsm", -999.1, np.nan), - ("speed", -9999.1, np.nan), - ("temperature", -99999.1, np.nan), + ("bx_gsm", -100000.1, np.nan), + ("speed", 0.0, np.nan), + ("temperature", -100000.1, np.nan), ("proton_density", 4.2, 4.2), ], ) - def test_invalid_value_handling(self, field, invalid_value, expected): - data = pd.DataFrame({field: [invalid_value]}) - - if field in DSCOVR.MAG_FIELDS: - mask = data[field] < -999.0 - elif field in ["proton_density", "speed"]: - mask = data[field] < -9999.0 - else: - mask = data[field] < -99999.0 + def test_invalid_value_handling(self, dscovr_instance, field, invalid_value, expected): + data = pd.DataFrame({column: [1.0] for column in DSCOVR.MAG_FIELDS + DSCOVR.SWEPAM_FIELDS}) + data[field] = invalid_value - data.loc[mask, field] = np.nan + dscovr_instance._replace_invalid_values(data) if np.isnan(expected): assert np.isnan(data[field].iloc[0]) @@ -181,8 +231,8 @@ def test_with_propagation(self): start_time = datetime(2024, 11, 21, tzinfo=timezone.utc) end_time = datetime(2024, 11, 24, tzinfo=timezone.utc) - swace_instance = DSCOVR(Path(__file__).parent / "data" / "DSCOVR") - data = swace_instance.read(start_time, end_time, propagation=True) + dscovr_instance = DSCOVR(Path(__file__).parent / "data" / "DSCOVR") + data = dscovr_instance.read(start_time, end_time, propagation=True) assert isinstance(data, pd.DataFrame) assert len(data) == 5109