Skip to content

Commit 6508b63

Browse files
committed
glows l2 night flags
1 parent 062ea48 commit 6508b63

2 files changed

Lines changed: 140 additions & 6 deletions

File tree

imap_processing/glows/l2/glows_l2_data.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,20 @@ def __init__(
370370
"""
371371
active_flags = np.array(pipeline_settings.active_bad_time_flags, dtype=float)
372372

373+
# Apply sunrise/sunset offsets to extend the night region around
374+
# is_night transitions before selecting good blocks.
375+
flags = self.apply_is_night_offsets(
376+
l1b_dataset["flags"].data,
377+
is_night_idx=6, # is_night is the 7th bad-time flag (0-indexed)
378+
sunrise_offset=pipeline_settings.sunrise_offset,
379+
sunset_offset=pipeline_settings.sunset_offset,
380+
)
381+
flags_da = xr.DataArray(flags, dims=l1b_dataset["flags"].dims)
382+
373383
# Select the good blocks (i.e. epoch values) according to the flags. Drop any
374384
# bad blocks before processing.
375385
good_data = l1b_dataset.isel(
376-
epoch=self.return_good_times(l1b_dataset["flags"], active_flags)
386+
epoch=self.return_good_times(flags_da, active_flags)
377387
)
378388
# TODO: bad angle filter
379389
# TODO: filter bad bins out. Needs to happen here while everything is still
@@ -533,6 +543,97 @@ def return_good_times(flags: xr.DataArray, active_flags: NDArray) -> NDArray:
533543
good_times = np.where(np.all(flags[:, active_flags == 1] == 1, axis=1))[0]
534544
return good_times
535545

546+
@staticmethod
547+
def apply_is_night_offsets(
548+
flags: np.ndarray,
549+
is_night_idx: int,
550+
sunrise_offset: float,
551+
sunset_offset: float,
552+
) -> np.ndarray:
553+
"""
554+
Apply sunrise/sunset offsets to is_night transitions.
555+
556+
Per algorithm doc v4.4.7, Sec. 3.9.1, item 2 (raw is_night: 1=night, 0=day):
557+
558+
sunset_offset applies at both transitions:
559+
>0: night shortens by N at each end (first N night epochs at sunset become
560+
day; last N night epochs before sunrise become day)
561+
<0: night extends by |N| at each end
562+
563+
sunrise_offset is an additional adjustment at sunrise (is_night 1->0) only:
564+
>0: night extends N histograms past the raw sunrise transition
565+
<0: night shortens by |N| before the raw sunrise transition
566+
567+
In the processed flags array: 0 = bad (night), 1 = good (day).
568+
569+
Parameters
570+
----------
571+
flags : numpy.ndarray
572+
Flags array with shape (n_epochs, FLAG_LENGTH), 0=bad, 1=good.
573+
is_night_idx : int
574+
Column index of the is_night flag in the flags array.
575+
sunrise_offset : float
576+
Additional histogram shift at the sunrise (is_night 1->0) transition.
577+
sunset_offset : float
578+
Histogram shift applied at both the sunset and sunrise transitions.
579+
580+
Returns
581+
-------
582+
numpy.ndarray
583+
Copy of flags with the is_night column adjusted per the offsets.
584+
585+
Notes
586+
-----
587+
Algorithm doc v4.4.7, Sec. 3.9.1, item 2
588+
is_night: 1 = daytime (good), 0 = night (bad)
589+
"""
590+
flags_with_offsets = flags.copy()
591+
592+
# If sunrise_offset=0 and sunset_offset=0 then no corrections are needed
593+
# relative to is_night transition set onboard.
594+
if sunrise_offset == 0 and sunset_offset == 0:
595+
return flags_with_offsets
596+
597+
is_night_col = flags[:, is_night_idx]
598+
n = flags.shape[0]
599+
diff = np.diff(is_night_col.astype(int))
600+
sunset_index = np.where(diff == -1)[0]
601+
sunrise_index = np.where(diff == 1)[0]
602+
603+
if sunrise_offset > 0:
604+
# Night (flag = 0) extends by sunrise_offset relative
605+
# to is_night 0 -> 1 transition.
606+
for i in sunrise_index:
607+
flags_with_offsets[
608+
i + 1 : min(n, i + 1 + int(sunrise_offset)), is_night_idx
609+
] = 0
610+
611+
if sunrise_offset < 0:
612+
# Night (flag = 0) shortens by sunrise_offset relative
613+
# to is_night 0 -> 1 transition.
614+
for i in sunrise_index:
615+
flags_with_offsets[
616+
max(0, i + 1 + int(sunrise_offset)) : i + 1, is_night_idx
617+
] = 1
618+
619+
if sunset_offset > 0:
620+
# Night (flag = 0) shortens by sunset_offset relative
621+
# to is_night 1 -> 0 transition.
622+
for i in sunset_index:
623+
flags_with_offsets[
624+
i + 1 : min(n, i + 1 + int(sunset_offset)), is_night_idx
625+
] = 1
626+
627+
if sunset_offset < 0:
628+
# Night (flag = 0) extends by sunset_offset relative
629+
# to is_night 1 -> 0 transition.
630+
for i in sunset_index:
631+
flags_with_offsets[
632+
max(0, i + 1 + int(sunset_offset)) : i + 1, is_night_idx
633+
] = 0
634+
635+
return flags_with_offsets
636+
536637
def compute_position_angle(self) -> float:
537638
"""
538639
Compute the position angle based on the instrument mounting.

imap_processing/tests/glows/test_glows_l2_data.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,38 @@ def test_filter_good_times():
346346
assert np.array_equal(good_times, expected_good_times)
347347

348348

349+
@pytest.mark.parametrize(
350+
"sunrise_offset, sunset_offset, expected_is_night",
351+
[
352+
# sunset>0 shortens at sunset; sunrise>0 extends at sunrise
353+
(1, 1, [1, 1, 1, 1, 0, 0, 0, 1]),
354+
# sunset>0 shortens at sunset; sunrise<0 shortens at sunrise
355+
(-1, 1, [1, 1, 1, 1, 0, 1, 1, 1]),
356+
# sunset<0 extends at sunset; sunrise>0 extends at sunrise
357+
(1, -1, [1, 1, 0, 0, 0, 0, 0, 1]),
358+
# sunset<0 extends at sunset; sunrise<0 shortens at sunrise
359+
(-1, -1, [1, 1, 0, 0, 0, 1, 1, 1]),
360+
# zero offsets: no change
361+
(0, 0, [1, 1, 1, 0, 0, 0, 1, 1]),
362+
],
363+
)
364+
def test_apply_is_night_offsets(sunrise_offset, sunset_offset, expected_is_night):
365+
"""Test apply_is_night_offsets function."""
366+
367+
# Setup: epochs 0-2 day, 3-5 night, 6-7 day (processed flags: 0=night, 1=day).
368+
flags = np.ones((8, 17), dtype=float)
369+
flags[3:6, 6] = 0 # epochs 3-5 are night
370+
371+
result = HistogramL2.apply_is_night_offsets(
372+
flags,
373+
is_night_idx=6,
374+
sunrise_offset=sunrise_offset,
375+
sunset_offset=sunset_offset,
376+
)
377+
378+
assert np.array_equal(result[:, 6], np.array(expected_is_night, dtype=float))
379+
380+
349381
# ── spin_angle tests ──────────────────────────────────────────────────────────
350382

351383

@@ -396,7 +428,8 @@ def test_compute_position_angle():
396428
def l1b_dataset_full():
397429
"""Minimal L1B dataset with all variables required by HistogramL2.
398430
399-
Two epochs, four bins, 17 flags (all good).
431+
Two epochs, four bins, 17 flags. Both epochs are daytime (is_night=1).
432+
All other flags are 1 (good).
400433
"""
401434
n_epochs, n_bins, n_angle_flags, n_time_flags = 2, 4, 4, 17
402435
fillval = GlowsConstants.HISTOGRAM_FILLVAL
@@ -406,6 +439,9 @@ def l1b_dataset_full():
406439
spin_angle = np.tile(np.linspace(0, 270, n_bins), (n_epochs, 1))
407440
histogram_flag_array = np.zeros((n_epochs, n_angle_flags, n_bins), dtype=np.uint8)
408441

442+
# All flags good (1). Index 6 is is_night: 1 = daytime (good).
443+
flags = np.ones((n_epochs, n_time_flags), dtype=float)
444+
409445
return xr.Dataset(
410446
{
411447
"histogram": (["epoch", "bins"], histogram),
@@ -417,10 +453,7 @@ def l1b_dataset_full():
417453
histogram_flag_array,
418454
),
419455
"number_of_bins_per_histogram": (["epoch"], [n_bins, n_bins]),
420-
"flags": (
421-
["epoch", "flag_index"],
422-
np.ones((n_epochs, n_time_flags)),
423-
),
456+
"flags": (["epoch", "flag_index"], flags),
424457
"filter_temperature_average": (["epoch"], [20.0, 21.0]),
425458
"hv_voltage_average": (["epoch"], [1000.0, 1000.0]),
426459
"pulse_length_average": (["epoch"], [5.0, 5.0]),

0 commit comments

Comments
 (0)