Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion imap_processing/glows/l2/glows_l2_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,10 +370,20 @@ def __init__(
"""
active_flags = np.array(pipeline_settings.active_bad_time_flags, dtype=float)

# Apply sunrise/sunset offsets to extend the night region around
# is_night transitions before selecting good blocks.
flags = self.apply_is_night_offsets(
l1b_dataset["flags"].data,
is_night_idx=GlowsConstants.IS_NIGHT_FLAG_IDX,
sunrise_offset=int(pipeline_settings.sunrise_offset),
sunset_offset=int(pipeline_settings.sunset_offset),
)
flags_da = xr.DataArray(flags, dims=l1b_dataset["flags"].dims)

# Select the good blocks (i.e. epoch values) according to the flags. Drop any
# bad blocks before processing.
good_data = l1b_dataset.isel(
epoch=self.return_good_times(l1b_dataset["flags"], active_flags)
epoch=self.return_good_times(flags_da, active_flags)
)
# TODO: bad angle filter
# TODO: filter bad bins out. Needs to happen here while everything is still
Expand Down Expand Up @@ -533,6 +543,98 @@ def return_good_times(flags: xr.DataArray, active_flags: NDArray) -> NDArray:
good_times = np.where(np.all(flags[:, active_flags == 1] == 1, axis=1))[0]
return good_times

@staticmethod
def apply_is_night_offsets(
flags: np.ndarray,
is_night_idx: int,
sunrise_offset: int,
sunset_offset: int,
) -> np.ndarray:
"""
Apply sunrise/sunset offsets to is_night transitions.

Per algorithm doc v4.4.7, Sec. 3.9.1, item 2 (raw is_night: 1=night, 0=day):

sunset_offset applies at both transitions:
>0: night shortens by N at each end (first N night epochs at sunset become
day; last N night epochs before sunrise become day)
<0: night extends by |N| at each end

sunrise_offset is an additional adjustment at sunrise (is_night 1->0) only:
>0: night extends N histograms past the raw sunrise transition
<0: night shortens by |N| before the raw sunrise transition

In the processed flags array: 0 = bad (night), 1 = good (day).

Parameters
----------
flags : numpy.ndarray
Flags array with shape (n_epochs, FLAG_LENGTH), 0=bad, 1=good.
is_night_idx : int
Column index of the is_night flag in the flags array.
sunrise_offset : int
Additional histogram shift at the sunrise (is_night 1->0) transition.
sunset_offset : int
Histogram shift applied at both the sunset and sunrise transitions.

Returns
-------
numpy.ndarray
Returns the original flags array if no offsets are applied,
otherwise returns a modified copy.

Notes
-----
Algorithm doc v4.4.7, Sec. 3.9.1, item 2
is_night: 1 = daytime (good), 0 = night (bad)
"""
# If sunrise_offset=0 and sunset_offset=0 then no corrections are needed
# relative to is_night transition set onboard.
if sunrise_offset == 0 and sunset_offset == 0:
return flags

flags_with_offsets = flags.copy()

is_night_col = flags[:, is_night_idx]
n = flags.shape[0]
diff = np.diff(is_night_col.astype(int))
sunset_index = np.where(diff == -1)[0]
sunrise_index = np.where(diff == 1)[0]

if sunrise_offset > 0:
# Night (flag = 0) extends by sunrise_offset relative
# to is_night 0 -> 1 transition.
for i in sunrise_index:
flags_with_offsets[
i + 1 : min(n, i + 1 + sunrise_offset), is_night_idx
] = 0

elif sunrise_offset < 0:
# Night (flag = 0) shortens by sunrise_offset relative
# to is_night 0 -> 1 transition.
for i in sunrise_index:
flags_with_offsets[
max(0, i + 1 + sunrise_offset) : i + 1, is_night_idx
] = 1

if sunset_offset > 0:
# Night (flag = 0) shortens by sunset_offset relative
# to is_night 1 -> 0 transition.
for i in sunset_index:
flags_with_offsets[
i + 1 : min(n, i + 1 + sunset_offset), is_night_idx
] = 1

elif sunset_offset < 0:
# Night (flag = 0) extends by sunset_offset relative
# to is_night 1 -> 0 transition.
for i in sunset_index:
flags_with_offsets[
max(0, i + 1 + sunset_offset) : i + 1, is_night_idx
] = 0

return flags_with_offsets

def compute_position_angle(self) -> float:
"""
Compute the position angle based on the instrument mounting.
Expand Down
3 changes: 3 additions & 0 deletions imap_processing/glows/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,15 @@ class GlowsConstants:
Fill value for histogram bins (65535 for uint16)
STANDARD_BIN_COUNT: int
Standard number of bins per histogram (3600)
IS_NIGHT_FLAG_IDX: int
Index of the is_night flag in the bad-time flags array (0-indexed)
"""

SUBSECOND_LIMIT: int = 2_000_000
SCAN_CIRCLE_ANGULAR_RADIUS: float = 75.0
HISTOGRAM_FILLVAL: int = 65535
STANDARD_BIN_COUNT: int = 3600
IS_NIGHT_FLAG_IDX: int = 6


@dataclass
Expand Down
52 changes: 47 additions & 5 deletions imap_processing/tests/glows/test_glows_l2_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,47 @@ def test_filter_good_times():
assert np.array_equal(good_times, expected_good_times)


@pytest.mark.parametrize(
"sunrise_offset, sunset_offset, expected_is_night",
[
# sunrise>0 extends at sunrise; sunset>0 shortens at sunset
(1, 1, [1, 1, 1, 1, 0, 0, 0, 1]),
# sunrise<0 shortens at sunrise; sunset>0 shortens at sunset
(-1, 1, [1, 1, 1, 1, 0, 1, 1, 1]),
# sunrise>0 extends at sunrise; sunset<0 extends at sunset
(1, -1, [1, 1, 0, 0, 0, 0, 0, 1]),
# sunrise<0 shortens at sunrise; sunset<0 extends at sunset
(-1, -1, [1, 1, 0, 0, 0, 1, 1, 1]),
# zero offsets: no change
(0, 0, [1, 1, 1, 0, 0, 0, 1, 1]),
],
Comment thread
laspsandoval marked this conversation as resolved.
)
def test_apply_is_night_offsets(sunrise_offset, sunset_offset, expected_is_night):
"""Test apply_is_night_offsets function."""

# Setup: epochs 0-2 day, 3-5 night, 6-7 day (processed flags: 0=night, 1=day).
flags = np.ones((8, 17), dtype=float)
flags[3:6, 6] = 0 # epochs 3-5 are night
original_flags = flags.copy()

result = HistogramL2.apply_is_night_offsets(
flags,
is_night_idx=6,
sunrise_offset=sunrise_offset,
sunset_offset=sunset_offset,
)

assert np.array_equal(result[:, 6], np.array(expected_is_night, dtype=float))
Comment thread
laspsandoval marked this conversation as resolved.

if sunrise_offset == 0 and sunset_offset == 0:
# No offsets: original array returned as-is (no copy)
assert result is flags
else:
# Offsets applied: result is a copy, original flags are unchanged
assert result is not flags
assert np.array_equal(flags, original_flags)


# ── spin_angle tests ──────────────────────────────────────────────────────────


Expand Down Expand Up @@ -396,7 +437,8 @@ def test_compute_position_angle():
def l1b_dataset_full():
"""Minimal L1B dataset with all variables required by HistogramL2.

Two epochs, four bins, 17 flags (all good).
Two epochs, four bins, 17 flags. Both epochs are daytime (is_night=1).
All other flags are 1 (good).
"""
n_epochs, n_bins, n_angle_flags, n_time_flags = 2, 4, 4, 17
fillval = GlowsConstants.HISTOGRAM_FILLVAL
Expand All @@ -406,6 +448,9 @@ def l1b_dataset_full():
spin_angle = np.tile(np.linspace(0, 270, n_bins), (n_epochs, 1))
histogram_flag_array = np.zeros((n_epochs, n_angle_flags, n_bins), dtype=np.uint8)

# All flags good (1). Index 6 is is_night: 1 = daytime (good).
flags = np.ones((n_epochs, n_time_flags), dtype=float)

return xr.Dataset(
{
"histogram": (["epoch", "bins"], histogram),
Expand All @@ -417,10 +462,7 @@ def l1b_dataset_full():
histogram_flag_array,
),
"number_of_bins_per_histogram": (["epoch"], [n_bins, n_bins]),
"flags": (
["epoch", "flag_index"],
np.ones((n_epochs, n_time_flags)),
),
"flags": (["epoch", "flag_index"], flags),
"filter_temperature_average": (["epoch"], [20.0, 21.0]),
"hv_voltage_average": (["epoch"], [1000.0, 1000.0]),
"pulse_length_average": (["epoch"], [5.0, 5.0]),
Expand Down
Loading