Skip to content

Commit f85aa17

Browse files
authored
ULTRA extended spin update variables to be ISTP compliant (#2826)
* updates to variables * ISTP compliant arrays * istp compliant arrays * pr comments * pr comments and logger statements * fixes from running * remove print statements * remove print
1 parent 698048a commit f85aa17

10 files changed

Lines changed: 120 additions & 37 deletions

File tree

imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ quality_scattering:
526526
LABLAXIS: quality_scattering
527527
# TODO: come back to format
528528
UNITS: " "
529+
DEPEND_0: spin_number
529530

530531
quality_low_voltage:
531532
<<: *default_uint16
@@ -534,6 +535,7 @@ quality_low_voltage:
534535
LABLAXIS: quality_low_voltage
535536
# TODO: come back to format
536537
UNITS: " "
538+
DEPEND_0: spin_number
537539

538540
quality_high_energy:
539541
<<: *default_uint16
@@ -542,6 +544,7 @@ quality_high_energy:
542544
LABLAXIS: quality_high_energy
543545
# TODO: come back to format
544546
UNITS: " "
547+
DEPEND_0: spin_number
545548

546549
quality_statistics:
547550
<<: *default_uint16
@@ -550,22 +553,30 @@ quality_statistics:
550553
LABLAXIS: quality_statistics
551554
# TODO: come back to format
552555
UNITS: " "
553-
556+
DEPEND_0: spin_number
554557

555558
energy_range_edges:
556-
<<: *default_float64
557-
CATDESC: Energy range edges for culling data at l1b.
559+
CATDESC: Energy range edges used for culling data.
558560
DISPLAY_TYPE: no_plot
559561
FIELDNAM: Energy Range Edges
562+
FILLVAL: -1.0e+31
563+
FORMAT: F12.4
560564
LABLAXIS: Energy Edges
561565
UNITS: keV
562566
VALIDMIN: 0.0
563-
VALIDMAX: 9223372036854775807
567+
VALIDMAX: 5000
568+
VAR_TYPE: support_data
569+
DEPEND_0: energy_range_edges_dim
564570

565571
energy_range_flags:
566-
<<: *default_float64
567-
CATDESC: Bit flags for each culling energy range
572+
CATDESC: Bit flags describing culling energy ranges.
568573
DISPLAY_TYPE: no_plot
569574
FIELDNAM: Energy Range Flags
575+
FILLVAL: 0
576+
FORMAT: I5
570577
LABLAXIS: Range Flags
571-
UNITS: " "
578+
UNITS: " "
579+
VALIDMIN: 1
580+
VALIDMAX: 65534
581+
VAR_TYPE: support_data
582+
DEPEND_0: energy_range_flags_dim

imap_processing/tests/ultra/unit/conftest.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import xarray as xr
99

1010
from imap_processing import imap_module_directory
11+
from imap_processing.ultra.constants import UltraConstants
1112
from imap_processing.ultra.l0.decom_ultra import (
1213
process_ultra_cmd_echo,
1314
process_ultra_energy_rates,
@@ -655,6 +656,15 @@ def mock_goodtimes_dataset():
655656
intervals, _, _ = build_energy_bins()
656657
energy_ranges = get_binned_energy_ranges(intervals)
657658
energy_flags = get_energy_range_flags(energy_ranges)
659+
660+
energy_flags_padded = np.zeros(UltraConstants.MAX_ENERGY_RANGES, dtype=np.uint16)
661+
energy_flags_padded[: len(energy_flags)] = energy_flags
662+
663+
energy_ranges_padded = np.full(
664+
UltraConstants.MAX_ENERGY_RANGE_EDGES, -1.0e31, dtype=np.float32
665+
)
666+
energy_ranges_padded[: len(energy_ranges)] = energy_ranges
667+
658668
nspins = 100
659669
flags = 2 ** np.arange(9)
660670
quality = np.zeros(nspins, dtype=np.uint16)
@@ -664,11 +674,11 @@ def mock_goodtimes_dataset():
664674
return xr.Dataset(
665675
{
666676
"spin_number": ("epoch", np.zeros(nspins)),
667-
"energy_range_flags": ("energy_flags", energy_flags),
677+
"energy_range_flags": ("energy_flags", energy_flags_padded),
668678
"quality_low_voltage": ("spin_number", quality),
669679
"quality_high_energy": ("spin_number", np.zeros(nspins, dtype=np.uint16)),
670680
"quality_statistics": ("spin_number", np.zeros(nspins, dtype=np.uint16)),
671-
"energy_range_edges": ("energy_ranges", energy_ranges),
681+
"energy_range_edges": ("energy_ranges", energy_ranges_padded),
672682
"spin_period": (
673683
"spin_number",
674684
np.full(nspins, 15),

imap_processing/tests/ultra/unit/test_ultra_l1b.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from imap_processing import imap_module_directory
88
from imap_processing.cdf.utils import load_cdf, write_cdf
99
from imap_processing.quality_flags import ImapDEOutliersUltraFlags
10+
from imap_processing.ultra.constants import UltraConstants
1011
from imap_processing.ultra.l1b.de import FILLVAL_FLOAT32
1112
from imap_processing.ultra.l1b.ultra_l1b import ultra_l1b
1213
from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
@@ -65,6 +66,13 @@ def mock_data_l1b_extendedspin_dict():
6566
quality = np.zeros((2, 3), dtype="uint16")
6667
# These should be shape: (3,)
6768
energy_dep_flags = np.zeros(len(spin), dtype="uint16")
69+
energy_range_flags = np.zeros(UltraConstants.MAX_ENERGY_RANGES, dtype=np.uint16)
70+
energy_range_flags[:5] = 1 # Set first 5 to 1 for testing
71+
energy_range_edges = np.ones(
72+
UltraConstants.MAX_ENERGY_RANGE_EDGES, dtype=np.float32
73+
)
74+
energy_range_edges[:4] = [3.0, 5.0, 7.0, 10.0] # Example values
75+
energy_range_edges[4:] = -1.0e31 # Fill remaining with fillval
6876
data_dict = {
6977
"epoch": epoch,
7078
"spin_number": spin,
@@ -74,8 +82,8 @@ def mock_data_l1b_extendedspin_dict():
7482
"quality_low_voltage": energy_dep_flags,
7583
"quality_high_energy": energy_dep_flags,
7684
"quality_statistics": energy_dep_flags,
77-
"energy_range_flags": np.ones(5, dtype=np.uint16),
78-
"energy_range_edges": np.ones(4, dtype=np.float64),
85+
"energy_range_flags": energy_range_flags,
86+
"energy_range_edges": energy_range_edges,
7987
}
8088
return data_dict
8189

imap_processing/tests/ultra/unit/test_ultra_l1b_culling.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,16 +415,23 @@ def test_expand_bin_flags_to_spins(caplog):
415415
def test_get_energy_and_spin_dependent_rejection_mask():
416416
"""Tests get_energy_and_spin_dependent_rejection_mask function."""
417417
n_spins = 10
418+
energy_range_flags = np.zeros(16, dtype=np.uint16)
419+
energy_range_flags[:3] = [2**1, 2**2, 2**3] # Example flags for 3 energy bins
420+
energy_range_edges = np.full(17, -1.0e31, dtype=np.float32)
421+
energy_range_edges[:4] = [
422+
3,
423+
5,
424+
7,
425+
18,
426+
] # Example energy bin edges (4 edges = 3 bins)
418427
goodtimes_dataset = xr.Dataset(
419428
data_vars={
420429
"spin_number": np.arange(n_spins),
421430
"quality_low_voltage": np.full(n_spins, 0),
422431
"quality_high_energy": np.full(n_spins, 0),
423432
"quality_statistics": np.full(n_spins, 0),
424-
"energy_range_flags": np.array(
425-
[2**1, 2**2, 2**3]
426-
), # Example flags for energy bins
427-
"energy_range_edges": np.array([3, 5, 7, 18]), # Example energy bin edges
433+
"energy_range_flags": energy_range_flags,
434+
"energy_range_edges": energy_range_edges,
428435
}
429436
)
430437
# update quality flags to test that events get rejected

imap_processing/ultra/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,9 @@ class UltraConstants:
216216
STAT_CULLING_N_ITER = 5
217217
# Sigma threshold to use for statistical outlier culling.
218218
STAT_CULLING_STD_THRESHOLD = 0.05
219+
220+
# Set dimensions for extended spin/goodtime support variables
221+
# ISTP requires fixed dimensions, so we set these to the maximum we expect to need
222+
# and pad with fill values if we use fewer bins.
223+
MAX_ENERGY_RANGES = 16
224+
MAX_ENERGY_RANGE_EDGES = MAX_ENERGY_RANGES + 1

imap_processing/ultra/l1b/extendedspin.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,28 @@ def calculate_extendedspin(
166166
extendedspin_dict["quality_hk"] = hk_qf
167167
extendedspin_dict["quality_instruments"] = inst_qf
168168
extendedspin_dict["quality_low_voltage"] = voltage_qf # shape (nspin,)
169-
# TODO calculate flags for high energy (SEPS) and statistics culling
170-
# Initialize these flags to NONE for now.
171169
extendedspin_dict["quality_statistics"] = stat_outliers_qf # shape (nspin,)
172170
extendedspin_dict["quality_high_energy"] = high_energy_qf # shape (nspin,)
173-
# Add an array of flags for each energy bin. Shape: (n_energy_bins)
174-
extendedspin_dict["energy_range_flags"] = energy_bin_flags
175-
# Add energy ranges Shape: (n_energy_bins + 1)
176-
extendedspin_dict["energy_range_edges"] = np.array(energy_ranges)
171+
# ISTP requires stable dimension sizes, so this field must always remain size 16.
172+
# If fewer bins are used, pad the remaining entries with 0.
173+
energy_flags = np.full(UltraConstants.MAX_ENERGY_RANGES, 0, dtype=np.uint16)
174+
energy_flags[: len(energy_bin_flags)] = energy_bin_flags
175+
extendedspin_dict["energy_range_flags"] = energy_flags
176+
extendedspin_dict["energy_range_flags_dim"] = np.arange(
177+
UltraConstants.MAX_ENERGY_RANGES
178+
)
179+
180+
# Initialize array of energy range edges with fill value, then fill in the valid
181+
# energy ranges. Set the length to be the max number of energy bins we expect to
182+
# use for culling. The number of edges is one more than the number of bins (17).
183+
ranges = np.full(
184+
(UltraConstants.MAX_ENERGY_RANGE_EDGES,), FILLVAL_FLOAT32, dtype=np.float32
185+
)
186+
ranges[: len(energy_ranges)] = energy_ranges
187+
extendedspin_dict["energy_range_edges"] = ranges
188+
extendedspin_dict["energy_range_edges_dim"] = np.arange(
189+
UltraConstants.MAX_ENERGY_RANGE_EDGES
190+
)
177191

178192
extendedspin_dataset = create_dataset(extendedspin_dict, name, "l1b")
179193
return extendedspin_dataset

imap_processing/ultra/l1b/goodtimes.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ def calculate_goodtimes(extendedspin_dataset: xr.Dataset, name: str) -> xr.Datas
5454
)
5555

5656
data_dict = extract_data_dict(filtered_dataset)
57-
5857
goodtimes_dataset = create_dataset(data_dict, name, "l1b")
59-
6058
if goodtimes_dataset["spin_number"].size == 0:
6159
goodtimes_dataset = goodtimes_dataset.drop_dims("spin_number")
6260
goodtimes_dataset = goodtimes_dataset.expand_dims(spin_number=[FILLVAL_UINT32])

imap_processing/ultra/l1b/ultra_l1b_culling.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,8 @@ def get_energy_and_spin_dependent_rejection_mask(
555555
"""
556556
# Get the ebin flags for each energy bin from the goodtimes dataset.
557557
energy_range_edges = goodtimes_dataset["energy_range_edges"].values
558+
# Filter out fill values from energy_range_edges (negative or zero)
559+
energy_range_edges = energy_range_edges[energy_range_edges > 0]
558560
# Get the quality flag arrays "turned on" for energy dependent culling from the
559561
# goodtimes dataset.
560562
flag_arrays = [
@@ -564,6 +566,8 @@ def get_energy_and_spin_dependent_rejection_mask(
564566
# Initialize all events to not rejected
565567
rejected = np.zeros_like(energy, dtype=bool)
566568
ebin_flags = goodtimes_dataset["energy_range_flags"].values
569+
# Filter out fill values (0s) from energy_range_flags
570+
ebin_flags = ebin_flags[ebin_flags > 0]
567571
# Get the index of the spin number in the goodtimes dataset for each event
568572
# all spin numbers should be present in the goodtimes dataset since we have already
569573
# filtered any events that are not
@@ -670,7 +674,11 @@ def flag_low_voltage(
670674
# For each low voltage ind, flag the corresponding flag
671675
quality_flags[lv_spin_inds] = True
672676

673-
# TODO add log summary.
677+
num_culled: int = np.sum(quality_flags)
678+
logger.info(
679+
f"Low voltage culling removed {num_culled} spin bins across all energy "
680+
f"channels. Voltage threshold: {voltage_threshold} V."
681+
)
674682

675683
return quality_flags
676684

@@ -748,7 +756,12 @@ def flag_high_energy(
748756
quality_flags[:, ~mask] = flagged[:, ~mask]
749757
else:
750758
quality_flags = flagged
751-
# TODO add log summary. E.g Tim's hi goodtimes code
759+
760+
num_culled: int = np.sum(quality_flags)
761+
logger.info(
762+
f"High energy culling removed {num_culled} spin bins across {n_energy_bins} "
763+
f"energy channels. Energy thresholds: {energy_thresholds.flatten()}, "
764+
)
752765

753766
return quality_flags
754767

@@ -885,7 +898,7 @@ def flag_statistical_outliers(
885898
convergence[e_idx] = True
886899

887900
num_culled: int = np.sum(quality_stats)
888-
logger.debug(
901+
logger.info(
889902
f"Statistical culling removed {num_culled} spin bins across {n_energy_bins}"
890903
f" energy channels. Convergence: {convergence} after "
891904
f"{iterations} iterations."

imap_processing/ultra/l1c/ultra_l1c_pset_bins.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,14 +480,19 @@ def get_spacecraft_exposure_times(
480480
spin_periods = goodtimes_dataset["spin_period"].values
481481
energy_flags = goodtimes_dataset["energy_range_flags"].values
482482
# only get valid flags for the energy bins we are using at l1c
483+
# Filter out fill values (0s) from energy_range_flags
483484
energy_flags = energy_flags[energy_flags > 0]
485+
# Filter out fill values from energy_range_edges
486+
energy_range_edges = goodtimes_dataset["energy_range_edges"].values
487+
# Remove fill values (negative or zero)
488+
energy_range_edges_valid = energy_range_edges[energy_range_edges > 0]
484489
# Get the quality flag arrays "turned on" for energy dependent culling from the
485490
# goodtimes dataset.
486491
flag_arrays = [
487492
goodtimes_dataset[flag_name].values
488493
for flag_name in ENERGY_DEPENDENT_SPIN_QUALITY_FLAG_FILTERS
489494
]
490-
bin_to_range = np.digitize(energy_bins, goodtimes_dataset.energy_range_edges)
495+
bin_to_range = np.digitize(energy_bins, energy_range_edges_valid)
491496
valid_spins = (
492497
np.bitwise_or.reduce(flag_arrays)[np.newaxis, :] & energy_flags[:, np.newaxis]
493498
) == 0

imap_processing/ultra/utils/ultra_l1_utils.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ def create_dataset( # noqa: PLR0912
9797
# epoch coordinate already created with correct attrs
9898
continue
9999
elif key == "epoch_delta":
100-
# Create epoch_delta variable
101100
dataset[key] = xr.DataArray(
102101
data,
103102
dims=["epoch"],
@@ -109,10 +108,16 @@ def create_dataset( # noqa: PLR0912
109108
"pixel_index",
110109
"spin_phase_step",
111110
]:
112-
# update attrs
111+
# update attrs on existing coords
113112
dataset[key].attrs = cdf_manager.get_variable_attributes(
114113
key, check_schema=False
115114
)
115+
elif key in ["energy_range_edges_dim", "energy_range_flags_dim"]:
116+
dataset[key] = xr.DataArray(
117+
data,
118+
dims=[key],
119+
attrs=cdf_manager.get_variable_attributes(key, check_schema=False),
120+
)
116121
elif key in velocity_keys:
117122
dataset[key] = xr.DataArray(
118123
data,
@@ -177,24 +182,22 @@ def create_dataset( # noqa: PLR0912
177182
dims=["energy_bin_geometric_mean", "pixel_index"],
178183
attrs=cdf_manager.get_variable_attributes(key, check_schema=False),
179184
)
180-
elif key in {
181-
"dead_time_ratio",
182-
}:
185+
elif key in {"dead_time_ratio"}:
183186
dataset[key] = xr.DataArray(
184187
data,
185188
dims=["spin_phase_step"],
186189
attrs=cdf_manager.get_variable_attributes(key, check_schema=False),
187190
)
188-
elif key in {"energy_range_edges"}:
191+
elif key == "energy_range_edges":
189192
dataset[key] = xr.DataArray(
190193
data,
191-
dims=["energy_range_edges"],
194+
dims=["energy_range_edges_dim"],
192195
attrs=cdf_manager.get_variable_attributes(key, check_schema=False),
193196
)
194-
elif key in {"energy_range_flags"}:
197+
elif key == "energy_range_flags":
195198
dataset[key] = xr.DataArray(
196199
data,
197-
dims=["energy_ranges"],
200+
dims=["energy_range_flags_dim"],
198201
attrs=cdf_manager.get_variable_attributes(key, check_schema=False),
199202
)
200203
else:
@@ -225,7 +228,15 @@ def extract_data_dict(dataset: xr.Dataset) -> dict:
225228
data_dict.update(
226229
{
227230
coord: dataset.coords[coord].values
228-
for coord in ("spin_number", "energy_bin_geometric_mean", "epoch")
231+
for coord in (
232+
"spin_number",
233+
"energy_bin_geometric_mean",
234+
"epoch",
235+
"energy_range_flags_dim",
236+
"energy_range_edges_dim",
237+
"energy_range_flags",
238+
"energy_range_edges",
239+
)
229240
if coord in dataset.coords
230241
}
231242
)

0 commit comments

Comments
 (0)