From e5739282c934bc50a4cdf66ed97b8e4c640fca0c Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Mon, 16 Mar 2026 11:33:45 -0600 Subject: [PATCH 1/8] Rebase dev to get recent glows updates needed. Add method to compute eclipitic coordinates of histogram bin centers to populate the DailyLightcurve ecliptic_lon and ecliptic_lat attributes. --- imap_processing/glows/l2/glows_l2_data.py | 74 +++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/imap_processing/glows/l2/glows_l2_data.py b/imap_processing/glows/l2/glows_l2_data.py index d5860c112..1261ea3da 100644 --- a/imap_processing/glows/l2/glows_l2_data.py +++ b/imap_processing/glows/l2/glows_l2_data.py @@ -8,8 +8,12 @@ from imap_processing.glows import FLAG_LENGTH from imap_processing.glows.l1b.glows_l1b_data import PipelineSettings -from imap_processing.glows.utils.constants import GlowsConstants -from imap_processing.spice.geometry import SpiceFrame, get_instrument_mounting_az_el +from imap_processing.spice.geometry import ( + SpiceFrame, + frame_transform_az_el, + get_instrument_mounting_az_el, +) +from imap_processing.spice.time import met_to_sclkticks, sct_to_et @dataclass @@ -122,8 +126,11 @@ def __post_init__(self, l1b_data: xr.Dataset, position_angle: float) -> None: ) else: self.histogram_flag_array = np.zeros(self.number_of_bins, dtype=np.uint8) - self.ecliptic_lon = np.zeros(self.number_of_bins) - self.ecliptic_lat = np.zeros(self.number_of_bins) + + # Get ecliptic longitude and latitude of bin centers + self.ecliptic_lon, self.ecliptic_lat = ( + self.compute_ecliptic_coords_of_bin_centers(l1b_data) + ) if self.number_of_bins: # imap_spin_angle_bin_cntr is the raw IMAP spin angle ψ (0 - 360°, @@ -167,6 +174,65 @@ def calculate_histogram_sums(histograms: NDArray) -> NDArray: histograms[histograms == GlowsConstants.HISTOGRAM_FILLVAL] = 0 return np.sum(histograms, axis=0, dtype=np.int64) + @staticmethod + def compute_ecliptic_coords_of_bin_centers( + l1b_data: xr.Dataset, + ) -> tuple[np.ndarray, np.ndarray]: + """ + Compute the ecliptic coordinates of the histogram bin centers. + + Histogram bin centers represent the center spin angle for each bin in the imap + frame and are stored in the "imap_spin_angle_bin_cntr" variable in the L1B + dataset. + + The ecliptic coordinates of the bin centers are computed as follows: + - Get the fixed elevation angle from the instrument pointing direction in + the DPS frame. + - Align azimuth with instrument pointing direction by rotating the center + spin angle for the bins by the position angle offset. + - Transform the resulting DPS coordinates into the ECLIPJ2000 frame using + SPICE transformations. + + Parameters + ---------- + l1b_data : xarray.Dataset + L1B data filtered by good times, good angles, and good bins for one + observation day. + + Returns + ------- + tuple[numpy.ndarray, numpy.ndarray] + Longitude and latitudes in the ECLIPJ2000 frame representing the pointing + direction of each histogram bin center, with shape (n_bins,). + """ + # Ephemeris start time of the histogram accumulation. + data_start_time_et = sct_to_et(met_to_sclkticks(l1b_data["imap_start_time"])) + + # Get elevation from instrument pointing direction in the DPS frame. + az_el = get_instrument_mounting_az_el(SpiceFrame.IMAP_GLOWS) + elevation = az_el[1] + + # Rotate spin-angle bin centers by the instrument position-angle offset + # so azimuth=0 aligns with the instrument pointing direction. + azimuth = ( + l1b_data["imap_spin_angle_bin_cntr"] + + l1b_data["position_angle_offset_average"] + ) % 360.0 + + # Create array of azimuth, elevation coordinates in the DPS frame (n_bins, 2) + az_el = np.column_stack((azimuth, np.full_like(azimuth, elevation))) + + # Transform coordinates to ECLIPJ2000 frame using SPICE transformations. + ecliptic_coords = frame_transform_az_el( + data_start_time_et, + az_el, + SpiceFrame.IMAP_DPS, + SpiceFrame.ECLIPJ2000, + ) + + # Return ecliptic longitudes and latitudes as separate arrays. + return ecliptic_coords[:, 0], ecliptic_coords[:, 1] + @dataclass class HistogramL2: From 5d56a7fbc65b1a4f0e77b038e321e4d104fe087f Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Tue, 17 Mar 2026 12:13:13 -0600 Subject: [PATCH 2/8] Mock method that computes ecliptic bin centers to update existing tests that were failing. Rebase and merge Maxine's spin angle work" --- imap_processing/tests/glows/test_glows_l2.py | 18 ++++++++++ .../tests/glows/test_glows_l2_data.py | 33 +++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/imap_processing/tests/glows/test_glows_l2.py b/imap_processing/tests/glows/test_glows_l2.py index ab7ab336b..8277314fa 100644 --- a/imap_processing/tests/glows/test_glows_l2.py +++ b/imap_processing/tests/glows/test_glows_l2.py @@ -18,6 +18,21 @@ from imap_processing.tests.glows.conftest import mock_update_spice_parameters +@pytest.fixture +def mock_ecliptic_bin_centers(monkeypatch): + """Keep DailyLightcurve unit tests independent from SPICE/time conversions.""" + + def _mock_compute_coords(l1b_data: xr.Dataset) -> tuple[np.ndarray, np.ndarray]: + n_bins = l1b_data["histogram"].shape[1] + return np.zeros(n_bins, dtype=float), np.zeros(n_bins, dtype=float) + + monkeypatch.setattr( + DailyLightcurve, + "compute_ecliptic_coords_of_bin_centers", + staticmethod(_mock_compute_coords), + ) + + @pytest.fixture def l1b_hists(): epoch = xr.DataArray(np.arange(4), name="epoch", dims=["epoch"]) @@ -51,6 +66,7 @@ def test_glows_l2( mock_ancillary_exclusions, mock_pipeline_settings, mock_conversion_table_dict, + mock_ecliptic_bin_centers, caplog, ): mock_spice_function.side_effect = mock_update_spice_parameters @@ -93,6 +109,8 @@ def test_generate_l2( mock_ancillary_exclusions, mock_pipeline_settings, mock_conversion_table_dict, + furnish_kernels, + mock_ecliptic_bin_centers, ): mock_spice_function.side_effect = mock_update_spice_parameters diff --git a/imap_processing/tests/glows/test_glows_l2_data.py b/imap_processing/tests/glows/test_glows_l2_data.py index 8885320a5..d5acc68ed 100644 --- a/imap_processing/tests/glows/test_glows_l2_data.py +++ b/imap_processing/tests/glows/test_glows_l2_data.py @@ -9,6 +9,21 @@ from imap_processing.glows.utils.constants import GlowsConstants +@pytest.fixture +def mock_ecliptic_bin_centers(monkeypatch): + """Keep DailyLightcurve unit tests independent from SPICE/time conversions.""" + + def _mock_compute_coords(l1b_data: xr.Dataset) -> tuple[np.ndarray, np.ndarray]: + n_bins = l1b_data["histogram"].shape[1] + return np.zeros(n_bins, dtype=float), np.zeros(n_bins, dtype=float) + + monkeypatch.setattr( + DailyLightcurve, + "compute_ecliptic_coords_of_bin_centers", + staticmethod(_mock_compute_coords), + ) + + @pytest.fixture def pipeline_settings(): """PipelineSettings with flags matching the default pipeline settings JSON. @@ -78,6 +93,8 @@ def l1b_dataset(): "spin_period_average": (["epoch"], [15.0, 15.0]), "number_of_spins_per_block": (["epoch"], [5, 5]), "imap_spin_angle_bin_cntr": (["epoch", "bins"], spin_angle), + "position_angle_offset_average": (["epoch"], [0.0, 0.0]), + "imap_start_time": (["epoch"], [0.0, 1.0]), "histogram_flag_array": ( ["epoch", "bad_angle_flags", "bins"], histogram_flag_array, @@ -89,7 +106,7 @@ def l1b_dataset(): return ds -def test_photon_flux(l1b_dataset): +def test_photon_flux(l1b_dataset, mock_ecliptic_bin_centers): """Flux = sum(histograms) / sum(exposure_times) per bin (Eq. 50).""" lc = DailyLightcurve(l1b_dataset, position_angle=0.0) @@ -108,7 +125,7 @@ def test_photon_flux(l1b_dataset): assert np.allclose(lc.photon_flux, expected_flux) -def test_flux_uncertainty(l1b_dataset): +def test_flux_uncertainty(l1b_dataset, mock_ecliptic_bin_centers): """Uncertainty = sqrt(sum_hist) / exposure per bin (Eq. 54).""" lc = DailyLightcurve(l1b_dataset, position_angle=0.0) @@ -116,7 +133,7 @@ def test_flux_uncertainty(l1b_dataset): assert np.allclose(lc.flux_uncertainties, expected_uncertainty) -def test_zero_exposure_bins(l1b_dataset): +def test_zero_exposure_bins(l1b_dataset, mock_ecliptic_bin_centers): """Bins with all-masked histograms get zero flux and uncertainty. Exposure time still accumulates uniformly from each good-time file even @@ -132,16 +149,18 @@ def test_zero_exposure_bins(l1b_dataset): assert np.allclose(lc.exposure_times, expected_exposure) -def test_number_of_bins(l1b_dataset): +def test_number_of_bins(l1b_dataset, mock_ecliptic_bin_centers): lc = DailyLightcurve(l1b_dataset, position_angle=0.0) assert lc.number_of_bins == 4 assert len(lc.spin_angle) == 4 assert len(lc.photon_flux) == 4 assert len(lc.flux_uncertainties) == 4 assert len(lc.exposure_times) == 4 + assert len(lc.ecliptic_lon) == 4 + assert len(lc.ecliptic_lat) == 4 -def test_histogram_flag_array_or_propagation(l1b_dataset): +def test_histogram_flag_array_or_propagation(l1b_dataset, mock_ecliptic_bin_centers): """histogram_flag_array is OR'd across all L1B epochs and flag rows per bin. Per Section 12.3.4: a flag is True in L2 if it is True in any L1B block. @@ -163,7 +182,7 @@ def test_histogram_flag_array_or_propagation(l1b_dataset): assert lc.histogram_flag_array[3] == 0 # no flags on bin 3 -def test_histogram_flag_array_zero_epochs(): +def test_histogram_flag_array_zero_epochs(mock_ecliptic_bin_centers): """histogram_flag_array is all zeros when the input dataset is empty. Note: this is NEVER expected to happen in production @@ -179,6 +198,8 @@ def test_histogram_flag_array_zero_epochs(): "spin_period_average": (["epoch"], []), "number_of_spins_per_block": (["epoch"], []), "imap_spin_angle_bin_cntr": (["epoch", "bins"], spin_angle), + "position_angle_offset_average": (["epoch"], []), + "imap_start_time": (["epoch"], []), "histogram_flag_array": ( ["epoch", "bad_angle_flags", "bins"], histogram_flag_array, From dc4f7a558f16174022aed1859b7a9e1c41c3a30c Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Tue, 24 Mar 2026 14:45:11 -0600 Subject: [PATCH 3/8] Add unit test for ecliptic coordinates (WIP) --- imap_processing/glows/l2/glows_l2_data.py | 2 +- .../tests/glows/test_glows_l2_data.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/imap_processing/glows/l2/glows_l2_data.py b/imap_processing/glows/l2/glows_l2_data.py index 1261ea3da..9778459d0 100644 --- a/imap_processing/glows/l2/glows_l2_data.py +++ b/imap_processing/glows/l2/glows_l2_data.py @@ -220,7 +220,7 @@ def compute_ecliptic_coords_of_bin_centers( ) % 360.0 # Create array of azimuth, elevation coordinates in the DPS frame (n_bins, 2) - az_el = np.column_stack((azimuth, np.full_like(azimuth, elevation))) + az_el = np.stack((azimuth, np.full_like(azimuth, elevation)), axis=-1) # Transform coordinates to ECLIPJ2000 frame using SPICE transformations. ecliptic_coords = frame_transform_az_el( diff --git a/imap_processing/tests/glows/test_glows_l2_data.py b/imap_processing/tests/glows/test_glows_l2_data.py index d5acc68ed..84c34f2b6 100644 --- a/imap_processing/tests/glows/test_glows_l2_data.py +++ b/imap_processing/tests/glows/test_glows_l2_data.py @@ -7,6 +7,7 @@ from imap_processing.glows.l1b.glows_l1b_data import PipelineSettings from imap_processing.glows.l2.glows_l2_data import DailyLightcurve, HistogramL2 from imap_processing.glows.utils.constants import GlowsConstants +from imap_processing.spice.time import met_to_ttj2000ns @pytest.fixture @@ -106,6 +107,35 @@ def l1b_dataset(): return ds +@pytest.mark.external_kernel +def test_ecliptic_coords_computation(furnish_kernels, l1b_dataset): + """Test method that computes ecliptic coordinates.""" + + # Update the epoch and imap_start time to real values + # for 2026-01-01 and 2026-01-02 in seconds since J2000 + # with leap seconds included + l1b_dataset = l1b_dataset.assign_coords( + epoch=xr.DataArray( + [met_to_ttj2000ns(504975603.125), met_to_ttj2000ns(505975604.125)], + dims=["epoch"], + ) + ) + l1b_dataset["imap_start_time"] = (["epoch"], [504975603.125, 505975604.125]) + kernels = [ + "naif0012.tls", + "de440s.bsp", + "imap_sclk_0000.tsc", + "imap_130.tf", + "imap_science_120.tf", + "sim_1yr_imap_attitude.bc", + "sim_1yr_imap_pointing_frame.bc", + ] + with furnish_kernels(kernels): + lc = DailyLightcurve(l1b_dataset) + assert np.all(lc.ecliptic_lon == 0) + assert np.all(lc.ecliptic_lat == 0) + + def test_photon_flux(l1b_dataset, mock_ecliptic_bin_centers): """Flux = sum(histograms) / sum(exposure_times) per bin (Eq. 50).""" lc = DailyLightcurve(l1b_dataset, position_angle=0.0) From bd97ee26e3481463b90c6e9e221792fcadb0d8f1 Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Wed, 25 Mar 2026 17:25:04 -0600 Subject: [PATCH 4/8] Complete unit test for new method. Move mocked ecliptic coordinates to conftest. Modify method computing ecliptic coordinates to take two args rather than the l1b dataset --- imap_processing/glows/l2/glows_l2_data.py | 56 ++++++-------- imap_processing/tests/glows/conftest.py | 18 +++++ imap_processing/tests/glows/test_glows_l2.py | 19 +---- .../tests/glows/test_glows_l2_data.py | 73 +++++++++---------- 4 files changed, 80 insertions(+), 86 deletions(-) diff --git a/imap_processing/glows/l2/glows_l2_data.py b/imap_processing/glows/l2/glows_l2_data.py index 9778459d0..165619f19 100644 --- a/imap_processing/glows/l2/glows_l2_data.py +++ b/imap_processing/glows/l2/glows_l2_data.py @@ -8,6 +8,7 @@ from imap_processing.glows import FLAG_LENGTH from imap_processing.glows.l1b.glows_l1b_data import PipelineSettings +from imap_processing.glows.utils.constants import GlowsConstants from imap_processing.spice.geometry import ( SpiceFrame, frame_transform_az_el, @@ -127,11 +128,6 @@ def __post_init__(self, l1b_data: xr.Dataset, position_angle: float) -> None: else: self.histogram_flag_array = np.zeros(self.number_of_bins, dtype=np.uint8) - # Get ecliptic longitude and latitude of bin centers - self.ecliptic_lon, self.ecliptic_lat = ( - self.compute_ecliptic_coords_of_bin_centers(l1b_data) - ) - if self.number_of_bins: # imap_spin_angle_bin_cntr is the raw IMAP spin angle ψ (0 - 360°, # bin midpoints). @@ -151,8 +147,14 @@ def __post_init__(self, l1b_data: xr.Dataset, position_angle: float) -> None: self.exposure_times = np.roll(self.exposure_times, roll) self.flux_uncertainties = np.roll(self.flux_uncertainties, roll) self.histogram_flag_array = np.roll(self.histogram_flag_array, roll) - self.ecliptic_lon = np.roll(self.ecliptic_lon, roll) - self.ecliptic_lat = np.roll(self.ecliptic_lat, roll) + et_imap_start_time = sct_to_et( + met_to_sclkticks(l1b_data["imap_start_time"][0].data) + ) + self.ecliptic_lon, self.ecliptic_lat = ( + self.compute_ecliptic_coords_of_bin_centers( + et_imap_start_time, self.spin_angle + ) + ) @staticmethod def calculate_histogram_sums(histograms: NDArray) -> NDArray: @@ -176,28 +178,25 @@ def calculate_histogram_sums(histograms: NDArray) -> NDArray: @staticmethod def compute_ecliptic_coords_of_bin_centers( - l1b_data: xr.Dataset, + data_start_time_et: float, spin_angle_bin_centers: NDArray ) -> tuple[np.ndarray, np.ndarray]: """ Compute the ecliptic coordinates of the histogram bin centers. Histogram bin centers represent the center spin angle for each bin in the imap - frame and are stored in the "imap_spin_angle_bin_cntr" variable in the L1B - dataset. - - The ecliptic coordinates of the bin centers are computed as follows: - - Get the fixed elevation angle from the instrument pointing direction in - the DPS frame. - - Align azimuth with instrument pointing direction by rotating the center - spin angle for the bins by the position angle offset. - - Transform the resulting DPS coordinates into the ECLIPJ2000 frame using - SPICE transformations. + frame, which corresponds to a specific pointing direction in space. This method + transforms the instrument pointing direction for each bin center from the IMAP + spacecraft frame to the ECLIPJ2000 frame. Parameters ---------- - l1b_data : xarray.Dataset - L1B data filtered by good times, good angles, and good bins for one - observation day. + data_start_time_et : float + Ephemeris time corresponding to the start of the histogram accumulation. + + spin_angle_bin_centers : numpy.ndarray + Spin angle bin centers for the histogram bins, measured in the IMAP frame, + with shape (n_bins,), and already corrected for the northernmost point of + the scanning circle. Returns ------- @@ -205,19 +204,12 @@ def compute_ecliptic_coords_of_bin_centers( Longitude and latitudes in the ECLIPJ2000 frame representing the pointing direction of each histogram bin center, with shape (n_bins,). """ - # Ephemeris start time of the histogram accumulation. - data_start_time_et = sct_to_et(met_to_sclkticks(l1b_data["imap_start_time"])) + # In the IMAP frame, the azimuth corresponds to the spin angle bin centers + azimuth = spin_angle_bin_centers # Get elevation from instrument pointing direction in the DPS frame. - az_el = get_instrument_mounting_az_el(SpiceFrame.IMAP_GLOWS) - elevation = az_el[1] - - # Rotate spin-angle bin centers by the instrument position-angle offset - # so azimuth=0 aligns with the instrument pointing direction. - azimuth = ( - l1b_data["imap_spin_angle_bin_cntr"] - + l1b_data["position_angle_offset_average"] - ) % 360.0 + az_el_instrument_mounting = get_instrument_mounting_az_el(SpiceFrame.IMAP_GLOWS) + elevation = az_el_instrument_mounting[1] # Create array of azimuth, elevation coordinates in the DPS frame (n_bins, 2) az_el = np.stack((azimuth, np.full_like(azimuth, elevation)), axis=-1) diff --git a/imap_processing/tests/glows/conftest.py b/imap_processing/tests/glows/conftest.py index c2d573f73..e8ed5e2fd 100644 --- a/imap_processing/tests/glows/conftest.py +++ b/imap_processing/tests/glows/conftest.py @@ -13,6 +13,7 @@ AncillaryParameters, ) from imap_processing.glows.l2.glows_l2 import glows_l2 +from imap_processing.glows.l2.glows_l2_data import DailyLightcurve @pytest.fixture @@ -278,6 +279,23 @@ def mock_pipeline_settings(): return mock_pipeline_dataset +@pytest.fixture +def mock_ecliptic_bin_centers(monkeypatch): + """Keep DailyLightcurve unit tests independent of SPICE/time conversions.""" + + def _mock_compute_coords( + _data_start_time_et: float, spin_angle: np.ndarray + ) -> tuple[np.ndarray, np.ndarray]: + n_bins = len(spin_angle) + return np.zeros(n_bins, dtype=float), np.zeros(n_bins, dtype=float) + + monkeypatch.setattr( + DailyLightcurve, + "compute_ecliptic_coords_of_bin_centers", + staticmethod(_mock_compute_coords), + ) + + def mock_update_spice_parameters(self, *args, **kwargs): self.spin_period_ground_average = np.float64(0.0) self.spin_period_ground_std_dev = np.float64(0.0) diff --git a/imap_processing/tests/glows/test_glows_l2.py b/imap_processing/tests/glows/test_glows_l2.py index 8277314fa..201cd090b 100644 --- a/imap_processing/tests/glows/test_glows_l2.py +++ b/imap_processing/tests/glows/test_glows_l2.py @@ -15,22 +15,9 @@ from imap_processing.glows.l2.glows_l2_data import DailyLightcurve, HistogramL2 from imap_processing.glows.utils.constants import GlowsConstants from imap_processing.spice.time import et_to_datetime64, ttj2000ns_to_et -from imap_processing.tests.glows.conftest import mock_update_spice_parameters - - -@pytest.fixture -def mock_ecliptic_bin_centers(monkeypatch): - """Keep DailyLightcurve unit tests independent from SPICE/time conversions.""" - - def _mock_compute_coords(l1b_data: xr.Dataset) -> tuple[np.ndarray, np.ndarray]: - n_bins = l1b_data["histogram"].shape[1] - return np.zeros(n_bins, dtype=float), np.zeros(n_bins, dtype=float) - - monkeypatch.setattr( - DailyLightcurve, - "compute_ecliptic_coords_of_bin_centers", - staticmethod(_mock_compute_coords), - ) +from imap_processing.tests.glows.conftest import ( + mock_update_spice_parameters, +) @pytest.fixture diff --git a/imap_processing/tests/glows/test_glows_l2_data.py b/imap_processing/tests/glows/test_glows_l2_data.py index 84c34f2b6..ccccf8993 100644 --- a/imap_processing/tests/glows/test_glows_l2_data.py +++ b/imap_processing/tests/glows/test_glows_l2_data.py @@ -7,22 +7,7 @@ from imap_processing.glows.l1b.glows_l1b_data import PipelineSettings from imap_processing.glows.l2.glows_l2_data import DailyLightcurve, HistogramL2 from imap_processing.glows.utils.constants import GlowsConstants -from imap_processing.spice.time import met_to_ttj2000ns - - -@pytest.fixture -def mock_ecliptic_bin_centers(monkeypatch): - """Keep DailyLightcurve unit tests independent from SPICE/time conversions.""" - - def _mock_compute_coords(l1b_data: xr.Dataset) -> tuple[np.ndarray, np.ndarray]: - n_bins = l1b_data["histogram"].shape[1] - return np.zeros(n_bins, dtype=float), np.zeros(n_bins, dtype=float) - - monkeypatch.setattr( - DailyLightcurve, - "compute_ecliptic_coords_of_bin_centers", - staticmethod(_mock_compute_coords), - ) +from imap_processing.spice.time import met_to_sclkticks, sct_to_et @pytest.fixture @@ -94,7 +79,6 @@ def l1b_dataset(): "spin_period_average": (["epoch"], [15.0, 15.0]), "number_of_spins_per_block": (["epoch"], [5, 5]), "imap_spin_angle_bin_cntr": (["epoch", "bins"], spin_angle), - "position_angle_offset_average": (["epoch"], [0.0, 0.0]), "imap_start_time": (["epoch"], [0.0, 1.0]), "histogram_flag_array": ( ["epoch", "bad_angle_flags", "bins"], @@ -108,32 +92,45 @@ def l1b_dataset(): @pytest.mark.external_kernel -def test_ecliptic_coords_computation(furnish_kernels, l1b_dataset): +def test_ecliptic_coords_computation(furnish_kernels): """Test method that computes ecliptic coordinates.""" - # Update the epoch and imap_start time to real values - # for 2026-01-01 and 2026-01-02 in seconds since J2000 - # with leap seconds included - l1b_dataset = l1b_dataset.assign_coords( - epoch=xr.DataArray( - [met_to_ttj2000ns(504975603.125), met_to_ttj2000ns(505975604.125)], - dims=["epoch"], - ) - ) - l1b_dataset["imap_start_time"] = (["epoch"], [504975603.125, 505975604.125]) + # Start time is 2026-01-01 since J2000 which is covered by + # the spice kernels + data_start_time_et = sct_to_et(met_to_sclkticks(504975603.125)) + n_bins = 4 + spin_angle = np.linspace(0, 270, n_bins) + kernels = [ "naif0012.tls", - "de440s.bsp", "imap_sclk_0000.tsc", "imap_130.tf", "imap_science_120.tf", - "sim_1yr_imap_attitude.bc", "sim_1yr_imap_pointing_frame.bc", ] + with furnish_kernels(kernels): - lc = DailyLightcurve(l1b_dataset) - assert np.all(lc.ecliptic_lon == 0) - assert np.all(lc.ecliptic_lat == 0) + ecliptic_lon, ecliptic_lat = ( + DailyLightcurve.compute_ecliptic_coords_of_bin_centers( + data_start_time_et, spin_angle + ) + ) + + # ecliptic_lon and ecliptic_lat must have one entry per bin + assert len(ecliptic_lon) == n_bins + assert len(ecliptic_lat) == n_bins + + # ecliptic longitude must be in [0, 360) + assert np.all(ecliptic_lon >= 0.0) + assert np.all(ecliptic_lon < 360.0) + + # ecliptic latitude must be in [-90, 90] + assert np.all(ecliptic_lat >= -90.0) + assert np.all(ecliptic_lat <= 90.0) + + # values must be finite (no NaN / Inf from SPICE) + assert np.all(np.isfinite(ecliptic_lon)) + assert np.all(np.isfinite(ecliptic_lat)) def test_photon_flux(l1b_dataset, mock_ecliptic_bin_centers): @@ -228,8 +225,6 @@ def test_histogram_flag_array_zero_epochs(mock_ecliptic_bin_centers): "spin_period_average": (["epoch"], []), "number_of_spins_per_block": (["epoch"], []), "imap_spin_angle_bin_cntr": (["epoch", "bins"], spin_angle), - "position_angle_offset_average": (["epoch"], []), - "imap_start_time": (["epoch"], []), "histogram_flag_array": ( ["epoch", "bad_angle_flags", "bins"], histogram_flag_array, @@ -263,7 +258,7 @@ def test_filter_good_times(): # ── spin_angle tests ────────────────────────────────────────────────────────── -def test_spin_angle_offset_formula(l1b_dataset): +def test_spin_angle_offset_formula(l1b_dataset, mock_ecliptic_bin_centers): """spin_angle = (imap_spin_angle_bin_cntr - position_angle + 360) % 360. Fixture spin_angle_bin_cntr = [0, 90, 180, 270], position_angle = 90. @@ -275,7 +270,7 @@ def test_spin_angle_offset_formula(l1b_dataset): assert np.allclose(lc.spin_angle, expected) -def test_spin_angle_starts_at_minimum(l1b_dataset): +def test_spin_angle_starts_at_minimum(l1b_dataset, mock_ecliptic_bin_centers): """After rolling, lc.spin_angle[0] is the minimum value. Fixture spin_angle_bin_cntr = [0, 90, 180, 270], position_angle = 45. @@ -352,7 +347,9 @@ def l1b_dataset_full(): ) -def test_position_angle_offset_average(l1b_dataset_full, pipeline_settings): +def test_position_angle_offset_average( + l1b_dataset_full, pipeline_settings, mock_ecliptic_bin_centers +): """position_angle_offset_average is a scalar equal to the result of compute_position_angle (Eq. 30, Section 10.6). It is constant across the observational day since it depends only on instrument mounting geometry. From 010387bd1c0f390a1da805ea7239c7225d1c92cd Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Thu, 26 Mar 2026 12:00:45 -0600 Subject: [PATCH 5/8] Address PR comments - simplify docstrings and remove furnished kernels from a test that doesn't need it --- imap_processing/glows/l2/glows_l2_data.py | 6 ++---- imap_processing/tests/glows/test_glows_l2.py | 1 - imap_processing/tests/glows/test_glows_l2_data.py | 3 +-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/imap_processing/glows/l2/glows_l2_data.py b/imap_processing/glows/l2/glows_l2_data.py index 165619f19..3117ea1b0 100644 --- a/imap_processing/glows/l2/glows_l2_data.py +++ b/imap_processing/glows/l2/glows_l2_data.py @@ -183,10 +183,8 @@ def compute_ecliptic_coords_of_bin_centers( """ Compute the ecliptic coordinates of the histogram bin centers. - Histogram bin centers represent the center spin angle for each bin in the imap - frame, which corresponds to a specific pointing direction in space. This method - transforms the instrument pointing direction for each bin center from the IMAP - spacecraft frame to the ECLIPJ2000 frame. + This method transforms the instrument pointing direction for each bin + center from the IMAP Pointing frame (IMAP_DPS) to the ECLIPJ2000 frame. Parameters ---------- diff --git a/imap_processing/tests/glows/test_glows_l2.py b/imap_processing/tests/glows/test_glows_l2.py index 201cd090b..d7632f79d 100644 --- a/imap_processing/tests/glows/test_glows_l2.py +++ b/imap_processing/tests/glows/test_glows_l2.py @@ -96,7 +96,6 @@ def test_generate_l2( mock_ancillary_exclusions, mock_pipeline_settings, mock_conversion_table_dict, - furnish_kernels, mock_ecliptic_bin_centers, ): mock_spice_function.side_effect = mock_update_spice_parameters diff --git a/imap_processing/tests/glows/test_glows_l2_data.py b/imap_processing/tests/glows/test_glows_l2_data.py index ccccf8993..d343fd630 100644 --- a/imap_processing/tests/glows/test_glows_l2_data.py +++ b/imap_processing/tests/glows/test_glows_l2_data.py @@ -95,8 +95,7 @@ def l1b_dataset(): def test_ecliptic_coords_computation(furnish_kernels): """Test method that computes ecliptic coordinates.""" - # Start time is 2026-01-01 since J2000 which is covered by - # the spice kernels + # Use a met value within the SPICE kernel coverage (2026-01-01). data_start_time_et = sct_to_et(met_to_sclkticks(504975603.125)) n_bins = 4 spin_angle = np.linspace(0, 270, n_bins) From 5a50f98c13b552fdcfd022f1d86ef1658b01ab4b Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Thu, 26 Mar 2026 12:11:40 -0600 Subject: [PATCH 6/8] Clarify docstring for mocked ecliptic coordinates --- imap_processing/tests/glows/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imap_processing/tests/glows/conftest.py b/imap_processing/tests/glows/conftest.py index e8ed5e2fd..0b95cde28 100644 --- a/imap_processing/tests/glows/conftest.py +++ b/imap_processing/tests/glows/conftest.py @@ -281,7 +281,7 @@ def mock_pipeline_settings(): @pytest.fixture def mock_ecliptic_bin_centers(monkeypatch): - """Keep DailyLightcurve unit tests independent of SPICE/time conversions.""" + """Mock ecliptic coordinates for bin centers.""" def _mock_compute_coords( _data_start_time_et: float, spin_angle: np.ndarray From 1c7bfbdb2c003d3669eeb0973a60a1e564a77a63 Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Mon, 30 Mar 2026 11:12:10 -0600 Subject: [PATCH 7/8] Address PR comments - use the midpoint time so that it falls within the repointing time coverage --- imap_processing/glows/l2/glows_l2_data.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/imap_processing/glows/l2/glows_l2_data.py b/imap_processing/glows/l2/glows_l2_data.py index 3117ea1b0..b08247660 100644 --- a/imap_processing/glows/l2/glows_l2_data.py +++ b/imap_processing/glows/l2/glows_l2_data.py @@ -147,8 +147,12 @@ def __post_init__(self, l1b_data: xr.Dataset, position_angle: float) -> None: self.exposure_times = np.roll(self.exposure_times, roll) self.flux_uncertainties = np.roll(self.flux_uncertainties, roll) self.histogram_flag_array = np.roll(self.histogram_flag_array, roll) + + # Get the midpoint start time covered by repointing kernels + # needed to compute ecliptic coordinates + mid_idx = len(l1b_data["imap_start_time"]) // 2 et_imap_start_time = sct_to_et( - met_to_sclkticks(l1b_data["imap_start_time"][0].data) + met_to_sclkticks(l1b_data["imap_start_time"][mid_idx].data) ) self.ecliptic_lon, self.ecliptic_lat = ( self.compute_ecliptic_coords_of_bin_centers( From 601562513383165b42cd3833ae2e7afdfc620e10 Mon Sep 17 00:00:00 2001 From: Veronica Martinez Date: Mon, 30 Mar 2026 13:15:36 -0600 Subject: [PATCH 8/8] Rename et time variable to clearly indicate that it's the midpoint pointing time --- imap_processing/glows/l2/glows_l2_data.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/imap_processing/glows/l2/glows_l2_data.py b/imap_processing/glows/l2/glows_l2_data.py index b08247660..c9fb7cf6d 100644 --- a/imap_processing/glows/l2/glows_l2_data.py +++ b/imap_processing/glows/l2/glows_l2_data.py @@ -151,12 +151,12 @@ def __post_init__(self, l1b_data: xr.Dataset, position_angle: float) -> None: # Get the midpoint start time covered by repointing kernels # needed to compute ecliptic coordinates mid_idx = len(l1b_data["imap_start_time"]) // 2 - et_imap_start_time = sct_to_et( + pointing_midpoint_time_et = sct_to_et( met_to_sclkticks(l1b_data["imap_start_time"][mid_idx].data) ) self.ecliptic_lon, self.ecliptic_lat = ( self.compute_ecliptic_coords_of_bin_centers( - et_imap_start_time, self.spin_angle + pointing_midpoint_time_et, self.spin_angle ) ) @@ -182,7 +182,7 @@ def calculate_histogram_sums(histograms: NDArray) -> NDArray: @staticmethod def compute_ecliptic_coords_of_bin_centers( - data_start_time_et: float, spin_angle_bin_centers: NDArray + data_time_et: float, spin_angle_bin_centers: NDArray ) -> tuple[np.ndarray, np.ndarray]: """ Compute the ecliptic coordinates of the histogram bin centers. @@ -192,8 +192,8 @@ def compute_ecliptic_coords_of_bin_centers( Parameters ---------- - data_start_time_et : float - Ephemeris time corresponding to the start of the histogram accumulation. + data_time_et : float + Ephemeris time corresponding to the midpoint of the histogram accumulation. spin_angle_bin_centers : numpy.ndarray Spin angle bin centers for the histogram bins, measured in the IMAP frame, @@ -218,7 +218,7 @@ def compute_ecliptic_coords_of_bin_centers( # Transform coordinates to ECLIPJ2000 frame using SPICE transformations. ecliptic_coords = frame_transform_az_el( - data_start_time_et, + data_time_et, az_el, SpiceFrame.IMAP_DPS, SpiceFrame.ECLIPJ2000,