Skip to content

Commit cc226cc

Browse files
committed
FIX: Lo update spin time calculation
- use universal spin table to get true onboard timing The Lo spin packet is not accurate enough or has different time information in it, so we need to reference DE events to the onboard timekeeping (spin table), which the Lo instrument references to reset its counter. - Change formula for DE time tag counter. It is a fixed length pulse of 4.096ms, not varying with spin period. This means we need to multiply each DE tick by that length of time.
1 parent d93f133 commit cc226cc

3 files changed

Lines changed: 108 additions & 130 deletions

File tree

imap_processing/lo/l1b/lo_l1b.py

Lines changed: 24 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
get_pointing_times,
2929
interpolate_repoint_data,
3030
)
31-
from imap_processing.spice.spin import get_spin_data, get_spin_number
31+
from imap_processing.spice.spin import (
32+
get_spin_data,
33+
get_spin_number,
34+
interpolate_spin_data,
35+
)
3236
from imap_processing.spice.time import (
3337
epoch_to_fractional_doy,
3438
et_to_utc,
@@ -166,6 +170,7 @@
166170
"spin_cycle",
167171
]
168172
# -------------------------------------------------------------------
173+
DE_CLOCK_TICK_S = 4.096e-3 # seconds per DE clock tick
169174

170175

