Skip to content
Merged
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
51 changes: 44 additions & 7 deletions imap_processing/glows/l1b/glows_l1b_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ def deserialize_flags(raw: int) -> np.ndarray[int]:

return flags

def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray:
def flag_uv_and_excluded(self, exclusions: AncillaryExclusions) -> tuple:
"""
Create boolean mask where True means bin is within radius of UV source.

Expand All @@ -992,6 +992,8 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray:
-------
close_to_uv_source : np.ndarray
Boolean mask for uv source.
inside_excluded_region : np.ndarray
Boolean mask for inside excluded region.
"""
# Rotate spin-angle bin centers by the instrument position-angle offset
# so azimuth=0 aligns with the instrument pointing direction.
Expand Down Expand Up @@ -1043,14 +1045,40 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray:
# If dot product -> 1 the two vectors point in almost
# the same direction and needs mask.
# If dot product -> 0 the two directions are perpendicular on the sky.
cos_sep = look_vecs_ecl @ uv_vecs.T # (nbin, n_src)
uv_cos_sep = look_vecs_ecl @ uv_vecs.T # (nbin, n_src)

# Determine if the pixel is too close to any of the source radii.
close_to_uv_source = np.any(
cos_sep >= np.cos(uv_radius)[None, :], axis=1
uv_cos_sep >= np.cos(uv_radius)[None, :], axis=1
) # (nbin,)

return close_to_uv_source
# Excluded region pixel centers.
region_longitude = exclusions.excluded_regions[
"ecliptic_longitude_deg"
].values # (n_region,)
region_latitude = exclusions.excluded_regions[
"ecliptic_latitude_deg"
].values # (n_region,)

region_spherical = np.stack(
[np.ones_like(region_longitude), region_longitude, region_latitude],
axis=-1,
) # (n_region, 3)

region_vecs = spherical_to_cartesian(region_spherical) # (n_region, 3)

# (nbin, 3) @ (3, n_region) -> (nbin, n_region)
region_cos_sep = look_vecs_ecl @ region_vecs.T

# Flag any bin whose pointing direction falls within half a bin width
# (0.1° / 2 = 0.05°) of an excluded sky direction.
half_bin_rad = np.deg2rad(0.1 / 2)

inside_excluded_region = np.any(
region_cos_sep >= np.cos(half_bin_rad), axis=1
) # (nbin,)

return close_to_uv_source, inside_excluded_region

