Skip to content

Commit 3d6feab

Browse files
authored
IDEX l1b Trigger origin, level, and mode fix (#2828)
* sort of changes * things working sort of * more investigation * triggertime * matchign * idex l1b trigger mode/origin/level fix * remove code * add new validation file to the external data download config * trigger origin update * update code * pr feedback fix comments
1 parent c48556e commit 3d6feab

8 files changed

Lines changed: 273 additions & 105 deletions

File tree

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
(r"py:.*", r".*np.ndarray.*"),
123123
(r"py:.*", r".*numpy._typing._array_like._ScalarType_co.*"),
124124
(r"py:.*", r".*idex.l1a.TRIGGER_DESCRIPTION.*"),
125+
(r"py:.*", r".*idex.l1b.TriggerOrigin.*"),
125126
(r"py:.*", r".*idex.l2a.BaselineNoiseTime.*"),
126127
(r"py:.*", r".*PacketProperties"),
127128
(r"py:.*", r".*.spice.geometry.SpiceBody.*"),

imap_processing/cdf/config/imap_idex_l1b_variable_attrs.yaml

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,41 @@ spice_base: &spice_base
5353
VALIDMAX: *spice_data_max
5454

5555
# <=== Instrument Setting Attributes ===>
56-
trigger_mode:
56+
trigger_mode_lg:
5757
<<: *string_base
58-
FIELDNAM: Trigger Mode
59-
CATDESC: Channel and mode that triggered the event
58+
FIELDNAM: Low Gain Trigger Mode
59+
CATDESC: Low Gain Trigger Mode.
6060

61-
trigger_level:
61+
trigger_level_lg:
6262
<<: *trigger_base
63-
FIELDNAM: Trigger Level
64-
CATDESC: Threshold signal level that triggered the event
63+
FIELDNAM: Low Gain Trigger Level
64+
CATDESC: Low Gain Trigger Level threshold.
65+
66+
trigger_mode_mg:
67+
<<: *string_base
68+
FIELDNAM: Mid Gain Trigger Mode
69+
CATDESC: Mid Gain Trigger Mode.
70+
71+
trigger_level_mg:
72+
<<: *trigger_base
73+
FIELDNAM: Mid Gain Trigger Level
74+
CATDESC: Mid Gain Trigger level threshold.
75+
76+
77+
trigger_mode_hg:
78+
<<: *string_base
79+
FIELDNAM: High Gain Trigger Mode
80+
CATDESC: High Gain Trigger Mode.
81+
82+
trigger_level_hg:
83+
<<: *trigger_base
84+
FIELDNAM: High Trigger Level
85+
CATDESC: High Trigger Level threshold.
86+
87+
trigger_origin:
88+
<<: *string_base
89+
FIELDNAM: Trigger Origin
90+
CATDESC: Trigger Origin of the event.
6591

6692
tof_high:
6793
<<: *l1b_tof_base

imap_processing/idex/idex_l1a.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -458,14 +458,36 @@ def _set_sample_trigger_times(
458458
"""
459459
# Retrieve the number of samples for high gain delay
460460

461-
# packet['IDX__TXHDRSAMPDELAY'] is a 32-bit value, with the last 10 bits
462-
# representing the high gain sample delay and the first 2 bits used for padding.
463-
# To extract the high gain bits, the bitwise right shift (>> 20) moves the bits
464-
# 20 positions to the right, and the mask (0b1111111111) keeps only the least
465-
# significant 10 bits.
466-
# TODO use the delay corresponding to the trigger
467-
high_gain_delay = (packet["IDX__TXHDRSAMPDELAY"] >> 22) & 0b1111111111
461+
# packet['IDX__TXHDRSAMPDELAY'] is a 32-bit value:
462+
# bits0-9: high-gain delay,
463+
# bits10-19: mid-gain delay,
464+
# bits20-29: low-gain delay.
465+
# bits30-31 are padding/reserved.
466+
# Each delay is extracted by right-shifting to align the field,
467+
# then masking with #0b1111111111 (10 bits).
468+
468469
n_blocks = packet["IDX__TXHDRBLOCKS"]
470+
trigger_item = packet["IDX__TXHDRTRIGID"]
471+
472+
tof_delay = packet["IDX__TXHDRSAMPDELAY"] # last two bits are padding
473+
474+
# mask to extract 10-bit values
475+
tof_mask = 0b1111111111
476+
477+
# Determine the delay based on the trigger id.
478+
hg_delay = tof_delay & tof_mask # first 10 bits (0-9)
479+
mg_delay = (tof_delay >> 10) & tof_mask # next 10 bits (10-19)
480+
lg_delay = (tof_delay >> 20) & tof_mask # next 10 bits (20-29)
481+
482+
u10 = trigger_item & 0x3FF
483+
if (u10 >> 0) & 1:
484+
delay = hg_delay
485+
elif (u10 >> 1) & 1:
486+
delay = lg_delay
487+
elif (u10 >> 2) & 1:
488+
delay = mg_delay
489+
else:
490+
delay = hg_delay
469491

470492
# Retrieve number of low/high sample pre-trigger blocks
471493

@@ -485,11 +507,10 @@ def _set_sample_trigger_times(
485507
* (num_low_sample_pretrigger_blocks + 1)
486508
* self.NUMBER_SAMPLES_PER_LOW_SAMPLE_BLOCK
487509
)
488-
self.high_sample_trigger_time = (
489-
self.HIGH_SAMPLE_RATE
490-
* (num_high_sample_pretrigger_blocks + 1)
491-
* self.NUMBER_SAMPLES_PER_HIGH_SAMPLE_BLOCK
492-
- self.HIGH_SAMPLE_RATE * high_gain_delay
510+
self.high_sample_trigger_time = self.HIGH_SAMPLE_RATE * (
511+
num_high_sample_pretrigger_blocks + 1
512+
) * self.NUMBER_SAMPLES_PER_HIGH_SAMPLE_BLOCK - self.HIGH_SAMPLE_RATE * (
513+
delay - 1
493514
)
494515

495516
def _parse_high_sample_waveform(self, waveform_raw: str) -> list[int]:
@@ -563,7 +584,7 @@ def _calc_low_sample_resolution(self, num_samples: int) -> npt.NDArray:
563584
time_low_sample_rate_data : numpy.ndarray
564585
Low time sample data array.
565586
"""
566-
time_low_sample_rate_init = np.linspace(0, num_samples, num_samples)
587+
time_low_sample_rate_init = np.arange(num_samples, dtype=np.float64)
567588
time_low_sample_rate_data = (
568589
self.LOW_SAMPLE_RATE * time_low_sample_rate_init
569590
- self.low_sample_trigger_time
@@ -590,7 +611,7 @@ def _calc_high_sample_resolution(self, num_samples: int) -> npt.NDArray:
590611
time_high_sample_rate_data : numpy.ndarray
591612
High sample time data array.
592613
"""
593-
time_high_sample_rate_init = np.linspace(0, num_samples, num_samples)
614+
time_high_sample_rate_init = np.arange(num_samples, dtype=np.float64)
594615
time_high_sample_rate_data = (
595616
self.HIGH_SAMPLE_RATE * time_high_sample_rate_init
596617
- self.high_sample_trigger_time

imap_processing/idex/idex_l1b.py

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
"""
1616

1717
import logging
18-
from enum import Enum
18+
from enum import Enum, IntEnum
1919

20+
import numpy as np
2021
import pandas as pd
2122
import xarray as xr
23+
from numpy.typing import NDArray
24+
from xarray import DataArray
2225

2326
from imap_processing import imap_module_directory
2427
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
@@ -42,6 +45,27 @@
4245
logger = logging.getLogger(__name__)
4346

4447

48+
class TriggerOrigin(IntEnum):
49+
"""Enum class for event trigger origins."""
50+
51+
HS_ADC0I_TOF_HG = 0
52+
HS_ADC0Q_TOF_LG = 1
53+
HS_ADC1Q_TOF_MG = 2
54+
LS_ADC1_TARGET_HG = 3
55+
SW_TRIGGER = 4
56+
EXTERNAL_TRIGGER = 5
57+
58+
59+
TRIGGER_LABELS = {
60+
TriggerOrigin.HS_ADC0I_TOF_HG: "HS ADC0I trigger (TOF HG)",
61+
TriggerOrigin.HS_ADC0Q_TOF_LG: "HS ADC0Q trigger (TOF LG)",
62+
TriggerOrigin.HS_ADC1Q_TOF_MG: "HS ADC1Q trigger (TOF MG)",
63+
TriggerOrigin.LS_ADC1_TARGET_HG: "LS ADC1 trigger (Target HG / low range)",
64+
TriggerOrigin.SW_TRIGGER: "SW trigger",
65+
TriggerOrigin.EXTERNAL_TRIGGER: "external trigger",
66+
}
67+
68+
4569
class TriggerMode(Enum):
4670
"""
4771
Enum class for data collection trigger Modes.
@@ -117,21 +141,21 @@ def idex_l1b(l1a_dataset: xr.Dataset) -> xr.Dataset:
117141
# used for calculations yet but are saved in the CDF for reference.
118142
spice_data = get_spice_data(l1a_dataset, idex_attrs)
119143

120-
trigger_settings = get_trigger_mode_and_level(l1a_dataset)
121-
if trigger_settings:
122-
trigger_settings["triggerlevel"].attrs = idex_attrs.get_variable_attributes(
123-
"trigger_level"
124-
)
125-
trigger_settings["triggermode"].attrs = idex_attrs.get_variable_attributes(
126-
"trigger_mode"
127-
)
128-
144+
trigger_settings = get_trigger_mode_and_level(l1a_dataset, idex_attrs)
145+
trigger_origin = get_trigger_origin(
146+
l1a_dataset["idx__txhdrtrigid"].data, idex_attrs
147+
)
129148
# Create l1b Dataset
130149
prefixes = ["shcoarse", "shfine", "time_high_sample", "time_low_sample"]
131-
data_vars = processed_vars | waveforms_converted | trigger_settings | spice_data
150+
data_vars = (
151+
processed_vars
152+
| waveforms_converted
153+
| trigger_settings
154+
| spice_data
155+
| trigger_origin
156+
)
132157
l1b_dataset = setup_dataset(l1a_dataset, prefixes, idex_attrs, data_vars)
133158
l1b_dataset.attrs = idex_attrs.get_global_attributes("imap_idex_l1b_sci")
134-
135159
# Convert variables
136160
l1b_dataset = convert_raw_to_eu(
137161
l1b_dataset,
@@ -225,6 +249,7 @@ def convert_waveforms(
225249

226250
def get_trigger_mode_and_level(
227251
l1a_dataset: xr.Dataset,
252+
idex_attrs: ImapCdfAttributes,
228253
) -> dict[str, xr.DataArray] | dict:
229254
"""
230255
Determine the trigger mode and threshold level for each event.
@@ -233,6 +258,8 @@ def get_trigger_mode_and_level(
233258
----------
234259
l1a_dataset : xarray.Dataset
235260
IDEX L1a dataset containing the six waveform arrays and instrument settings.
261+
idex_attrs : ImapCdfAttributes
262+
CDF attribute manager object.
236263
237264
Returns
238265
-------
@@ -243,8 +270,8 @@ def get_trigger_mode_and_level(
243270
channels = ["lg", "mg", "hg"]
244271
# 10 bit mask
245272
mask = 0b1111111111
246-
trigger_modes = []
247-
trigger_levels = []
273+
# Initialize a dict to hold the mode labels and threshold levels for each channel
274+
data_dict = {}
248275

249276
def compute_trigger_values(
250277
trigger_mode: int, trigger_controls: int, gain_channel: str
@@ -302,28 +329,63 @@ def compute_trigger_values(
302329
vectorize=True,
303330
output_dtypes=[object, float],
304331
)
305-
trigger_modes.append(mode_array.rename("trigger_mode"))
306-
trigger_levels.append(level_array.rename("trigger_level"))
307-
308-
try:
309332
# There should be an array of modes and threshold levels for each channel.
310-
# At each index (event) only one of the three arrays should have a value that is
311-
# not 'None' because each event can only be triggered by one channel.
312-
# By merging the three arrays, we get value for each event.
313-
merged_modes = xr.merge([trigger_modes[0], xr.merge(trigger_modes[1:])])
314-
merged_levels = xr.merge([trigger_levels[0], xr.merge(trigger_levels[1:])])
315-
316-
return {
317-
"triggermode": merged_modes.trigger_mode,
318-
"triggerlevel": merged_levels.trigger_level,
319-
}
320-
321-
except xr.MergeError as e:
322-
raise ValueError(
323-
f"Only one channel can trigger a dust event. Please make sure "
324-
f"there is only one valid trigger value per event. This "
325-
f"caused Merge Error: {e}"
326-
) from e
333+
# write each of them out as separate variables because there may be
334+
# multiple channels that can trigger an event. The trigger origin variable
335+
# can be used to determine which channel(s) triggered the event.
336+
mode_array.attrs = idex_attrs.get_variable_attributes(f"trigger_mode_{channel}")
337+
data_dict[f"trigger_mode_{channel}"] = mode_array
338+
level_array.attrs = idex_attrs.get_variable_attributes(
339+
f"trigger_level_{channel}"
340+
)
341+
data_dict[f"trigger_level_{channel}"] = level_array
342+
343+
return data_dict
344+
345+
346+
def get_trigger_origin(
347+
trigger_id: NDArray, idex_attrs: ImapCdfAttributes
348+
) -> dict[str, DataArray]:
349+
"""
350+
Determine the trigger origin for each event.
351+
352+
Parameters
353+
----------
354+
trigger_id : numpy.ndarray
355+
Array of raw trigger ID values from the l1a dataset. The trigger ID is a 32-bit
356+
integer where the lower 10 bits contain information about the trigger origin.
357+
idex_attrs : ImapCdfAttributes
358+
CDF attribute manager object.
359+
360+
Returns
361+
-------
362+
dict[str, xarray.DataArray]
363+
A dictionary containing the trigger_origin DataArray with the trigger
364+
origin info for each event.
365+
"""
366+
# extract the lower 10 bits of the trigger ID to get the trigger origin information
367+
trigger_bits = trigger_id & 0x3FF
368+
# For each event, determine which bits are set and get the corresponding trigger
369+
# origin labels
370+
origin_labels = np.array(
371+
[
372+
", ".join(
373+
[TRIGGER_LABELS[TriggerOrigin(i)] for i in range(6) if (bits >> i) & 1]
374+
)
375+
for bits in trigger_bits
376+
],
377+
dtype=object,
378+
)
379+
# Update any events with no trigger bits set to "unknown trigger origin"
380+
origin_labels[origin_labels == ""] = "Unknown trigger origin"
381+
return {
382+
"trigger_origin": xr.DataArray(
383+
name="trigger_origin",
384+
data=np.squeeze(origin_labels),
385+
dims="epoch",
386+
attrs=idex_attrs.get_variable_attributes("trigger_origin"),
387+
)
388+
}
327389

328390

329391
def get_spice_data(

imap_processing/tests/external_test_data_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@
134134

135135
# IDEX
136136
("idex_l1a_validation_file.h5", "idex/test_data/"),
137-
("idex_l1b_validation_file.h5", "idex/test_data/"),
137+
("imap_idex_l1b_sci_20231218_v001.h5", "idex/test_data/"),
138138
("imap_idex_l2a-calibration-curve-yield-params_20250101_v001.csv", "idex/test_data/"),
139139
("imap_idex_l2a-calibration-curve-t-rise_20250101_v001.csv", "idex/test_data/"),
140140

imap_processing/tests/idex/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
TEST_L0_FILE_CATLST = TEST_DATA_PATH / "imap_idex_l0_raw_20241206_v001.pkts" # 1419
1818

1919
L1A_EXAMPLE_FILE = TEST_DATA_PATH / "idex_l1a_validation_file.h5"
20-
L1B_EXAMPLE_FILE = TEST_DATA_PATH / "idex_l1b_validation_file.h5"
20+
L1B_EXAMPLE_FILE = TEST_DATA_PATH / "imap_idex_l1b_sci_20231218_v001.h5"
2121

2222
L2A_CDF = TEST_DATA_PATH / "imap_idex_l2a_sci-1week_20251017_v001.cdf"
2323
L1B_EVT_CDF = TEST_DATA_PATH / "imap_idex_l1b_evt_20250108_v001.cdf"

imap_processing/tests/idex/test_idex_l1a.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,15 @@ def test_validate_l1a_idex_data_variables(
160160
# The Engineering data is converting to UTC, and the SDC is converting to J2000,
161161
# for 'epoch' and 'Timestamp' so this test is using the raw time value 'SCHOARSE' to
162162
# validate time
163-
arrays_to_skip = ["Timestamp", "Epoch", "event"]
163+
# TODO remove the low and high time from this list after the IDEX team produces a
164+
# new l1a h5 file.
165+
arrays_to_skip = [
166+
"Timestamp",
167+
"Epoch",
168+
"event",
169+
"Time (high sampling)",
170+
"Time (low sampling)",
171+
]
164172

165173
# loop through all keys from the l1a example dict
166174
for var in l1a_example_data.variables:

0 commit comments

Comments
 (0)