171176
def lo_l1b(
@@ -277,13 +282,9 @@ def l1b_de(
277282
avg_spin_durations_per_cycle = get_avg_spin_durations_per_cycle(spin_data)
278283
# set the spin cycle for each direct event
279284
l1b_de = set_spin_cycle(pointing_start_met, l1a_de, l1b_de)
280-
# get spin start times for each event
281-
spin_start_time = get_spin_start_times(l1a_de, l1b_de, spin_data)
282285

283286
# get the absolute met for each event
284-
l1b_de = set_event_met(
285-
l1a_de, l1b_de, spin_start_time, avg_spin_durations_per_cycle
286-
)
287+
l1b_de = set_event_met(l1a_de, l1b_de)
287288
# set the epoch for each event
288289
l1b_de = set_each_event_epoch(l1b_de)
289290
# Set the ESA mode for each direct event
@@ -794,7 +795,7 @@ def _check_sufficient_spins(spin_data: xr.Dataset) -> np.ndarray:
794795

795796

796797
def get_spin_start_times(
797-
l1a_de: xr.Dataset, l1b_de: xr.Dataset, spin_data: xr.Dataset
798+
l1a_de: xr.Dataset,
798799
) -> xr.DataArray:
799800
"""
800801
Get the start time for the spin that each direct event is in.
@@ -807,59 +808,31 @@ def get_spin_start_times(
807808
----------
808809
l1a_de : xr.Dataset
809810
The L1A DE dataset.
810-
l1b_de : xr.Dataset
811-
The L1B DE dataset.
812-
spin_data : xr.Dataset
813-
The L1A Spin dataset.
814811
815812
Returns
816813
-------
817-
spin_start_time : xr.DataArray
814+
spin_start_time : np.ndarray
818815
The start time for the spin that each direct event is in.
819816
"""
820-
# align l1a_de packets with spin_data packets
821-
de_to_spin_indices = match_science_to_spin_asc(
822-
l1a_de["epoch"].values, spin_data["epoch"].values
823-
)
824-
# Repeat this for each direct event based on the de_count
825-
de_to_spin_indices = np.repeat(de_to_spin_indices, l1a_de["de_count"])
826-
827-
# There are 28 spins per epoch (1 aggregated science cycle)
828-
# Set the spin_cycle_num to the spin number relative to the
829-
# start of the ASC
830-
spin_cycle_num = l1b_de["spin_cycle"] % 28
831-
asc_starts = spin_data["acq_start_sec"] + spin_data["acq_start_subsec"] * 1e-6
832-
avg_spin_durations = get_avg_spin_durations_per_cycle(spin_data)
817+
# Get the actual spin start times from the spin data
818+
# Use the individual spin start times rather than calculating from ASC averages
819+
spin_start_times = interpolate_spin_data(l1a_de["shcoarse"].values)[
820+
"spin_start_met"
821+
].values
833822

834-
# Calculate the time based off of the start of the acquisition period
835-
# then using an average spin duration to calculate the offset within the ASC
836-
# NOTE: We don't want to use the spin start times directly from the ASC spin packet
837-
# because we are using an average spin_cycle for the ASC and there are
838-
# times when only half the spins were completed in an ASC. This allows us
839-
# to still get a valid spin_cycle start time for each direct event, even
840-
# if the average spin_cycle was after the end of the acquisition period.
841-
# TODO: Can we do even better by knowing how many esa_steps and spins were complete?
842-
# i.e. change the spin_cycle calculation
843-
spin_start_time = (
844-
asc_starts[de_to_spin_indices]
845-
+ spin_cycle_num * avg_spin_durations[de_to_spin_indices]
846-
)
847-
return spin_start_time
823+
return spin_start_times
848824

849825

850826
def set_event_met(
851827
l1a_de: xr.Dataset,
852828
l1b_de: xr.Dataset,
853-
spin_start_time: xr.DataArray,
854-
avg_spin_durations: xr.DataArray,
855829
) -> xr.Dataset:
856830
"""
857831
Get the event MET for each direct event.
858832
859833
Each direct event is converted from a data number to engineering unit in seconds.
860-
de_eu_time de_dn_time / 4096 * avg_spin_duration
861-
where de_time is the direct event time Data Number (DN) and avg_spin_duration
862-
is the average spin duration for the ASC that the event was measured in.
834+
time_from_start_of_spin = de_time * DE_CLOCK_TICK_S
835+
where de_time is the direct event time Data Number (DN).
863836
864837
The direct event time is the time of direct event relative to the start of the spin.
865838
The event MET is the sum of the start time of the spin and the
@@ -871,26 +844,18 @@ def set_event_met(
871844
The L1A DE dataset.
872845
l1b_de : xr.Dataset
873846
The L1B DE dataset.
874-
spin_start_time : np.ndarray
875-
The start time for the spin that each direct event is in.
876-
avg_spin_durations : xr.DataArray
877-
The average spin duration for each epoch.
878847
879848
Returns
880849
-------
881850
l1b_de : xr.Dataset
882851
The L1B DE dataset with the event MET.
883852
"""
884-
counts = l1a_de["de_count"].values
885-
de_time_asc_groups = np.split(l1a_de["de_time"].values, np.cumsum(counts)[:-1])
886-
de_times_eu = []
887-
for i, de_time_asc in enumerate(de_time_asc_groups):
888-
# DE Time is 12 bit DN. The max possible value is 4095
889-
# divide by 4096 to get fraction of a spin duration
890-
de_times_eu.extend(de_time_asc / 4096 * avg_spin_durations[i].values)
853+
# get spin start times for each event
854+
spin_start_times = get_spin_start_times(l1a_de)
891855

856+
# spin start + offset based on de_time ticks
892857
l1b_de["event_met"] = xr.DataArray(
893-
spin_start_time + de_times_eu,
858+
spin_start_times + l1a_de["de_time"].values * DE_CLOCK_TICK_S,
894859
dims=["epoch"],
895860
# attrs=attr_mgr.get_variable_attributes("epoch")
896861
)
@@ -1344,12 +1309,14 @@ def set_pointing_bin(l1b_de: xr.Dataset) -> xr.Dataset:
13441309
# first column: radius (Not needed)
13451310
# second column: longitude
13461311
lons = direction[:, 1]
1312+
# shift to 0-360 range (spin-phase 0 should be in bin 0)
1313+
lons = (lons + 360) % 360
13471314
# third column: latitude
13481315
lats = direction[:, 2]
13491316

13501317
# Define bin edges
13511318
# 3600 bins, 0.1° each
1352-
lon_bins = np.linspace(-180, 180, 3601)
1319+
lon_bins = np.linspace(0, 360, 3601)
13531320
# 40 bins, 0.1° each
13541321
lat_bins = np.linspace(-2, 2, 41)
13551322

imap_processing/tests/lo/test_lo_l1b.py

Lines changed: 77 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
1212
from imap_processing.cdf.utils import load_cdf
1313
from imap_processing.lo.l1b.lo_l1b import (
14+
DE_CLOCK_TICK_S,
1415
calculate_de_rates,
1516
calculate_histogram_rates,
1617
calculate_star_sensor_profile_for_group,
@@ -219,7 +220,9 @@ def l1a_hist():
219220
"imap_processing.lo.l1b.lo_l1b.cartesian_to_latitudinal",
220221
return_value=np.zeros((2000, 3)),
221222
)
223+
@patch("imap_processing.lo.l1b.lo_l1b.interpolate_spin_data")
222224
def test_lo_l1b_de(
225+
mock_interpolate_spin_data,
223226
mock_frame_transform,
224227
mock_lo_instrument_pointing,
225228
mocked_get_pointing_times,
@@ -229,6 +232,15 @@ def test_lo_l1b_de(
229232
anc_dependencies,
230233
):
231234
# Arrange
235+
# Mock the spin data to provide spin start times
236+
# Create a DataFrame covering the time range of the test data
237+
num_events = 2000
238+
mock_spin_df = pd.DataFrame(
239+
{
240+
"spin_start_met": np.ones(num_events),
241+
}
242+
)
243+
mock_interpolate_spin_data.return_value = mock_spin_df
232244

233245
# Add l1b_nhk dependency with pivot angle information
234246
l1b_nhk = xr.Dataset(
@@ -490,94 +502,82 @@ def test_spin_cycle(mock_get_spin_number):
490502
np.testing.assert_array_equal(spin_cycle_data["spin_cycle"], spin_cycle_expected)
491503

492504

493-
def test_get_spin_start_times():
505+
@patch("imap_processing.lo.l1b.lo_l1b.interpolate_spin_data")
506+
def test_get_spin_start_times(mock_interpolate_spin_data):
494507
# Arrange
495-
l1b_de = xr.Dataset(
508+
# Mock the spin data to return specific spin start times
509+
mock_spin_df = pd.DataFrame(
496510
{
497-
"spin_cycle": ("epoch", [0, 1, 2, 3, 4]),
498-
},
499-
coords={
500-
"epoch": [
501-
0,
502-
1,
503-
2,
504-
3,
505-
4,
506-
]
507-
},
511+
"spin_start_met": [10.5, 10.5, 30.1, 30.1, 30.1],
512+
}
508513
)
514+
mock_interpolate_spin_data.return_value = mock_spin_df
515+
509516
l1a_de = xr.Dataset(
510517
{
511-
"shcoarse": ("epoch", [0, 1]),
518+
"shcoarse": ("epoch", [15, 35]),
512519
"de_count": ("epoch", [2, 3]),
513-
"met": ("epoch", [0, 1]), # MET per time epoch, not per direct event
514-
"de_time": ("direct_event", [0000, 1000, 2000, 3000, 4000]),
520+
"de_time": ("direct_event", [0, 1000, 2000, 3000, 4000]),
515521
},
516522
coords={"epoch": [0, 1], "direct_event": [0, 1, 2, 3, 4]},
517523
)
518-
spin = xr.Dataset(
519-
{
520-
"shcoarse": ("epoch", [0, 1]),
521-
"acq_start_sec": (
522-
"epoch",
523-
[20, 25],
524-
),
525-
"acq_start_subsec": (
526-
"epoch",
527-
[0, 0],
528-
),
529-
"acq_end_sec": (
530-
"epoch",
531-
[25, 30],
532-
),
533-
"acq_end_subsec": (
534-
"epoch",
535-
[0, 0],
536-
),
537-
"num_completed": (
538-
"epoch",
539-
[28, 14],
540-
),
541-
}
542-
)
543524

525+
# Expected: shcoarse 15 should match spin at index 0 (10 < 15 < 20)
526+
# shcoarse 35 should match spin at index 2 (30 < 35 < 40)
527+
# Repeated by de_count: [2, 3] -> [index0, index0, index2, index2, index2]
544528
spin_start_times_expected = np.array(
545-
[20, 20 + 5 / 28, 25 + 5 * 2 / 14, 25 + 5 * 3 / 14, 25 + 5 * 4 / 14]
529+
[10.5, 10.5, 30.1, 30.1, 30.1] # 10 + 0.5e6*1e-6 # 30 + 0.1e6*1e-6
546530
)
547-
spin_start_times = get_spin_start_times(l1a_de, l1b_de, spin)
548531

532+
# Act
533+
spin_start_times = get_spin_start_times(l1a_de)
534+
535+
# Assert
549536
np.testing.assert_allclose(
550537
spin_start_times,
551538
spin_start_times_expected,
552539
atol=1e-4,
553540
)
554541

555542

556-
def test_set_event_met():
543+
@patch("imap_processing.lo.l1b.lo_l1b.interpolate_spin_data")
544+
def test_set_event_met(mock_interpolate_spin_data):
557545
# Arrange
546+
# Mock the spin data
547+
mock_spin_df = pd.DataFrame(
548+
{
549+
"spin_start_met": [10, 10, 30, 30, 30],
550+
}
551+
)
552+
mock_interpolate_spin_data.return_value = mock_spin_df
553+
558554
l1b_de = xr.Dataset()
559555
l1a_de = xr.Dataset(
560556
{
557+
"shcoarse": ("epoch", [15, 35]),
561558
"de_count": ("epoch", [2, 3]),
562-
"de_time": ("direct_event", [0000, 1000, 2000, 3000, 4000]),
559+
"de_time": ("direct_event", [0, 1000, 2000, 3000, 4000]),
563560
},
564561
coords={
565562
"epoch": [0, 1],
566-
"direct_event": [
567-
0,
568-
1,
569-
2,
570-
3,
571-
4,
572-
],
563+
"direct_event": [0, 1, 2, 3, 4],
573564
},
574565
)
575-
avg_spin_durations = xr.DataArray([5, 10])
576-
spin_start_times = xr.DataArray([10, 20, 30, 40, 50])
577-
expected_event_met = np.array([10, 21.2207, 34.8828, 47.3242, 59.7656])
566+
567+
# shcoarse 15 -> spin_start 10, shcoarse 35 -> spin_start 30
568+
# event_met = spin_start + de_time * DE_CLOCK_TICK_S
569+
expected_event_met = np.array(
570+
[
571+
10 + 0 * DE_CLOCK_TICK_S, # 10.0
572+
10 + 1000 * DE_CLOCK_TICK_S, # 14.096
573+
30 + 2000 * DE_CLOCK_TICK_S, # 38.192
574+
30 + 3000 * DE_CLOCK_TICK_S, # 42.288
575+
30 + 4000 * DE_CLOCK_TICK_S, # 46.384
576+
]
577+
)
578578

579579
# Act
580-
l1b_de = set_event_met(l1a_de, l1b_de, spin_start_times, avg_spin_durations)
580+
l1b_de = set_event_met(l1a_de, l1b_de)
581581

582582
# Assert
583583
np.testing.assert_allclose(
@@ -586,24 +586,25 @@ def test_set_event_met():
586586
atol=1e-4,
587587
)
588588

589-
def test_set_each_event_epoch():
590-
l1b_de = xr.Dataset(
591-
{
592-
"event_met": ("epoch", [10, 20, 30, 40, 50]),
593-
},
594-
coords={
595-
"epoch": [0, 1, 2, 3, 4],
596-
},
597-
)
598-
epoch_expected = met_to_ttj2000ns(np.array([10, 20, 30, 40, 50]))
599589

600-
l1b_de = set_each_event_epoch(l1b_de)
590+
def test_set_each_event_epoch():
591+
l1b_de = xr.Dataset(
592+
{
593+
"event_met": ("epoch", [10, 20, 30, 40, 50]),
594+
},
595+
coords={
596+
"epoch": [0, 1, 2, 3, 4],
597+
},
598+
)
599+
epoch_expected = met_to_ttj2000ns(np.array([10, 20, 30, 40, 50]))
601600

602-
np.testing.assert_allclose(
603-
l1b_de["epoch"].values,
604-
epoch_expected,
605-
atol=1e-4,
606-
)
601+
l1b_de = set_each_event_epoch(l1b_de)
602+
603+
np.testing.assert_allclose(
604+
l1b_de["epoch"].values,
605+
epoch_expected,
606+
atol=1e-4,
607+
)
607608

608609

609610
def test_set_avg_spin_durations_per_event():
@@ -838,6 +839,8 @@ def test_set_direction(mock_lo_instrument_pointing, imap_ena_sim_metakernel):
838839
)
839840
@patch(
840841
"imap_processing.lo.l1b.lo_l1b.cartesian_to_latitudinal",
842+
# Longitudes: -180 -> 180, 0 -> 0, 90 -> 90, 180 -> 180
843+
# After shift to 0-360: 180, 0, 90, 180
841844
return_value=np.array([[0, -180, -2], [0, 0, 0], [0, 90, 1], [0, 180, 2]]),
842845
)
843846
def test_pointing_bins(mock_cartesian_to_latitudinal, mock_frame_transform):
@@ -859,7 +862,8 @@ def test_pointing_bins(mock_cartesian_to_latitudinal, mock_frame_transform):
859862
)
860863

861864
expected_pointing_lats = np.array([0, 20, 30, 40])
862-
expected_pointing_lons = np.array([0, 1800, 2700, 3600])
865+
# Longitude bins are now in 0-360 range after the shift
866+
expected_pointing_lons = np.array([1800, 0, 900, 1800])
863867

864868
# Act
865869
l1b_de = set_pointing_bin(l1b_de)

0 commit comments

Comments
 (0)