From ce32c4ec8dbd032e12eabbcc33122b6418e2b781 Mon Sep 17 00:00:00 2001 From: Luisa Date: Thu, 18 Dec 2025 16:16:43 -0700 Subject: [PATCH 1/3] add backup method for handling events where the aux dataset may not cover it --- .../ultra/unit/test_ultra_l1b_culling.py | 7 +- .../ultra/unit/test_ultra_l1b_extended.py | 42 ++--- .../ultra/unit/test_ultra_l1c_pset_bins.py | 12 +- imap_processing/ultra/constants.py | 3 +- imap_processing/ultra/l1b/de.py | 9 +- .../ultra/l1b/ultra_l1b_culling.py | 5 +- .../ultra/l1b/ultra_l1b_extended.py | 150 +++++++++++++----- imap_processing/ultra/l1c/helio_pset.py | 1 + imap_processing/ultra/l1c/spacecraft_pset.py | 1 + .../ultra/l1c/ultra_l1c_pset_bins.py | 23 +-- 10 files changed, 168 insertions(+), 85 deletions(-) diff --git a/imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py b/imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py index 0c5cbac2d..3e25c9587 100644 --- a/imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py +++ b/imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py @@ -26,9 +26,9 @@ get_energy_histogram, get_n_sigma, get_pulses_per_spin, - get_spin_and_duration, get_spin_data, ) +from imap_processing.ultra.l1b.ultra_l1b_extended import get_spin_info TEST_PATH = imap_module_directory / "tests" / "ultra" / "data" / "l1" @@ -185,6 +185,7 @@ def test_get_duration(rates_l1_test_path, use_fake_spin_data_for_time): aux_ds = xr.Dataset( data_vars={ "timespinstart": ("epoch", spin_start_times), + "timespinstartsub": ("epoch", np.ones_like(spin_start_times)), "duration": ("epoch", np.full(num_spins, 15)), "spinnumber": ("epoch", spin_numbers), }, @@ -193,7 +194,9 @@ def test_get_duration(rates_l1_test_path, use_fake_spin_data_for_time): met = df["TimeTag"] - df["TimeTag"].values[0] spin = df["Spin"] - spin_number, duration = get_spin_and_duration(aux_ds, met) + spin_ds = get_spin_info(aux_ds, met) + spin_number = spin_ds["spin_number"].values + duration = spin_ds["spin_duration"].values assert np.array_equal(spin, spin_number) assert np.all(duration == 15) diff --git a/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py b/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py index 25c79ce20..c7af81f19 100644 --- a/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +++ b/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py @@ -31,7 +31,7 @@ get_path_length, get_ph_tof_and_back_positions, get_phi_theta, - get_spin_and_duration, + get_spin_info, get_ssd_back_position_and_tof_offset, get_ssd_tof, interpolate_fwhm, @@ -561,44 +561,50 @@ def test_get_eventtimes(test_fixture, aux_dataset): @pytest.mark.external_test_data def test_get_spin_and_duration(test_fixture, aux_dataset): - """Tests get_spin_and_duration function.""" + """Tests get_spin_info function.""" df_filt, _, _, de_dataset = test_fixture - spin_number, spin_duration = get_spin_and_duration( + spin_ds = get_spin_info( aux_dataset, de_dataset["shcoarse"].values, ) # Check shapes - assert spin_number.shape == spin_duration.shape == de_dataset["shcoarse"].shape + assert ( + spin_ds.spin_number.shape + == spin_ds.spin_duration.shape + == de_dataset["shcoarse"].shape + ) t1_spin_number = aux_dataset["spinnumber"].values[0] t1_start_dur = aux_dataset["duration"].values[0] # Check the first event spin number and duration - assert spin_number[0] == t1_spin_number - assert spin_duration[0] == t1_start_dur + assert spin_ds.spin_number[0] == t1_spin_number + assert spin_ds.spin_duration[0] == t1_start_dur @pytest.mark.external_test_data -def test_get_event_times_out_of_range(test_fixture, aux_dataset): +def test_get_event_times_out_of_range( + test_fixture, aux_dataset, use_fake_spin_data_for_time, caplog +): """Tests get_event_times with out of range values.""" df_filt, _, _, de_dataset = test_fixture # Get min time from aux_dataset min_time = aux_dataset["timespinstart"].values.min() # Set some coarse times to be out of range (less than min_time) coarse_times = de_dataset["shcoarse"].values.copy() - # Set first coarse time to be out of range + # Set first coarse time to be out of range of the aux data coarse_times[0] = min_time - 1000 - - with pytest.raises( - ValueError, - match="Coarse MET time contains events outside aux_dataset time range", - ): - get_event_times( - aux_dataset, - de_dataset["phase_angle"].values, - coarse_times, - ) + # set spin data that DOES cover the range of coarse_times + use_fake_spin_data_for_time(min_time - 1000, min_time + 10000) + # This should not raise an error. + event_times, spin_starts = get_event_times( + aux_dataset, + de_dataset["phase_angle"].values, + coarse_times, + ) + assert event_times.shape == coarse_times.shape + assert spin_starts.shape == coarse_times.shape @pytest.mark.external_test_data diff --git a/imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py b/imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py index 8aab16049..2d6f24957 100644 --- a/imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py +++ b/imap_processing/tests/ultra/unit/test_ultra_l1c_pset_bins.py @@ -201,7 +201,7 @@ def test_get_deadtime_ratios(): assert np.all(deadtime_correction_factors >= 0) -def test_get_deadtime_interpolator(use_fake_spin_data_for_time): +def test_get_deadtime_interpolator(use_fake_spin_data_for_time, aux_dataset): """Tests get_deadtime_correction_factors function.""" use_fake_spin_data_for_time(1, 10) sector_rate_seconds = 20 * 60 # 20 minutes in seconds @@ -223,7 +223,7 @@ def test_get_deadtime_interpolator(use_fake_spin_data_for_time): return_value=deadtime_ratios, ): deadtime_ratios = get_deadtime_ratios_by_spin_phase( - sectored_rates_ds, spin_steps=num_deadtimes + sectored_rates_ds, aux_dataset, spin_steps=num_deadtimes ) np.testing.assert_array_equal(deadtime_ratios.shape, (num_deadtimes)) @@ -237,12 +237,12 @@ def test_get_deadtime_interpolator(use_fake_spin_data_for_time): match="All dead time ratios are NaN, cannot interpolate", ): get_deadtime_ratios_by_spin_phase( - sectored_rates_ds, spin_steps=num_deadtimes + sectored_rates_ds, aux_dataset, spin_steps=num_deadtimes ) @pytest.mark.external_test_data -def test_get_deadtime_interpolator_no_sectored_rates(ancillary_files): +def test_get_deadtime_interpolator_no_sectored_rates(ancillary_files, aux_dataset): """Tests get_deadtime_correction_factors function.""" num_deadtimes = 15000 # Standard number of spin phases @@ -251,6 +251,7 @@ def test_get_deadtime_interpolator_no_sectored_rates(ancillary_files): # static deadtime ratios lookup. dt_ratios = get_deadtime_ratios_by_spin_phase( sectored_rates=None, + aux_dataset=aux_dataset, spin_steps=num_deadtimes, sensor_id=sensor, ancillary_files=ancillary_files, @@ -393,6 +394,7 @@ def test_get_spacecraft_exposure_times( imap_ena_sim_metakernel, ancillary_files, use_fake_spin_data_for_time, + aux_dataset, ): """Test get_spacecraft_exposure_times function.""" data_start_time = 445015665.0 @@ -419,6 +421,7 @@ def test_get_spacecraft_exposure_times( rates_dataset, pixels_below_threshold, boundary_sf, + aux_dataset, ( data_start_time, data_start_time, @@ -446,6 +449,7 @@ def test_get_spacecraft_background_rates( aux_ds = xr.Dataset( data_vars={ "timespinstart": ("epoch", spin_start_times), + "timespinstartsub": ("epoch", np.ones_like(spin_start_times)), "duration": ("epoch", np.full(num_spins, 15)), "spinnumber": ("epoch", spin_numbers), }, diff --git a/imap_processing/ultra/constants.py b/imap_processing/ultra/constants.py index e4c5eb644..983d29541 100644 --- a/imap_processing/ultra/constants.py +++ b/imap_processing/ultra/constants.py @@ -179,4 +179,5 @@ class UltraConstants: # For spatiotemporal culling EARTH_RADIUS_KM: float = 6378.1 - DEFAULT_EARTH_CULLING_RADIUS = EARTH_RADIUS_KM * 30 + N_RE = 50 + DEFAULT_EARTH_CULLING_RADIUS = EARTH_RADIUS_KM * N_RE diff --git a/imap_processing/ultra/l1b/de.py b/imap_processing/ultra/l1b/de.py index d8f6e266b..38af1bedf 100644 --- a/imap_processing/ultra/l1b/de.py +++ b/imap_processing/ultra/l1b/de.py @@ -37,7 +37,7 @@ get_path_length, get_ph_tof_and_back_positions, get_phi_theta, - get_spin_and_duration, + get_spin_info, get_ssd_back_position_and_tof_offset, get_ssd_tof, is_back_tof_valid, @@ -158,15 +158,18 @@ def calculate_de( f"ultra{sensor}", ancillary_files, ) + start_type[valid_indices] = de_dataset["start_type"].data[valid_indices] + spin_ds = get_spin_info(aux_dataset, de_dataset["shcoarse"].data) + (event_times, spin_starts) = get_event_times( aux_dataset, de_dataset["phase_angle"].data, de_dataset["shcoarse"].data, + spin_ds, ) - spin_number, _ = get_spin_and_duration(aux_dataset, de_dataset["shcoarse"].data) - de_dict["spin"] = spin_number + de_dict["spin"] = spin_ds.spin_number.data de_dict["event_times"] = event_times.astype(np.float64) # Pulse height ph_result = get_ph_tof_and_back_positions( diff --git a/imap_processing/ultra/l1b/ultra_l1b_culling.py b/imap_processing/ultra/l1b/ultra_l1b_culling.py index 518e71a2b..2d4dbe10d 100644 --- a/imap_processing/ultra/l1b/ultra_l1b_culling.py +++ b/imap_processing/ultra/l1b/ultra_l1b_culling.py @@ -22,7 +22,7 @@ get_scattering_thresholds, ) from imap_processing.ultra.l1b.quality_flag_filters import DE_QUALITY_FLAG_FILTERS -from imap_processing.ultra.l1b.ultra_l1b_extended import get_spin_and_duration +from imap_processing.ultra.l1b.ultra_l1b_extended import get_spin_info from imap_processing.ultra.l1c.l1c_lookup_utils import build_energy_bins logger = logging.getLogger(__name__) @@ -368,7 +368,8 @@ def get_pulses_per_spin(aux: xr.Dataset, rates: xr.Dataset) -> RateResult: coin_pulses : NDArray Total coincidence pulses. """ - spin_number, _duration = get_spin_and_duration(aux, rates["shcoarse"].values) + spin_ds = get_spin_info(aux, rates["shcoarse"].values) + spin_number = spin_ds["spin_number"].values # Top coin pulses top_coin_pulses = np.stack( diff --git a/imap_processing/ultra/l1b/ultra_l1b_extended.py b/imap_processing/ultra/l1b/ultra_l1b_extended.py index 251940a39..758914c92 100644 --- a/imap_processing/ultra/l1b/ultra_l1b_extended.py +++ b/imap_processing/ultra/l1b/ultra_l1b_extended.py @@ -13,6 +13,7 @@ from scipy.interpolate import LinearNDInterpolator, RegularGridInterpolator from imap_processing.quality_flags import ImapDEOutliersUltraFlags +from imap_processing.spice.spin import get_spin_data from imap_processing.spice.time import met_to_ttj2000ns, ttj2000ns_to_et from imap_processing.ultra.constants import UltraConstants from imap_processing.ultra.l1b.lookup_utils import ( @@ -917,7 +918,9 @@ def get_phi_theta( return np.degrees(phi), np.degrees(theta) -def get_spin_start_indices(aux_dataset: xr.Dataset, de_event_met: NDArray) -> NDArray: +def get_spin_start_indices( + aux_dataset: xr.Dataset, de_event_met: NDArray +) -> tuple[NDArray, NDArray]: """ Get the spin start indices in the aux dataset for each event. @@ -932,32 +935,76 @@ def get_spin_start_indices(aux_dataset: xr.Dataset, de_event_met: NDArray) -> ND ------- start_inds : numpy.ndarray Spin start indices for each event. + missing_aux_data_mask : numpy.ndarray + Boolean array indicating where there are events out of the aux data range. The + universal spin table should be used to fill in missing data for these events. """ # Get Spin Start Time in seconds spin_start_sec = aux_dataset["timespinstart"].values - # Check that all events fall within the aux dataset time range. # The time window spans from the first spin start to the end of the last spin. first_spin_start = spin_start_sec[0] # Define the end of the last spin as start time + max duration (15s) last_spin_end = spin_start_sec[-1] + 15.0 - if np.any(de_event_met < first_spin_start) or np.any(de_event_met > last_spin_end): - raise ValueError( + missing_aux_data_mask = (de_event_met < first_spin_start) | ( + de_event_met > last_spin_end + ) + if np.any(missing_aux_data_mask): + logger.info( "Coarse MET time contains events outside aux_dataset time range " f"({first_spin_start} - {last_spin_end}). " - f"Found min={de_event_met.min()}, max={de_event_met.max()}." + f"Found min={de_event_met.min()}, max={de_event_met.max()}. " + f"Found {np.sum(missing_aux_data_mask)} events not covered by aux data. " + f" Trying to fill missing data using universal spin table." ) - # Find the spin_start_sec that started directly before each event. - start_inds = np.searchsorted(spin_start_sec, de_event_met, side="right") - 1 - # Clip to valid range of indices - start_inds = np.clip(start_inds, 0, len(spin_start_sec) - 1) + start_inds = ( + np.searchsorted( + spin_start_sec, de_event_met[~missing_aux_data_mask], side="right" + ) + - 1 + ) - return start_inds + return start_inds, missing_aux_data_mask + + +def get_spin_data_from_universal_spin_table(de_event_met: NDArray) -> pandas.DataFrame: + """ + Get spin data from the universal spin table for each event. + + Parameters + ---------- + de_event_met : numpy.ndarray + Direct event MET. + + Returns + ------- + spin_data_per_event : pandas.DataFrame + Spin information for each event. + """ + # Get the relevant spin parameters for each event + spin_df = get_spin_data() + ut_spin_start = spin_df["spin_start_met"].values + if np.any( + (de_event_met < np.min(ut_spin_start)) | (de_event_met > np.max(ut_spin_start)) + ): + raise ValueError( + "Coarse MET time contains events outside of universal spin table time " + f"range ({np.min(ut_spin_start)} - {np.max(ut_spin_start)}). " + f"Found min={de_event_met.min()}, max={de_event_met.max()}." + ) + ut_start_inds = ( + np.searchsorted(spin_df["spin_start_met"].values, de_event_met, side="right") + - 1 + ) + return spin_df.iloc[ut_start_inds] def get_event_times( - aux_dataset: xr.Dataset, phase_angle: NDArray, de_event_met: NDArray + aux_dataset: xr.Dataset, + de_event_met: NDArray, + phase_angle: NDArray, + spin_ds: xr.Dataset | None = None, ) -> tuple[NDArray, NDArray]: """ Get the event times, spin start times. @@ -970,10 +1017,12 @@ def get_event_times( ---------- aux_dataset : xarray.Dataset Auxiliary dataset containing spin information. - phase_angle : numpy.ndarray - Phase angle. de_event_met : numpy.ndarray Direct event MET. + phase_angle : numpy.ndarray + Phase angle. + spin_ds : xarray.Dataset, optional + Pre-computed spin information. If None, will be computed from aux_dataset. Returns ------- @@ -982,37 +1031,29 @@ def get_event_times( spin_start_times: numpy.ndarray Spin start times in et. """ - # Get Spin Start Time in seconds - spin_start_sec = aux_dataset["timespinstart"].values - # Get Spin Start Subsecond in milliseconds - spin_start_subsec = aux_dataset["timespinstartsub"].values - # Get spin duration in milliseconds - spin_duration = aux_dataset["duration"].values - - start_inds = get_spin_start_indices(aux_dataset, de_event_met) - # Get the relevant spin parameters for each event - evt_spin_starts = spin_start_sec[start_inds] - evt_spin_start_subs = spin_start_subsec[start_inds] - evt_spin_durations = spin_duration[start_inds] + # Get or compute spin info + if spin_ds is None: + spin_ds = get_spin_info(aux_dataset, de_event_met) # spin start with subsecond precision - spin_start_times = evt_spin_starts + (evt_spin_start_subs / 1000.0) + spin_start_times = spin_ds.spin_starts + (spin_ds.spin_start_subs / 1000.0) + # add the fractional spin offset - event_times = spin_start_times + (evt_spin_durations / 1000.0) * ( + event_times = spin_start_times + (spin_ds.spin_duration / 1000.0) * ( phase_angle / 720.0 ) - return ( ttj2000ns_to_et(met_to_ttj2000ns(event_times)), ttj2000ns_to_et(met_to_ttj2000ns(spin_start_times)), ) -def get_spin_and_duration( - aux_dataset: xr.Dataset, de_event_met: NDArray -) -> tuple[NDArray, NDArray]: +def get_spin_info(aux_dataset: xr.Dataset, de_event_met: NDArray) -> xr.Dataset: """ - Get the spin numbers and durations. + Get the spin information for each event. + + The returned dataset contains the spin number, spin duration, + spin start time, and spin start subsecond for each event. Parameters ---------- @@ -1023,19 +1064,40 @@ def get_spin_and_duration( Returns ------- - spin_numbers: numpy.ndarray - Spin numbers for each event. - spin_durations: numpy.ndarray - Spin durations for each event. + spin_info_per_event : xarray.Dataset + Spin information for each event. """ - # Get the spin start indices for each event - start_inds = get_spin_start_indices(aux_dataset, de_event_met) - # Get the spin numbers for each event - spin_numbers = aux_dataset["spinnumber"].values[start_inds] - # Get the spin duration for each event - spin_durations = aux_dataset["duration"].values[start_inds] - - return spin_numbers, spin_durations + start_inds, missing_events = get_spin_start_indices(aux_dataset, de_event_met) + # Initialize spin info dataset + spin_info_per_event = xr.Dataset() + # Create dict of var name lookups + var_names = { + "spin_number": ("spinnumber", "spin_number"), + "spin_duration": ("duration", "spin_period_sec"), + "spin_starts": ("timespinstart", "spin_start_sec_sclk"), + "spin_start_subs": ("timespinstartsub", "spin_start_subsec_sclk"), + } + # If there is not enough aux data covering an event, query the universal + # spin table using the start time to fill in the missing data. + # This can happen for the first event if the aux data starts after the DE data. + spin_data = ( + get_spin_data_from_universal_spin_table(de_event_met[missing_events]) + if np.any(missing_events) + else None + ) + + for var in var_names.keys(): + # Get the corresponding aux and universal table variable names + aux_name, ut_name = var_names[str(var)] + init_array = np.zeros_like(de_event_met) + if np.any(missing_events) and spin_data is not None: + # Get data from universal table for events missing aux data + init_array[missing_events] = spin_data[ut_name].values + # Get data from aux dataset for the rest of the events + init_array[~missing_events] = aux_dataset[aux_name].values[start_inds] + spin_info_per_event[var] = (("epoch",), init_array) + + return spin_info_per_event def interpolate_fwhm( diff --git a/imap_processing/ultra/l1c/helio_pset.py b/imap_processing/ultra/l1c/helio_pset.py index 5f699076a..4fc2fd474 100644 --- a/imap_processing/ultra/l1c/helio_pset.py +++ b/imap_processing/ultra/l1c/helio_pset.py @@ -156,6 +156,7 @@ def calculate_helio_pset( rates_dataset, pixels_below_scattering, boundary_scale_factors, + aux_dataset, pointing_range_met, n_energy_bins=len(energy_bin_geometric_means), sensor_id=sensor_id, diff --git a/imap_processing/ultra/l1c/spacecraft_pset.py b/imap_processing/ultra/l1c/spacecraft_pset.py index 40a2e401c..f80035a3d 100644 --- a/imap_processing/ultra/l1c/spacecraft_pset.py +++ b/imap_processing/ultra/l1c/spacecraft_pset.py @@ -144,6 +144,7 @@ def calculate_spacecraft_pset( rates_dataset, valid_spun_pixels, boundary_scale_factors, + aux_dataset, pointing_range_met, n_energy_bins=len(energy_bin_geometric_means), sensor_id=sensor_id, diff --git a/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py b/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py index b46aba032..7237c27a3 100644 --- a/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +++ b/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py @@ -13,7 +13,6 @@ ) from imap_processing.spice.spin import ( get_spin_data, - get_spin_number, ) from imap_processing.ultra.constants import UltraConstants from imap_processing.ultra.l1b.lookup_utils import ( @@ -27,7 +26,7 @@ from imap_processing.ultra.l1b.ultra_l1b_extended import ( get_efficiency, get_efficiency_interpolator, - get_spin_and_duration, + get_spin_info, ) from imap_processing.ultra.l1c.l1c_lookup_utils import ( build_energy_bins, @@ -267,6 +266,7 @@ def get_sectored_rates(rates_ds: xr.Dataset) -> xr.Dataset | None: def get_deadtime_ratios_by_spin_phase( sectored_rates: xr.Dataset | None, + aux_dataset: xr.Dataset, spin_steps: int, sensor_id: int | None = None, ancillary_files: dict | None = None, @@ -278,6 +278,8 @@ def get_deadtime_ratios_by_spin_phase( ---------- sectored_rates : xarray.Dataset, optional Dataset containing sector mode image rates data. + aux_dataset : xarray.Dataset + Auxiliary dataset containing spin information. spin_steps : int Number of spin phase steps (e.g. 15000 for 1ms resolution). sensor_id : int, optional @@ -309,16 +311,13 @@ def get_deadtime_ratios_by_spin_phase( # Get timestamps at the start of each spin (sector 0) spin_start_indices = np.where(sector_indices == 0)[0] met_time = sectored_rates["shcoarse"].values[spin_start_indices] - spin_data = get_spin_data() - spin_numbers = get_spin_number(met_time) - # Get spin durations for each spin - spin_durations = spin_data.loc[spin_numbers, "spin_period_sec"].values + spin_ds = get_spin_info(aux_dataset, met_time) # Repeat the spin duration for each of the 15 sectors. # Sectors are all within a spin so each one corresponds to the same spin # duration sectored_rates["spin_durations"] = ( "epoch", - np.repeat(spin_durations, num_spin_sectors), + np.repeat(spin_ds.spin_duration.data, num_spin_sectors), ) deadtime_ratios = get_deadtime_ratios(sectored_rates).data # The center spin phase is the closest / most accurate spin phase. @@ -408,6 +407,7 @@ def get_spacecraft_exposure_times( rates_dataset: xr.Dataset, valid_spun_pixels: xr.DataArray, boundary_scale_factors: xr.DataArray, + aux_dataset: xr.Dataset, pointing_range_met: tuple[float, float], n_energy_bins: int, sensor_id: int | None = None, @@ -430,6 +430,8 @@ def get_spacecraft_exposure_times( shape = (spin_phase_steps, 1, n_pix). boundary_scale_factors : xarray.DataArray Boundary scale factors for each pixel at each spin phase. + aux_dataset : xarray.Dataset, + Auxiliary dataset containing spin information. pointing_range_met : tuple Start and stop time of the pointing period in mission elapsed time. n_energy_bins : int @@ -454,7 +456,7 @@ def get_spacecraft_exposure_times( # Get the number of steps used in the spun pointing lookup tables spin_steps = valid_spun_pixels.shape[0] nominal_deadtime_ratios = get_deadtime_ratios_by_spin_phase( - sectored_rates, spin_steps, sensor_id, ancillary_files + sectored_rates, aux_dataset, spin_steps, sensor_id, ancillary_files ) # The exposure time will be approximately the same per spin, so to save # computation time, calculate the exposure time for a single spin and then scale it @@ -687,9 +689,8 @@ def get_spacecraft_background_rates( # Pulses for the pointing. etof_min = get_image_params("eTOFMin", f"ultra{sensor_id}", ancillary_files) etof_max = get_image_params("eTOFMax", f"ultra{sensor_id}", ancillary_files) - spin_number, _ = get_spin_and_duration( - aux_dataset, rates_dataset["shcoarse"].values - ) + spin_ds = get_spin_info(aux_dataset, rates_dataset["shcoarse"].values) + spin_number = spin_ds["spin_number"].values # Get dmin for PH (mm). dmin_ctof = UltraConstants.DMIN_PH_CTOF From cdd665aa6d73f63562606bc5aaf1029bad33978e Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 19 Dec 2025 09:00:03 -0700 Subject: [PATCH 2/3] dtype --- .../ultra/unit/test_ultra_l1b_extended.py | 6 +-- imap_processing/ultra/l1b/de.py | 2 +- .../ultra/l1b/ultra_l1b_extended.py | 45 +++---------------- .../ultra/l1c/ultra_l1c_pset_bins.py | 2 +- 4 files changed, 12 insertions(+), 43 deletions(-) diff --git a/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py b/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py index c7af81f19..0127477c1 100644 --- a/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py +++ b/imap_processing/tests/ultra/unit/test_ultra_l1b_extended.py @@ -524,8 +524,8 @@ def test_get_eventtimes(test_fixture, aux_dataset): event_times, spin_start_times = get_event_times( aux_dataset, - de_dataset["phase_angle"].values, de_dataset["shcoarse"].values, + de_dataset["phase_angle"].values, ) # Check shapes @@ -585,7 +585,7 @@ def test_get_spin_and_duration(test_fixture, aux_dataset): @pytest.mark.external_test_data def test_get_event_times_out_of_range( - test_fixture, aux_dataset, use_fake_spin_data_for_time, caplog + test_fixture, aux_dataset, use_fake_spin_data_for_time ): """Tests get_event_times with out of range values.""" df_filt, _, _, de_dataset = test_fixture @@ -600,8 +600,8 @@ def test_get_event_times_out_of_range( # This should not raise an error. event_times, spin_starts = get_event_times( aux_dataset, - de_dataset["phase_angle"].values, coarse_times, + de_dataset["phase_angle"].values, ) assert event_times.shape == coarse_times.shape assert spin_starts.shape == coarse_times.shape diff --git a/imap_processing/ultra/l1b/de.py b/imap_processing/ultra/l1b/de.py index 38af1bedf..219100a15 100644 --- a/imap_processing/ultra/l1b/de.py +++ b/imap_processing/ultra/l1b/de.py @@ -164,8 +164,8 @@ def calculate_de( (event_times, spin_starts) = get_event_times( aux_dataset, - de_dataset["phase_angle"].data, de_dataset["shcoarse"].data, + de_dataset["phase_angle"].data, spin_ds, ) diff --git a/imap_processing/ultra/l1b/ultra_l1b_extended.py b/imap_processing/ultra/l1b/ultra_l1b_extended.py index 758914c92..1ff755d5e 100644 --- a/imap_processing/ultra/l1b/ultra_l1b_extended.py +++ b/imap_processing/ultra/l1b/ultra_l1b_extended.py @@ -13,7 +13,7 @@ from scipy.interpolate import LinearNDInterpolator, RegularGridInterpolator from imap_processing.quality_flags import ImapDEOutliersUltraFlags -from imap_processing.spice.spin import get_spin_data +from imap_processing.spice.spin import interpolate_spin_data from imap_processing.spice.time import met_to_ttj2000ns, ttj2000ns_to_et from imap_processing.ultra.constants import UltraConstants from imap_processing.ultra.l1b.lookup_utils import ( @@ -968,38 +968,6 @@ def get_spin_start_indices( return start_inds, missing_aux_data_mask -def get_spin_data_from_universal_spin_table(de_event_met: NDArray) -> pandas.DataFrame: - """ - Get spin data from the universal spin table for each event. - - Parameters - ---------- - de_event_met : numpy.ndarray - Direct event MET. - - Returns - ------- - spin_data_per_event : pandas.DataFrame - Spin information for each event. - """ - # Get the relevant spin parameters for each event - spin_df = get_spin_data() - ut_spin_start = spin_df["spin_start_met"].values - if np.any( - (de_event_met < np.min(ut_spin_start)) | (de_event_met > np.max(ut_spin_start)) - ): - raise ValueError( - "Coarse MET time contains events outside of universal spin table time " - f"range ({np.min(ut_spin_start)} - {np.max(ut_spin_start)}). " - f"Found min={de_event_met.min()}, max={de_event_met.max()}." - ) - ut_start_inds = ( - np.searchsorted(spin_df["spin_start_met"].values, de_event_met, side="right") - - 1 - ) - return spin_df.iloc[ut_start_inds] - - def get_event_times( aux_dataset: xr.Dataset, de_event_met: NDArray, @@ -1081,18 +1049,19 @@ def get_spin_info(aux_dataset: xr.Dataset, de_event_met: NDArray) -> xr.Dataset: # spin table using the start time to fill in the missing data. # This can happen for the first event if the aux data starts after the DE data. spin_data = ( - get_spin_data_from_universal_spin_table(de_event_met[missing_events]) + interpolate_spin_data(de_event_met[missing_events]) if np.any(missing_events) else None ) - for var in var_names.keys(): - # Get the corresponding aux and universal table variable names - aux_name, ut_name = var_names[str(var)] - init_array = np.zeros_like(de_event_met) + for var, (aux_name, ut_name) in var_names.items(): + init_array = np.zeros_like(de_event_met, dtype=np.float64) if np.any(missing_events) and spin_data is not None: # Get data from universal table for events missing aux data init_array[missing_events] = spin_data[ut_name].values + if ut_name == "spin_start_subsec_sclk": + # Convert from microseconds to milliseconds to match aux data units + init_array[missing_events] /= 1000.0 # Get data from aux dataset for the rest of the events init_array[~missing_events] = aux_dataset[aux_name].values[start_inds] spin_info_per_event[var] = (("epoch",), init_array) diff --git a/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py b/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py index 7237c27a3..3b15fc99f 100644 --- a/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +++ b/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py @@ -430,7 +430,7 @@ def get_spacecraft_exposure_times( shape = (spin_phase_steps, 1, n_pix). boundary_scale_factors : xarray.DataArray Boundary scale factors for each pixel at each spin phase. - aux_dataset : xarray.Dataset, + aux_dataset : xarray.Dataset Auxiliary dataset containing spin information. pointing_range_met : tuple Start and stop time of the pointing period in mission elapsed time. From 51dc7fe2cafa7691f9f1e8649acd38df5b34cb5e Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 19 Dec 2025 09:05:24 -0700 Subject: [PATCH 3/3] remove redundant log --- imap_processing/ultra/l1c/ultra_l1c_pset_bins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py b/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py index 3b15fc99f..ece5f0f0e 100644 --- a/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +++ b/imap_processing/ultra/l1c/ultra_l1c_pset_bins.py @@ -250,7 +250,6 @@ def get_sectored_rates(rates_ds: xr.Dataset) -> xr.Dataset | None: spin_run_inds = np.where(spin_runs == 15)[0] if len(spin_run_inds) == 0: - logger.warning("No sector mode data found in the rates dataset.") return None # Get the start indices of each sector mode spin