Skip to content
12 changes: 12 additions & 0 deletions imap_processing/cdf/config/imap_constant_attrs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ epoch:
VALIDMIN: 315576066184000000 # 2010-01-01T00:00:00 mission start (APL 0 epoch)
VAR_TYPE: support_data

epoch_delta:
CATDESC: Epoch delta
DEPEND_0: epoch
FIELDNAM: Epoch Delta
FILLVAL: -9223372036854775808
FORMAT: I19
LABLAXIS: Epoch delta
SCALETYP: linear
UNITS: ns
VALIDMAX: 86000000000000
Comment thread
leowerneck marked this conversation as resolved.
VALIDMIN: 0
VAR_TYPE: support_data

# <=== Data Variables ===>
# Default Attrs for all metadata variables unless overridden
Expand Down
4 changes: 4 additions & 0 deletions imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ default_angle_attrs: &default_angle
VAR_TYPE: support_data

# <=== Coordinates ===>
epoch_macropixel:
DELTA_MINUS_VAR: epoch_delta
DELTA_PLUS_VAR: epoch_delta

zenith:
<<: *default_angle
CATDESC: Angle from the spin axis (0 deg.) to anti-spin axis (180 deg.) in 8 bins
Expand Down
71 changes: 70 additions & 1 deletion imap_processing/hit/l2/hit_l2.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ def add_cdf_attributes(
),
)
dataset.coords[f"{dim}_label"] = label_array
elif "macropixel" in logical_source:
dataset["epoch"].attrs.update(
attr_mgr.get_variable_attributes("epoch_macropixel", check_schema=False)
)

return dataset

Expand Down Expand Up @@ -782,4 +786,69 @@ def process_macropixel_intensity(
{var: f"{var}_macropixel_intensity"}
)

return macropixel_intensity_dataset
return transform_to_10_minute_chunks(macropixel_intensity_dataset)


def transform_to_10_minute_chunks(macropixel_dataset: xr.Dataset) -> xr.Dataset:
"""Transform macropixel records into 10-minute integration chunks.

Parameters
----------
macropixel_dataset : xarray.Dataset
Macropixel data containing one species and energy combination per
one-minute epoch.

Returns
-------
xarray.Dataset
Macropixel data combined into one record per 10-minute integration.
"""
species_energy = [
("h", 3),
("he4", 2),
("cno", 2),
("nemgsi", 2),
("fe", 1),
]

# Use the first record in each 10-record group as the output template.
transformed_dataset = macropixel_dataset.isel(
epoch=slice(None, None, 10),
).copy(deep=True)

# Each minute in a 10-record group contains one species/energy combination,
# ordered as described by species_energy. Track that minute's packet offset.
species_i = 0
for species, num_energy_levels in species_energy:
energy_dim = f"{species}_energy_mean"
# Gather the intensity and uncertainty variables that share this
# species' energy dimension.
species_variables = [
var
for var in macropixel_dataset.data_vars
if macropixel_dataset[var].dims[:2] == ("epoch", energy_dim)
]

for energy_i in range(num_energy_levels):
for var in species_variables:
# Select this species/energy packet from every 10-record group
# and place it in the corresponding output energy plane.
data_i = macropixel_dataset[var].values[species_i::10, energy_i]
transformed_dataset[var].values[:, energy_i] = data_i
species_i += 1
Comment thread
leowerneck marked this conversation as resolved.

minute_cadence_epochs = macropixel_dataset["epoch"].values
ten_minute_cadence_epochs = minute_cadence_epochs.reshape(-1, 10)
nanoseconds_per_10_min = SECONDS_PER_10_MIN * 1_000_000_000
nanoseconds_per_5_min = nanoseconds_per_10_min // 2
start_times = ten_minute_cadence_epochs[:, 0]
end_times = ten_minute_cadence_epochs[:, -1]
new_epochs = start_times + (end_times - start_times) // 2 - nanoseconds_per_10_min

transformed_dataset = transformed_dataset.assign_coords(epoch=np.array(new_epochs))
transformed_dataset["epoch_delta"] = xr.DataArray(
np.full(len(new_epochs), nanoseconds_per_5_min, dtype=np.int64),
dims=["epoch"],
)

return transformed_dataset
71 changes: 71 additions & 0 deletions imap_processing/tests/hit/test_hit_l2.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
process_standard_intensity,
process_summed_intensity,
reshape_for_sectored,
transform_to_10_minute_chunks,
)

EXPECTED_STANDARD_LABLAXIS = {
Expand Down Expand Up @@ -793,6 +794,76 @@ def test_process_macropixel_intensity(
)


def test_transform_to_10_minute_chunks():
"""Test that transform_to_10_minute_chunks correctly regroups one-minute
macropixel records into 10-minute chunks and re-centers the epoch."""
n_minutes = 20
minute_ns = 60_000_000_000
epochs = np.arange(n_minutes, dtype=np.int64) * minute_ns

# Each species/energy combination occupies one fixed minute slot within
# every 10-record group, in this order (mirrors the physical packet
# cadence handled by transform_to_10_minute_chunks).
species_energy = [
("h", 3),
("he4", 2),
("cno", 2),
("nemgsi", 2),
("fe", 1),
]

data_vars = {}
slot_for = {}
species_i = 0
for species, num_energy_levels in species_energy:
energy_dim = f"{species}_energy_mean"
values = np.array(
[[m * 100 + e for e in range(num_energy_levels)] for m in range(n_minutes)],
dtype=np.float32,
)
data_vars[f"{species}_macropixel_intensity"] = (("epoch", energy_dim), values)
for energy_i in range(num_energy_levels):
slot_for[(species, energy_i)] = species_i
species_i += 1

macropixel_dataset = xr.Dataset(data_vars, coords={"epoch": epochs})

result = transform_to_10_minute_chunks(macropixel_dataset)

n_chunks = n_minutes // 10
assert len(result["epoch"]) == n_chunks

# epoch_delta is always half of a 10-minute chunk.
expected_epoch_delta = np.full(
n_chunks, SECONDS_PER_10_MIN * 1_000_000_000 // 2, dtype=np.int64
)
np.testing.assert_array_equal(result["epoch_delta"].values, expected_epoch_delta)

# Each new epoch is centered on its 10-minute group, then shifted back by
# a full 10-minute chunk.
for chunk in range(n_chunks):
start = epochs[chunk * 10]
end = epochs[chunk * 10 + 9]
expected_epoch = start + (end - start) // 2 - SECONDS_PER_10_MIN * 1_000_000_000
assert result["epoch"].values[chunk] == expected_epoch

# Each species/energy variable should pull its value from the minute slot
# it physically occupies within every 10-record group.
for species, num_energy_levels in species_energy:
var = f"{species}_macropixel_intensity"
for energy_i in range(num_energy_levels):
slot = slot_for[(species, energy_i)]
expected = np.array(
[(chunk * 10 + slot) * 100 + energy_i for chunk in range(n_chunks)],
dtype=np.float32,
)
np.testing.assert_array_equal(
result[var].values[:, energy_i],
expected,
err_msg=f"Mismatch for {var} energy index {energy_i}",
)


def test_process_summed_intensity(l1b_summed_rates_dataset, ancillary_dependencies):
"""Test the variables in the summed intensity dataset"""

Expand Down
Loading