Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
117 changes: 109 additions & 8 deletions imap_processing/glows/l1b/glows_l1b_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@

from imap_processing.glows import FLAG_LENGTH
from imap_processing.glows.utils.constants import TimeTuple
from imap_processing.quality_flags import GLOWSL1bFlags
from imap_processing.spice import geometry
from imap_processing.spice.geometry import SpiceBody, SpiceFrame
from imap_processing.spice.geometry import (
SpiceBody,
SpiceFrame,
frame_transform,
instrument_pointing,
spherical_to_cartesian,
)
from imap_processing.spice.spin import (
get_instrument_spin_phase,
get_spin_angle,
Expand Down Expand Up @@ -819,8 +826,11 @@ def __post_init__(

# Add SPICE related variables
self.update_spice_parameters()
# Will require some additional inputs
self.imap_spin_angle_bin_cntr = np.zeros((3600,))
# Calculate the spin angle bin center
phi = (
np.arange(self.number_of_bins_per_histogram, dtype=np.float64) + 0.5
) / self.number_of_bins_per_histogram
self.imap_spin_angle_bin_cntr = phi * 360.0

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

return flags

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

Parameters
----------
exclusions : AncillaryExclusions
Ancillary exclusions data filtered for the current day.

Returns
-------
close_to_uv_source : np.ndarray
Boolean mask for uv source.
"""
# Rotate spin-angle bin centers by the instrument position-angle offset
# so azimuth=0 aligns with the instrument pointing direction.
azimuth = (
self.imap_spin_angle_bin_cntr + self.position_angle_offset_average
) % 360.0
# Ephemeris start time of the histogram accumulation.
data_start_time_et = sct_to_et(met_to_sclkticks(self.imap_start_time))

# Instrument pointing direction in the DPS frame.
dps_pointing = instrument_pointing(
data_start_time_et,
SpiceFrame.IMAP_GLOWS,
SpiceFrame.IMAP_DPS,
)
elevation = dps_pointing[1]
Comment thread
laspsandoval marked this conversation as resolved.
Outdated

spherical = np.stack(
[np.ones_like(azimuth), azimuth, np.full_like(azimuth, elevation)],
axis=-1,
) # (nbin, 3)

# Convert to unit cartesian vectors.
look_vecs_dps = spherical_to_cartesian(spherical) # (nbin, 3)
# Create ephemeris time array.
et_bins = np.full(
self.number_of_bins_per_histogram, data_start_time_et, dtype=np.float64
)
Comment thread
laspsandoval marked this conversation as resolved.
Outdated

# Transform unit cartesian vectors to ECLIPJ2000 frame.
look_vecs_ecl = frame_transform(
et_bins,
look_vecs_dps,
SpiceFrame.IMAP_DPS,
SpiceFrame.ECLIPJ2000,
)

# UV source vectors.
uv_longitude = exclusions.uv_sources[
"ecliptic_longitude_deg"
].values # (n_src,)
uv_latitude = exclusions.uv_sources["ecliptic_latitude_deg"].values # (n_src,)
uv_radius = np.deg2rad(
exclusions.uv_sources["angular_radius_for_masking"].values
)

uv_spherical = np.stack(
[np.ones_like(uv_longitude), uv_longitude, uv_latitude],
axis=-1,
) # (n_src, 3): (r, azimuth, elevation) in degrees

uv_vecs = spherical_to_cartesian(uv_spherical) # (n_src, 3)

# Dot product of unit vectors gives cos(separation_angle) for each
# histogram bin vs. each UV source -> shape (nbin, n_src).
# (nbin, 3) @ (3, n_src) -> (nbin, n_src)
# 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)

# 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
) # (nbin,)
Comment thread
laspsandoval marked this conversation as resolved.

return close_to_uv_source

def _compute_histogram_flag_array(
self, exclusions: AncillaryExclusions
) -> np.ndarray:
Expand All @@ -978,9 +1069,9 @@ 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
- Row 2: is_excluded_by_instr_team
- Row 3: is_suspected_transient
- Row 1: is_inside_excluded_region (TODO)
- Row 2: is_excluded_by_instr_team (TODO)
- Row 3: is_suspected_transient (TODO)

Parameters
----------
Expand All @@ -992,5 +1083,15 @@ def _compute_histogram_flag_array(
np.ndarray
Array of shape (4, 3600) with bad-angle flags for each bin.
"""
# TODO: fill out once spice data is available
return np.zeros((4, 3600), dtype=np.uint8)
histogram_flags = np.full(
(4, self.number_of_bins_per_histogram),
GLOWSL1bFlags.NONE.value,
dtype=np.uint8,
)

close_any = self.flag_uv_source(exclusions)

# close if within radius of any UV source
histogram_flags[0][close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.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 @@ -148,6 +148,7 @@ class GLOWSL1bFlags(FlagNameMixin):
"""Glows L1b flags."""

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


class ImapHiL1bDeFlags(FlagNameMixin):
Expand Down
17 changes: 14 additions & 3 deletions imap_processing/tests/glows/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,28 @@ def mock_ancillary_exclusions():
["epoch", "source"],
[["star1", "star2", "star3"]] * len(epoch_range),
),
# degrees in [0, 360)
"ecliptic_longitude_deg": (
["epoch", "source"],
np.random.rand(len(epoch_range), 3),
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", "source"],
np.random.rand(len(epoch_range), 3),
np.tile(
np.array([18.4119, 0.0, 35.0], dtype=np.float64),
(len(epoch_range), 1),
),
),
# masking radius in degrees
"angular_radius_for_masking": (
["epoch", "source"],
np.random.rand(len(epoch_range), 3),
np.tile(
np.array([2.0, 0.0, 0.0], dtype=np.float64), (len(epoch_range), 1)
),
),
},
coords={"epoch": epoch_range},
Expand Down
9 changes: 9 additions & 0 deletions imap_processing/tests/glows/test_glows_l1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
HistogramL1B,
PipelineSettings,
)
from imap_processing.spice.time import met_to_datetime64
from imap_processing.tests.glows.conftest import mock_update_spice_parameters


Expand Down Expand Up @@ -532,6 +533,11 @@ def test_hist_spice_output(
with furnish_kernels(kernels):
hist_data = HistogramL1B(**params)

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)

# Assert that all these variables are the correct shape:
assert isinstance(hist_data.spin_period_ground_average, np.float64)
assert isinstance(hist_data.spin_period_ground_std_dev, np.float64)
Expand All @@ -543,5 +549,8 @@ 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,)
# For 2 degree radius: 20 + 20 + 1(center) ≈ 41 bins.
assert np.count_nonzero(mask) == 41

# TODO: Maxine will validate actual data with GLOWS team
4 changes: 4 additions & 0 deletions imap_processing/tests/glows/test_glows_l2.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ def l1b_hists():
return input


@patch.object(HistogramL1B, "flag_uv_source", return_value=np.zeros(3600, dtype=bool))
@patch.object(HistogramL1B, "update_spice_parameters", autospec=True)
def test_glows_l2(
mock_spice_function,
mock_flag_uv_source,
l1a_dataset,
mock_ancillary_exclusions,
mock_pipeline_settings,
Expand All @@ -60,9 +62,11 @@ 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, "update_spice_parameters", autospec=True)
def test_generate_l2(
mock_spice_function,
mock_flag_uv_source,
l1a_dataset,
mock_ancillary_exclusions,
mock_pipeline_settings,
Expand Down
Loading