def _compute_histogram_flag_array(
self, exclusions: AncillaryExclusions
Expand All @@ -1060,7 +1088,7 @@ def _compute_histogram_flag_array(

Creates a (4, 3600) array where each row represents a different flag type:
- Row 0: is_close_to_uv_source
- Row 1: is_inside_excluded_region (TODO)
- Row 1: is_inside_excluded_region
- Row 2: is_excluded_by_instr_team (TODO)
- Row 3: is_suspected_transient (TODO)

Expand All @@ -1080,9 +1108,18 @@ def _compute_histogram_flag_array(
dtype=np.uint8,
)

close_any = self.flag_uv_source(exclusions)
close_to_uv_source, inside_excluded_region = self.flag_uv_and_excluded(
exclusions
)

# close if within radius of any UV source
histogram_flags[0][close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value
histogram_flags[0][close_to_uv_source] |= (
GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value
)

# inside if within half pixel size of any excluded region center
histogram_flags[1][inside_excluded_region] |= (
GLOWSL1bFlags.IS_INSIDE_EXCLUDED_REGION.value
)

return histogram_flags
1 change: 1 addition & 0 deletions imap_processing/quality_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class GLOWSL1bFlags(FlagNameMixin):

NONE = CommonFlags.NONE
IS_CLOSE_TO_UV_SOURCE = 2**0 # Is the bin close to a UV source.
IS_INSIDE_EXCLUDED_REGION = 2**1 # Is the bin inside an excluded sky region.


class ImapHiL1bDeFlags(FlagNameMixin):
Expand Down
16 changes: 12 additions & 4 deletions imap_processing/tests/glows/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,21 @@ def mock_ancillary_exclusions():
# Create datasets with epoch dimension and some mock data
mock_excluded_regions = xr.Dataset(
{
# degrees in [0, 360)
"ecliptic_longitude_deg": (
["epoch", "region"],
np.random.rand(len(epoch_range), 5),
["epoch", "source"],
np.tile(
np.array([202.0812, 120.0, 250.0], dtype=np.float64),
(len(epoch_range), 1),
),
),
# degrees in [-90, 90]
"ecliptic_latitude_deg": (
["epoch", "region"],
np.random.rand(len(epoch_range), 5),
["epoch", "source"],
np.tile(
np.array([18.4119, 0.0, 35.0], dtype=np.float64),
(len(epoch_range), 1),
),
),
},
coords={"epoch": epoch_range},
Expand Down
49 changes: 36 additions & 13 deletions imap_processing/tests/glows/test_glows_l1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,15 @@ def ancillary_dict():
return dictionary


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_histogram_mapping(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
mock_ancillary_exclusions,
mock_ancillary_parameters,
mock_pipeline_settings,
Expand Down Expand Up @@ -258,11 +262,15 @@ def test_histogram_mapping(
assert output[10] - expected_temp < 0.1


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_process_histogram(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
hist_dataset,
mock_ancillary_exclusions,
mock_ancillary_parameters,
Expand Down Expand Up @@ -322,11 +330,15 @@ def test_process_histogram(
assert len(output) == len(dataclasses.asdict(test_l1b))


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_bins_from_histogram_not_nbins(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
hist_dataset,
mock_ancillary_exclusions,
mock_ancillary_parameters,
Expand Down Expand Up @@ -372,11 +384,15 @@ def test_process_de(de_dataset, ancillary_dict, mock_ancillary_parameters):
assert np.isclose(output[8].data[0], expected_temp)


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_glows_l1b(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
de_dataset,
hist_dataset,
mock_ancillary_exclusions,
Expand Down Expand Up @@ -467,11 +483,15 @@ def test_glows_l1b(
assert key in de_output


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_generate_histogram_dataset(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
hist_dataset,
mock_ancillary_exclusions,
mock_pipeline_settings,
Expand Down Expand Up @@ -576,7 +596,7 @@ def test_hist_spice_output(
day = met_to_datetime64(hist_data.imap_start_time)
day_exclusions = mock_ancillary_exclusions.limit_by_day(day)

mask = hist_data.flag_uv_source(day_exclusions)
uv_mask, region_mask = hist_data.flag_uv_and_excluded(day_exclusions)

# Assert that all these variables are the correct shape:
assert isinstance(hist_data.spin_period_ground_average, np.float64)
Expand All @@ -589,8 +609,11 @@ def test_hist_spice_output(
assert hist_data.spacecraft_location_std_dev.shape == (3,)
assert hist_data.spacecraft_velocity_average.shape == (3,)
assert hist_data.spacecraft_velocity_std_dev.shape == (3,)
assert mask.shape == (3600,)
assert uv_mask.shape == (3600,)
# For 2 degree radius: 20 + 20 + 1(center) ≈ 41 bins.
assert np.count_nonzero(mask) == 41
assert np.count_nonzero(uv_mask) == 41
# Each individual excluded region center can only flag 0 or 1 bins
# (since the 0.05° threshold is exactly half the 0.1° bin spacing.
assert np.count_nonzero(region_mask) == 1

# TODO: Maxine will validate actual data with GLOWS team
8 changes: 6 additions & 2 deletions imap_processing/tests/glows/test_glows_l1b_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,15 @@ def test_glows_l1b_de():
assert np.allclose(pulse_len, expected_pulse)


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_validation_data_histogram(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
l1a_dataset,
mock_ancillary_exclusions,
mock_pipeline_settings,
Expand Down
16 changes: 12 additions & 4 deletions imap_processing/tests/glows/test_glows_l2.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ def l1b_hists():
return input


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_glows_l2(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
l1a_dataset,
mock_ancillary_exclusions,
mock_pipeline_settings,
Expand All @@ -62,11 +66,15 @@ def test_glows_l2(
assert np.allclose(l2["filter_temperature_average"].values, [57.6], rtol=0.1)


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(
HistogramL1B,
"flag_uv_and_excluded",
return_value=(np.zeros(3600, dtype=bool), np.zeros(3600, dtype=bool)),
)
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_generate_l2(
mock_spice_function,
mock_flag_uv_source,
mock_flag_uv_and_excluded,
l1a_dataset,
mock_ancillary_exclusions,
mock_pipeline_settings,
Expand Down