Skip to content

Commit c85d662

Browse files
authored
Glows badtime flag (#2760)
1 parent 6044bf1 commit c85d662

6 files changed

Lines changed: 142 additions & 14 deletions

File tree

imap_processing/glows/l1b/glows_l1b_data.py

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@
99

1010
from imap_processing.glows import FLAG_LENGTH
1111
from imap_processing.glows.utils.constants import TimeTuple
12+
from imap_processing.quality_flags import GLOWSL1bFlags
1213
from imap_processing.spice import geometry
13-
from imap_processing.spice.geometry import SpiceBody, SpiceFrame
14+
from imap_processing.spice.geometry import (
15+
SpiceBody,
16+
SpiceFrame,
17+
frame_transform,
18+
get_instrument_mounting_az_el,
19+
spherical_to_cartesian,
20+
)
1421
from imap_processing.spice.spin import (
1522
get_instrument_spin_phase,
1623
get_spin_angle,
@@ -819,8 +826,11 @@ def __post_init__(
819826

820827
# Add SPICE related variables
821828
self.update_spice_parameters()
822-
# Will require some additional inputs
823-
self.imap_spin_angle_bin_cntr = np.zeros((3600,))
829+
# Calculate the spin angle bin center
830+
phi = (
831+
np.arange(self.number_of_bins_per_histogram, dtype=np.float64) + 0.5
832+
) / self.number_of_bins_per_histogram
833+
self.imap_spin_angle_bin_cntr = phi * 360.0
824834

825835
# TODO: This should probably be an AWS file
826836
# TODO Pass in AncillaryParameters object instead of reading here.
@@ -970,6 +980,79 @@ def deserialize_flags(raw: int) -> np.ndarray[int]:
970980

971981
return flags
972982

983+
def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray:
984+
"""
985+
Create boolean mask where True means bin is within radius of UV source.
986+
987+
Parameters
988+
----------
989+
exclusions : AncillaryExclusions
990+
Ancillary exclusions data filtered for the current day.
991+
992+
Returns
993+
-------
994+
close_to_uv_source : np.ndarray
995+
Boolean mask for uv source.
996+
"""
997+
# Rotate spin-angle bin centers by the instrument position-angle offset
998+
# so azimuth=0 aligns with the instrument pointing direction.
999+
azimuth = (
1000+
self.imap_spin_angle_bin_cntr + self.position_angle_offset_average
1001+
) % 360.0
1002+
# Ephemeris start time of the histogram accumulation.
1003+
data_start_time_et = sct_to_et(met_to_sclkticks(self.imap_start_time))
1004+
1005+
# Instrument pointing direction in the DPS frame.
1006+
az_el = get_instrument_mounting_az_el(SpiceFrame.IMAP_GLOWS)
1007+
elevation = az_el[1]
1008+
1009+
spherical = np.stack(
1010+
[np.ones_like(azimuth), azimuth, np.full_like(azimuth, elevation)],
1011+
axis=-1,
1012+
) # (nbin, 3)
1013+
1014+
# Convert to unit cartesian vectors.
1015+
look_vecs_dps = spherical_to_cartesian(spherical) # (nbin, 3)
1016+
1017+
# Transform unit cartesian vectors to ECLIPJ2000 frame.
1018+
look_vecs_ecl = frame_transform(
1019+
data_start_time_et,
1020+
look_vecs_dps,
1021+
SpiceFrame.IMAP_DPS,
1022+
SpiceFrame.ECLIPJ2000,
1023+
)
1024+
1025+
# UV source vectors.
1026+
uv_longitude = exclusions.uv_sources[
1027+
"ecliptic_longitude_deg"
1028+
].values # (n_src,)
1029+
uv_latitude = exclusions.uv_sources["ecliptic_latitude_deg"].values # (n_src,)
1030+
uv_radius = np.deg2rad(
1031+
exclusions.uv_sources["angular_radius_for_masking"].values
1032+
)
1033+
1034+
uv_spherical = np.stack(
1035+
[np.ones_like(uv_longitude), uv_longitude, uv_latitude],
1036+
axis=-1,
1037+
) # (n_src, 3): (r, azimuth, elevation) in degrees
1038+
1039+
uv_vecs = spherical_to_cartesian(uv_spherical) # (n_src, 3)
1040+
1041+
# Dot product of unit vectors gives cos(separation_angle) for each
1042+
# histogram bin vs. each UV source -> shape (nbin, n_src).
1043+
# (nbin, 3) @ (3, n_src) -> (nbin, n_src)
1044+
# If dot product -> 1 the two vectors point in almost
1045+
# the same direction and needs mask.
1046+
# If dot product -> 0 the two directions are perpendicular on the sky.
1047+
cos_sep = look_vecs_ecl @ uv_vecs.T # (nbin, n_src)
1048+
1049+
# Determine if the pixel is too close to any of the source radii.
1050+
close_to_uv_source = np.any(
1051+
cos_sep >= np.cos(uv_radius)[None, :], axis=1
1052+
) # (nbin,)
1053+
1054+
return close_to_uv_source
1055+
9731056
def _compute_histogram_flag_array(
9741057
self, exclusions: AncillaryExclusions
9751058
) -> np.ndarray:
@@ -978,9 +1061,9 @@ def _compute_histogram_flag_array(
9781061
9791062
Creates a (4, 3600) array where each row represents a different flag type:
9801063
- Row 0: is_close_to_uv_source
981-
- Row 1: is_inside_excluded_region
982-
- Row 2: is_excluded_by_instr_team
983-
- Row 3: is_suspected_transient
1064+
- Row 1: is_inside_excluded_region (TODO)
1065+
- Row 2: is_excluded_by_instr_team (TODO)
1066+
- Row 3: is_suspected_transient (TODO)
9841067
9851068
Parameters
9861069
----------
@@ -992,5 +1075,15 @@ def _compute_histogram_flag_array(
9921075
np.ndarray
9931076
Array of shape (4, 3600) with bad-angle flags for each bin.
9941077
"""
995-
# TODO: fill out once spice data is available
996-
return np.zeros((4, 3600), dtype=np.uint8)
1078+
histogram_flags = np.full(
1079+
(4, self.number_of_bins_per_histogram),
1080+
GLOWSL1bFlags.NONE.value,
1081+
dtype=np.uint8,
1082+
)
1083+
1084+
close_any = self.flag_uv_source(exclusions)
1085+
1086+
# close if within radius of any UV source
1087+
histogram_flags[0][close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value
1088+
1089+
return histogram_flags

imap_processing/quality_flags.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class GLOWSL1bFlags(FlagNameMixin):
148148
"""Glows L1b flags."""
149149

150150
NONE = CommonFlags.NONE
151+
IS_CLOSE_TO_UV_SOURCE = 2**0 # Is the bin close to a UV source.
151152

152153

153154
class ImapHiL1bDeFlags(FlagNameMixin):

imap_processing/tests/glows/conftest.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,28 @@ def mock_ancillary_exclusions():
9999
["epoch", "source"],
100100
[["star1", "star2", "star3"]] * len(epoch_range),
101101
),
102+
# degrees in [0, 360)
102103
"ecliptic_longitude_deg": (
103104
["epoch", "source"],
104-
np.random.rand(len(epoch_range), 3),
105+
np.tile(
106+
np.array([202.0812, 120.0, 250.0], dtype=np.float64),
107+
(len(epoch_range), 1),
108+
),
105109
),
110+
# degrees in [-90, 90]
106111
"ecliptic_latitude_deg": (
107112
["epoch", "source"],
108-
np.random.rand(len(epoch_range), 3),
113+
np.tile(
114+
np.array([18.4119, 0.0, 35.0], dtype=np.float64),
115+
(len(epoch_range), 1),
116+
),
109117
),
118+
# masking radius in degrees
110119
"angular_radius_for_masking": (
111120
["epoch", "source"],
112-
np.random.rand(len(epoch_range), 3),
121+
np.tile(
122+
np.array([2.0, 0.0, 0.0], dtype=np.float64), (len(epoch_range), 1)
123+
),
113124
),
114125
},
115126
coords={"epoch": epoch_range},

imap_processing/tests/glows/test_glows_l1b.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
HistogramL1B,
2121
PipelineSettings,
2222
)
23+
from imap_processing.spice.time import met_to_datetime64
2324
from imap_processing.tests.glows.conftest import mock_update_spice_parameters
2425

2526

@@ -33,7 +34,7 @@ def hist_dataset():
3334
"flags_set_onboard": np.zeros((20,)),
3435
"is_generated_on_ground": np.zeros((20,)),
3536
"number_of_spins_per_block": np.zeros((20,)),
36-
"number_of_bins_per_histogram": np.zeros((20,)),
37+
"number_of_bins_per_histogram": np.full((20,), 3600),
3738
"number_of_events": np.zeros((20,)),
3839
"filter_temperature_average": np.zeros((20,)),
3940
"filter_temperature_variance": np.zeros((20,)),
@@ -193,9 +194,11 @@ def ancillary_dict():
193194
return dictionary
194195

195196

197+
@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
196198
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
197199
def test_histogram_mapping(
198200
mock_spice_function,
201+
mock_flag_uv_source,
199202
mock_ancillary_exclusions,
200203
mock_ancillary_parameters,
201204
mock_pipeline_settings,
@@ -228,7 +231,7 @@ def test_histogram_mapping(
228231
0,
229232
0,
230233
0,
231-
0,
234+
3600,
232235
0,
233236
encoded_val,
234237
encoded_val,
@@ -255,9 +258,11 @@ def test_histogram_mapping(
255258
assert output[10] - expected_temp < 0.1
256259

257260

261+
@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
258262
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
259263
def test_process_histogram(
260264
mock_spice_function,
265+
mock_flag_uv_source,
261266
hist_dataset,
262267
mock_ancillary_exclusions,
263268
mock_ancillary_parameters,
@@ -289,7 +294,7 @@ def test_process_histogram(
289294
0,
290295
0,
291296
0,
292-
0,
297+
3600,
293298
0,
294299
encoded_val,
295300
encoded_val,
@@ -335,9 +340,11 @@ def test_process_de(de_dataset, ancillary_dict, mock_ancillary_parameters):
335340
assert np.isclose(output[8].data[0], expected_temp)
336341

337342

343+
@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
338344
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
339345
def test_glows_l1b(
340346
mock_spice_function,
347+
mock_flag_uv_source,
341348
de_dataset,
342349
hist_dataset,
343350
mock_ancillary_exclusions,
@@ -428,9 +435,11 @@ def test_glows_l1b(
428435
assert key in de_output
429436

430437

438+
@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
431439
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
432440
def test_generate_histogram_dataset(
433441
mock_spice_function,
442+
mock_flag_uv_source,
434443
hist_dataset,
435444
mock_ancillary_exclusions,
436445
mock_pipeline_settings,
@@ -532,6 +541,11 @@ def test_hist_spice_output(
532541
with furnish_kernels(kernels):
533542
hist_data = HistogramL1B(**params)
534543

544+
day = met_to_datetime64(hist_data.imap_start_time)
545+
day_exclusions = mock_ancillary_exclusions.limit_by_day(day)
546+
547+
mask = hist_data.flag_uv_source(day_exclusions)
548+
535549
# Assert that all these variables are the correct shape:
536550
assert isinstance(hist_data.spin_period_ground_average, np.float64)
537551
assert isinstance(hist_data.spin_period_ground_std_dev, np.float64)
@@ -543,5 +557,8 @@ def test_hist_spice_output(
543557
assert hist_data.spacecraft_location_std_dev.shape == (3,)
544558
assert hist_data.spacecraft_velocity_average.shape == (3,)
545559
assert hist_data.spacecraft_velocity_std_dev.shape == (3,)
560+
assert mask.shape == (3600,)
561+
# For 2 degree radius: 20 + 20 + 1(center) ≈ 41 bins.
562+
assert np.count_nonzero(mask) == 41
546563

547564
# TODO: Maxine will validate actual data with GLOWS team

imap_processing/tests/glows/test_glows_l1b_data.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,11 @@ def test_glows_l1b_de():
8383
assert np.allclose(pulse_len, expected_pulse)
8484

8585

86+
@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
8687
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
8788
def test_validation_data_histogram(
8889
mock_spice_function,
90+
mock_flag_uv_source,
8991
l1a_dataset,
9092
mock_ancillary_exclusions,
9193
mock_pipeline_settings,

imap_processing/tests/glows/test_glows_l2.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ def l1b_hists():
3535
return input
3636

3737

38+
@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
3839
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
3940
def test_glows_l2(
4041
mock_spice_function,
42+
mock_flag_uv_source,
4143
l1a_dataset,
4244
mock_ancillary_exclusions,
4345
mock_pipeline_settings,
@@ -60,9 +62,11 @@ def test_glows_l2(
6062
assert np.allclose(l2["filter_temperature_average"].values, [57.6], rtol=0.1)
6163

6264

65+
@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
6366
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
6467
def test_generate_l2(
6568
mock_spice_function,
69+
mock_flag_uv_source,
6670
l1a_dataset,
6771
mock_ancillary_exclusions,
6872
mock_pipeline_settings,

0 commit comments

Comments
 (0)