From 959ca79b46f3263c894dcb95db4e1c77d807f2d0 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Mon, 23 Feb 2026 15:27:53 -0700 Subject: [PATCH 01/17] improve test data --- imap_processing/tests/glows/conftest.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/imap_processing/tests/glows/conftest.py b/imap_processing/tests/glows/conftest.py index bb6fa285a..c6f9a0b7f 100644 --- a/imap_processing/tests/glows/conftest.py +++ b/imap_processing/tests/glows/conftest.py @@ -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([10.0, 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([-20.0, 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, 5.0, 1.0], dtype=np.float64), (len(epoch_range), 1) + ), ), }, coords={"epoch": epoch_range}, From d1e70fffd2630fccd3904e5ca08151a558daed86 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Mon, 23 Feb 2026 16:42:18 -0700 Subject: [PATCH 02/17] first pass --- imap_processing/glows/l1b/glows_l1b_data.py | 79 +++++++++++++++++++-- imap_processing/quality_flags.py | 1 + 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index a5409f187..5cf120577 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -18,6 +18,9 @@ ) from imap_processing.spice.time import met_to_datetime64, met_to_sclkticks, sct_to_et +from imap_processing.quality_flags import GLOWSL1bFlags +from imap_processing.spice.geometry import instrument_pointing + @dataclass class PipelineSettings: # numpydoc ignore=PR02 @@ -819,8 +822,9 @@ 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. @@ -970,6 +974,62 @@ def deserialize_flags(raw: int) -> np.ndarray[int]: return flags + def flag_uv_source(self, exclusions: AncillaryExclusions): + + effective_spin_angle_deg = (self.imap_spin_angle_bin_cntr + self.position_angle_offset_average) % 360.0 + + data_start_time_et = sct_to_et(met_to_sclkticks(self.imap_start_time)) + time_for_each_bin = np.full(self.number_of_bins_per_histogram, data_start_time_et) + + uv_lon = np.deg2rad(exclusions.uv_sources["ecliptic_longitude_deg"].values) + uv_lat = np.deg2rad(exclusions.uv_sources["ecliptic_latitude_deg"].values) + uv_rad = np.deg2rad(exclusions.uv_sources["angular_radius_for_masking"].values) + + uv_vecs = np.stack( + [ + np.cos(uv_lat) * np.cos(uv_lon), + np.cos(uv_lat) * np.sin(uv_lon), + np.sin(uv_lat), + ], + axis=1, + ) # (n_src, 3) + + # Get GLOWS boresight lon/lat in DPS at block start time. + # In DPS, latitude is the off-pointing of the boresight; treat it as constant across bins. + look_lonlat_dps = geometry.instrument_pointing( + data_start_time_et, SpiceFrame.IMAP_GLOWS, SpiceFrame.IMAP_DPS + ) # shape (2,) for scalar et + offpoint_deg = float(look_lonlat_dps[1]) + + # Build per-bin (az, el) in DPS: az = spin angle bin center, el = offpoint + look_az_el_dps = np.column_stack( + [effective_spin_angle_deg, np.full(self.number_of_bins_per_histogram, offpoint_deg)] + ) # (nbin, 2) + + # Convert DPS az/el -> DPS cartesian unit vectors + # frame_transform_az_el uses spherical_to_cartesian under the hood with r=1; + # doing it explicitly is often clearer: + look_sph_dps = np.column_stack( + [np.ones(self.number_of_bins_per_histogram), look_az_el_dps] + ) # (nbin, 3) : (r, az, el) + look_vecs_dps = geometry.spherical_to_cartesian(look_sph_dps) # (nbin, 3) + + # Transform look vectors into ECLIPJ2000 to compare with UV sources (which are ecliptic lon/lat) + look_vecs = geometry.frame_transform( + time_for_each_bin, + look_vecs_dps, + SpiceFrame.IMAP_DPS, + SpiceFrame.ECLIPJ2000, + ) # (nbin, 3) + + cos_sep = look_vecs @ uv_vecs.T # (nbin, n_src) + cos_sep = np.clip(cos_sep, -1.0, 1.0) + sep_angle = np.arccos(cos_sep) # radians, (nbin, n_src) + + return sep_angle, uv_rad + + + def _compute_histogram_flag_array( self, exclusions: AncillaryExclusions ) -> np.ndarray: @@ -992,5 +1052,16 @@ 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( + self.number_of_bins_per_histogram, GLOWSL1bFlags.NONE.value, dtype=np.uint8 + ) + + sep_angle, uv_rad = self.flag_uv_source(exclusions) + + # close if within radius of any UV source + close_any = np.any(sep_angle <= uv_rad[None, :], axis=1) # (nbin,) + histogram_flags[close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value + + return histogram_flags + + diff --git a/imap_processing/quality_flags.py b/imap_processing/quality_flags.py index 84119f650..ba28b754c 100644 --- a/imap_processing/quality_flags.py +++ b/imap_processing/quality_flags.py @@ -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): From 3c16c6f0662238aade8e33610f6fe2ba65e87f6a Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 08:37:25 -0700 Subject: [PATCH 03/17] second pass --- imap_processing/glows/l1b/glows_l1b_data.py | 66 ++++++++------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 5cf120577..93bdd2254 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -976,55 +976,41 @@ def deserialize_flags(raw: int) -> np.ndarray[int]: def flag_uv_source(self, exclusions: AncillaryExclusions): - effective_spin_angle_deg = (self.imap_spin_angle_bin_cntr + self.position_angle_offset_average) % 360.0 - - data_start_time_et = sct_to_et(met_to_sclkticks(self.imap_start_time)) - time_for_each_bin = np.full(self.number_of_bins_per_histogram, data_start_time_et) + # 1) Spin angle for each histogram bin (deg) — you already have this + effective_spin_angle_deg = ( + self.imap_spin_angle_bin_cntr + self.position_angle_offset_average + ) % 360.0 + + # 2) Choose an off-pointing (latitude in DPS). If you don't model it, use 0 deg. + offpoint_deg = 0.0 + + # 3) Build per-bin look unit vectors in DPS from (lon, lat) + lon = np.deg2rad(effective_spin_angle_deg) # (nbin,) + lat = np.deg2rad(offpoint_deg) # scalar + + look_vecs = np.column_stack( + ( + np.cos(lat) * np.cos(lon), + np.cos(lat) * np.sin(lon), + np.sin(lat) * np.ones_like(lon), + ) + ) # shape (nbin, 3) - uv_lon = np.deg2rad(exclusions.uv_sources["ecliptic_longitude_deg"].values) - uv_lat = np.deg2rad(exclusions.uv_sources["ecliptic_latitude_deg"].values) - uv_rad = np.deg2rad(exclusions.uv_sources["angular_radius_for_masking"].values) + uv_lon = np.deg2rad(exclusions.uv_sources["ecliptic_longitude_deg"].values) # (n_src,) + uv_lat = np.deg2rad(exclusions.uv_sources["ecliptic_latitude_deg"].values) # (n_src,) + uv_rad = np.deg2rad(exclusions.uv_sources["angular_radius_for_masking"].values) # (n_src,) - uv_vecs = np.stack( - [ + uv_vecs = np.column_stack( + ( np.cos(uv_lat) * np.cos(uv_lon), np.cos(uv_lat) * np.sin(uv_lon), np.sin(uv_lat), - ], - axis=1, + ) ) # (n_src, 3) - # Get GLOWS boresight lon/lat in DPS at block start time. - # In DPS, latitude is the off-pointing of the boresight; treat it as constant across bins. - look_lonlat_dps = geometry.instrument_pointing( - data_start_time_et, SpiceFrame.IMAP_GLOWS, SpiceFrame.IMAP_DPS - ) # shape (2,) for scalar et - offpoint_deg = float(look_lonlat_dps[1]) - - # Build per-bin (az, el) in DPS: az = spin angle bin center, el = offpoint - look_az_el_dps = np.column_stack( - [effective_spin_angle_deg, np.full(self.number_of_bins_per_histogram, offpoint_deg)] - ) # (nbin, 2) - - # Convert DPS az/el -> DPS cartesian unit vectors - # frame_transform_az_el uses spherical_to_cartesian under the hood with r=1; - # doing it explicitly is often clearer: - look_sph_dps = np.column_stack( - [np.ones(self.number_of_bins_per_histogram), look_az_el_dps] - ) # (nbin, 3) : (r, az, el) - look_vecs_dps = geometry.spherical_to_cartesian(look_sph_dps) # (nbin, 3) - - # Transform look vectors into ECLIPJ2000 to compare with UV sources (which are ecliptic lon/lat) - look_vecs = geometry.frame_transform( - time_for_each_bin, - look_vecs_dps, - SpiceFrame.IMAP_DPS, - SpiceFrame.ECLIPJ2000, - ) # (nbin, 3) - cos_sep = look_vecs @ uv_vecs.T # (nbin, n_src) cos_sep = np.clip(cos_sep, -1.0, 1.0) - sep_angle = np.arccos(cos_sep) # radians, (nbin, n_src) + sep_angle = np.arccos(cos_sep) return sep_angle, uv_rad From 5247b4a075a8e3b2b12376a53bc4946d23a978fe Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 08:39:04 -0700 Subject: [PATCH 04/17] third pass --- imap_processing/glows/l1b/glows_l1b_data.py | 44 ++++++++++----------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 93bdd2254..2990fdbc1 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -974,45 +974,41 @@ def deserialize_flags(raw: int) -> np.ndarray[int]: return flags - def flag_uv_source(self, exclusions: AncillaryExclusions): - - # 1) Spin angle for each histogram bin (deg) — you already have this - effective_spin_angle_deg = ( - self.imap_spin_angle_bin_cntr + self.position_angle_offset_average - ) % 360.0 + def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: + """ + Returns a boolean mask (nbin,) where True means the bin is within the + masking radius of any UV source. + """ + # Spin angle for each histogram bin (deg) + lon_deg = (self.imap_spin_angle_bin_cntr + self.position_angle_offset_average) % 360.0 - # 2) Choose an off-pointing (latitude in DPS). If you don't model it, use 0 deg. + # If you aren't modeling off-pointing yet, keep it constant offpoint_deg = 0.0 - # 3) Build per-bin look unit vectors in DPS from (lon, lat) - lon = np.deg2rad(effective_spin_angle_deg) # (nbin,) + # Look vectors in DPS + lon = np.deg2rad(lon_deg) # (nbin,) lat = np.deg2rad(offpoint_deg) # scalar - + coslat = np.cos(lat) look_vecs = np.column_stack( - ( - np.cos(lat) * np.cos(lon), - np.cos(lat) * np.sin(lon), - np.sin(lat) * np.ones_like(lon), - ) - ) # shape (nbin, 3) + (coslat * np.cos(lon), coslat * np.sin(lon), np.sin(lat) * np.ones_like(lon)) + ) # (nbin, 3) + # UV source vectors (assumed already in DPS lon/lat) uv_lon = np.deg2rad(exclusions.uv_sources["ecliptic_longitude_deg"].values) # (n_src,) uv_lat = np.deg2rad(exclusions.uv_sources["ecliptic_latitude_deg"].values) # (n_src,) uv_rad = np.deg2rad(exclusions.uv_sources["angular_radius_for_masking"].values) # (n_src,) uv_vecs = np.column_stack( - ( - np.cos(uv_lat) * np.cos(uv_lon), - np.cos(uv_lat) * np.sin(uv_lon), - np.sin(uv_lat), - ) + (np.cos(uv_lat) * np.cos(uv_lon), np.cos(uv_lat) * np.sin(uv_lon), np.sin(uv_lat)) ) # (n_src, 3) + # Cosine of separation angles cos_sep = look_vecs @ uv_vecs.T # (nbin, n_src) - cos_sep = np.clip(cos_sep, -1.0, 1.0) - sep_angle = np.arccos(cos_sep) - return sep_angle, uv_rad + # Close if within any source radius + close_any = np.any(cos_sep >= np.cos(uv_rad)[None, :], axis=1) # (nbin,) + + return close_any From a669897f82c2fb5f9069daae73b03fb6dcc74a65 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 09:14:54 -0700 Subject: [PATCH 05/17] fourth pass --- imap_processing/glows/l1b/glows_l1b_data.py | 24 ++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 2990fdbc1..e3374e53f 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -981,19 +981,33 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: """ # Spin angle for each histogram bin (deg) lon_deg = (self.imap_spin_angle_bin_cntr + self.position_angle_offset_average) % 360.0 + # Start time of the spin. + data_start_time_et = sct_to_et(met_to_sclkticks(self.imap_start_time)) - # If you aren't modeling off-pointing yet, keep it constant - offpoint_deg = 0.0 + look_lonlat = geometry.instrument_pointing( + data_start_time_et, + SpiceFrame.IMAP_GLOWS, + SpiceFrame.IMAP_DPS, + ) + offpoint_deg = look_lonlat[1] # Look vectors in DPS lon = np.deg2rad(lon_deg) # (nbin,) lat = np.deg2rad(offpoint_deg) # scalar coslat = np.cos(lat) - look_vecs = np.column_stack( + look_vecs_dps = np.column_stack( (coslat * np.cos(lon), coslat * np.sin(lon), np.sin(lat) * np.ones_like(lon)) ) # (nbin, 3) - # UV source vectors (assumed already in DPS lon/lat) + # Transform look vectors to ECLIPJ2000 + look_vecs_ecl = geometry.frame_transform( + data_start_time_et, + look_vecs_dps, + SpiceFrame.IMAP_DPS, + SpiceFrame.ECLIPJ2000, + ) + + # UV source vectors uv_lon = np.deg2rad(exclusions.uv_sources["ecliptic_longitude_deg"].values) # (n_src,) uv_lat = np.deg2rad(exclusions.uv_sources["ecliptic_latitude_deg"].values) # (n_src,) uv_rad = np.deg2rad(exclusions.uv_sources["angular_radius_for_masking"].values) # (n_src,) @@ -1003,7 +1017,7 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: ) # (n_src, 3) # Cosine of separation angles - cos_sep = look_vecs @ uv_vecs.T # (nbin, n_src) + cos_sep = look_vecs_ecl @ uv_vecs.T # (nbin, n_src) # Close if within any source radius close_any = np.any(cos_sep >= np.cos(uv_rad)[None, :], axis=1) # (nbin,) From 6a6f6bbf932dece913ddc5a78261710ec66bfe32 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 09:19:11 -0700 Subject: [PATCH 06/17] fourth pass --- imap_processing/glows/l1b/glows_l1b_data.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index e3374e53f..513117194 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -999,9 +999,11 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: (coslat * np.cos(lon), coslat * np.sin(lon), np.sin(lat) * np.ones_like(lon)) ) # (nbin, 3) + et_bins = np.full(self.number_of_bins_per_histogram, data_start_time_et, dtype=np.float64) + # Transform look vectors to ECLIPJ2000 look_vecs_ecl = geometry.frame_transform( - data_start_time_et, + et_bins, look_vecs_dps, SpiceFrame.IMAP_DPS, SpiceFrame.ECLIPJ2000, @@ -1032,11 +1034,11 @@ def _compute_histogram_flag_array( """ Compute the histogram flag array for bad-angle flags. - 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 + Creates a n = 3600 array for flag types: + - is_close_to_uv_source + - is_inside_excluded_region (TODO) + - is_excluded_by_instr_team (TODO) + - is_suspected_transient (TODO) Parameters ---------- @@ -1046,16 +1048,15 @@ def _compute_histogram_flag_array( Returns ------- np.ndarray - Array of shape (4, 3600) with bad-angle flags for each bin. + Array of shape n = 3600 with bad-angle flags for each bin. """ histogram_flags = np.full( self.number_of_bins_per_histogram, GLOWSL1bFlags.NONE.value, dtype=np.uint8 ) - sep_angle, uv_rad = self.flag_uv_source(exclusions) + close_any = self.flag_uv_source(exclusions) # close if within radius of any UV source - close_any = np.any(sep_angle <= uv_rad[None, :], axis=1) # (nbin,) histogram_flags[close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value return histogram_flags From 1b0a16fbdc3affd74d4aadb3bc625579f7333e8d Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 09:50:51 -0700 Subject: [PATCH 07/17] fourth pass --- imap_processing/glows/l1b/glows_l1b_data.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 513117194..6af765148 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -974,6 +974,7 @@ def deserialize_flags(raw: int) -> np.ndarray[int]: return flags + def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: """ Returns a boolean mask (nbin,) where True means the bin is within the @@ -1027,18 +1028,17 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: return close_any - def _compute_histogram_flag_array( self, exclusions: AncillaryExclusions ) -> np.ndarray: """ Compute the histogram flag array for bad-angle flags. - Creates a n = 3600 array for flag types: - - is_close_to_uv_source - - is_inside_excluded_region (TODO) - - is_excluded_by_instr_team (TODO) - - is_suspected_transient (TODO) + 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 Parameters ---------- @@ -1048,16 +1048,16 @@ def _compute_histogram_flag_array( Returns ------- np.ndarray - Array of shape n = 3600 with bad-angle flags for each bin. + Array of shape (4, 3600) with bad-angle flags for each bin. """ histogram_flags = np.full( - self.number_of_bins_per_histogram, GLOWSL1bFlags.NONE.value, dtype=np.uint8 + (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[close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value + histogram_flags[0][close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value return histogram_flags From 1e4990414710d3482a2c8b5b3de59165ef791b65 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 11:15:49 -0700 Subject: [PATCH 08/17] update comments --- imap_processing/glows/l1b/glows_l1b_data.py | 82 ++++++++++++--------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 6af765148..f8b4ab5db 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -9,6 +9,7 @@ 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.spin import ( @@ -18,9 +19,6 @@ ) from imap_processing.spice.time import met_to_datetime64, met_to_sclkticks, sct_to_et -from imap_processing.quality_flags import GLOWSL1bFlags -from imap_processing.spice.geometry import instrument_pointing - @dataclass class PipelineSettings: # numpydoc ignore=PR02 @@ -823,7 +821,9 @@ def __post_init__( # Add SPICE related variables self.update_spice_parameters() # 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 + 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 @@ -974,35 +974,40 @@ def deserialize_flags(raw: int) -> np.ndarray[int]: return flags - def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: """ Returns a boolean mask (nbin,) where True means the bin is within the masking radius of any UV source. """ - # Spin angle for each histogram bin (deg) - lon_deg = (self.imap_spin_angle_bin_cntr + self.position_angle_offset_average) % 360.0 - # Start time of the spin. + # 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 spin. data_start_time_et = sct_to_et(met_to_sclkticks(self.imap_start_time)) - look_lonlat = geometry.instrument_pointing( + # Instrument pointing direction in the DPS frame. + dps_pointing = geometry.instrument_pointing( data_start_time_et, SpiceFrame.IMAP_GLOWS, SpiceFrame.IMAP_DPS, ) - offpoint_deg = look_lonlat[1] - - # Look vectors in DPS - lon = np.deg2rad(lon_deg) # (nbin,) - lat = np.deg2rad(offpoint_deg) # scalar - coslat = np.cos(lat) - look_vecs_dps = np.column_stack( - (coslat * np.cos(lon), coslat * np.sin(lon), np.sin(lat) * np.ones_like(lon)) + elevation = dps_pointing[1] + + spherical = np.stack( + [np.ones_like(azimuth), azimuth, np.full_like(azimuth, elevation)], + axis=-1, ) # (nbin, 3) - et_bins = np.full(self.number_of_bins_per_histogram, data_start_time_et, dtype=np.float64) + # Convert to unit cartesian vectors. + look_vecs_dps = geometry.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 + ) - # Transform look vectors to ECLIPJ2000 + # Transform unit cartesian vectors to ECLIPJ2000 frame. look_vecs_ecl = geometry.frame_transform( et_bins, look_vecs_dps, @@ -1010,24 +1015,35 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: SpiceFrame.ECLIPJ2000, ) - # UV source vectors - uv_lon = np.deg2rad(exclusions.uv_sources["ecliptic_longitude_deg"].values) # (n_src,) - uv_lat = np.deg2rad(exclusions.uv_sources["ecliptic_latitude_deg"].values) # (n_src,) - uv_rad = np.deg2rad(exclusions.uv_sources["angular_radius_for_masking"].values) # (n_src,) + # 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 = np.column_stack( - (np.cos(uv_lat) * np.cos(uv_lon), np.cos(uv_lat) * np.sin(uv_lon), np.sin(uv_lat)) - ) # (n_src, 3) + uv_vecs = geometry.spherical_to_cartesian(uv_spherical) # (n_src, 3) - # Cosine of separation angles + # 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) - # Close if within any source radius - close_any = np.any(cos_sep >= np.cos(uv_rad)[None, :], axis=1) # (nbin,) + # Determine if the pixel is too close to any of the source radii. + close_any = np.any(cos_sep >= np.cos(uv_radius)[None, :], axis=1) # (nbin,) return close_any - def _compute_histogram_flag_array( self, exclusions: AncillaryExclusions ) -> np.ndarray: @@ -1051,7 +1067,9 @@ def _compute_histogram_flag_array( Array of shape (4, 3600) with bad-angle flags for each bin. """ histogram_flags = np.full( - (4, self.number_of_bins_per_histogram), GLOWSL1bFlags.NONE.value, dtype=np.uint8 + (4, self.number_of_bins_per_histogram), + GLOWSL1bFlags.NONE.value, + dtype=np.uint8, ) close_any = self.flag_uv_source(exclusions) @@ -1060,5 +1078,3 @@ def _compute_histogram_flag_array( histogram_flags[0][close_any] |= GLOWSL1bFlags.IS_CLOSE_TO_UV_SOURCE.value return histogram_flags - - From 85bed99f52c085cfe7b9440e5e9a42740662b463 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 11:26:34 -0700 Subject: [PATCH 09/17] update comments --- imap_processing/glows/l1b/glows_l1b_data.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index f8b4ab5db..13aa1a08c 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -976,15 +976,24 @@ def deserialize_flags(raw: int) -> np.ndarray[int]: def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: """ - Returns a boolean mask (nbin,) where True means the bin is within the - masking radius of any UV source. + 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_any : np.ndarray + Boolean mask. """ # 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 spin. + # 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. From af2181d5230543053d4f9d2e7f42d0e66d7f8a6a Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 12:00:15 -0700 Subject: [PATCH 10/17] improve tests --- imap_processing/glows/l1b/glows_l1b_data.py | 16 +++++++++++----- imap_processing/tests/glows/conftest.py | 6 +++--- imap_processing/tests/glows/test_glows_l1b.py | 9 +++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 13aa1a08c..a4901b2c8 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -11,7 +11,13 @@ 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, @@ -997,7 +1003,7 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: data_start_time_et = sct_to_et(met_to_sclkticks(self.imap_start_time)) # Instrument pointing direction in the DPS frame. - dps_pointing = geometry.instrument_pointing( + dps_pointing = instrument_pointing( data_start_time_et, SpiceFrame.IMAP_GLOWS, SpiceFrame.IMAP_DPS, @@ -1010,14 +1016,14 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: ) # (nbin, 3) # Convert to unit cartesian vectors. - look_vecs_dps = geometry.spherical_to_cartesian(spherical) # (nbin, 3) + 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 ) # Transform unit cartesian vectors to ECLIPJ2000 frame. - look_vecs_ecl = geometry.frame_transform( + look_vecs_ecl = frame_transform( et_bins, look_vecs_dps, SpiceFrame.IMAP_DPS, @@ -1038,7 +1044,7 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: axis=-1, ) # (n_src, 3): (r, azimuth, elevation) in degrees - uv_vecs = geometry.spherical_to_cartesian(uv_spherical) # (n_src, 3) + 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). diff --git a/imap_processing/tests/glows/conftest.py b/imap_processing/tests/glows/conftest.py index c6f9a0b7f..9083a8f18 100644 --- a/imap_processing/tests/glows/conftest.py +++ b/imap_processing/tests/glows/conftest.py @@ -103,7 +103,7 @@ def mock_ancillary_exclusions(): "ecliptic_longitude_deg": ( ["epoch", "source"], np.tile( - np.array([10.0, 120.0, 250.0], dtype=np.float64), + np.array([202.0812, 120.0, 250.0], dtype=np.float64), (len(epoch_range), 1), ), ), @@ -111,7 +111,7 @@ def mock_ancillary_exclusions(): "ecliptic_latitude_deg": ( ["epoch", "source"], np.tile( - np.array([-20.0, 0.0, 35.0], dtype=np.float64), + np.array([18.4119, 0.0, 35.0], dtype=np.float64), (len(epoch_range), 1), ), ), @@ -119,7 +119,7 @@ def mock_ancillary_exclusions(): "angular_radius_for_masking": ( ["epoch", "source"], np.tile( - np.array([2.0, 5.0, 1.0], dtype=np.float64), (len(epoch_range), 1) + np.array([2.0, 0.0, 0.0], dtype=np.float64), (len(epoch_range), 1) ), ), }, diff --git a/imap_processing/tests/glows/test_glows_l1b.py b/imap_processing/tests/glows/test_glows_l1b.py index 3fec7ba0a..6887f893e 100644 --- a/imap_processing/tests/glows/test_glows_l1b.py +++ b/imap_processing/tests/glows/test_glows_l1b.py @@ -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 @@ -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) @@ -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 From e1fc3fb8f347879687f930c2c88bf0ff53304309 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 12:12:56 -0700 Subject: [PATCH 11/17] improve tests --- imap_processing/glows/l1b/glows_l1b_data.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index a4901b2c8..67adb56c0 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -991,8 +991,8 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: Returns ------- - close_any : np.ndarray - Boolean mask. + 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. @@ -1055,9 +1055,11 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: 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_any = np.any(cos_sep >= np.cos(uv_radius)[None, :], axis=1) # (nbin,) + close_to_uv_source = np.any( + cos_sep >= np.cos(uv_radius)[None, :], axis=1 + ) # (nbin,) - return close_any + return close_to_uv_source def _compute_histogram_flag_array( self, exclusions: AncillaryExclusions @@ -1067,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 ---------- From 0e2a0f7c5345198e41429dc2a6a095d71b555a51 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 24 Feb 2026 15:46:15 -0700 Subject: [PATCH 12/17] fix test --- imap_processing/tests/glows/test_glows_l2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/imap_processing/tests/glows/test_glows_l2.py b/imap_processing/tests/glows/test_glows_l2.py index e73233a77..d4df5fda3 100644 --- a/imap_processing/tests/glows/test_glows_l2.py +++ b/imap_processing/tests/glows/test_glows_l2.py @@ -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, @@ -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, From ce8631486f13114874f721e3c6788714431b24e8 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Wed, 25 Feb 2026 13:29:32 -0700 Subject: [PATCH 13/17] update tests --- imap_processing/tests/glows/test_glows_l1b_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imap_processing/tests/glows/test_glows_l1b_data.py b/imap_processing/tests/glows/test_glows_l1b_data.py index d7f9114bb..eaaf43a07 100644 --- a/imap_processing/tests/glows/test_glows_l1b_data.py +++ b/imap_processing/tests/glows/test_glows_l1b_data.py @@ -83,9 +83,11 @@ 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, "update_spice_parameters", autospec=True) def test_validation_data_histogram( mock_spice_function, + mock_flag_uv_source, l1a_dataset, mock_ancillary_exclusions, mock_pipeline_settings, From 9ae55c01f3d94675bc0ed7461ea2675e218c5b54 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Wed, 25 Feb 2026 13:36:45 -0700 Subject: [PATCH 14/17] update tests --- imap_processing/tests/glows/test_glows_l1b.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/imap_processing/tests/glows/test_glows_l1b.py b/imap_processing/tests/glows/test_glows_l1b.py index 6887f893e..4b621f5c8 100644 --- a/imap_processing/tests/glows/test_glows_l1b.py +++ b/imap_processing/tests/glows/test_glows_l1b.py @@ -34,7 +34,7 @@ def hist_dataset(): "flags_set_onboard": np.zeros((20,)), "is_generated_on_ground": np.zeros((20,)), "number_of_spins_per_block": np.zeros((20,)), - "number_of_bins_per_histogram": np.zeros((20,)), + "number_of_bins_per_histogram": np.full((20,), 3600), "number_of_events": np.zeros((20,)), "filter_temperature_average": np.zeros((20,)), "filter_temperature_variance": np.zeros((20,)), @@ -194,9 +194,11 @@ def ancillary_dict(): return dictionary +@patch.object(HistogramL1B, "flag_uv_source", return_value=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_ancillary_exclusions, mock_ancillary_parameters, mock_pipeline_settings, @@ -229,7 +231,7 @@ def test_histogram_mapping( 0, 0, 0, - 0, + 3600, 0, encoded_val, encoded_val, @@ -256,9 +258,11 @@ 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, "update_spice_parameters", autospec=True) def test_process_histogram( mock_spice_function, + mock_flag_uv_source, hist_dataset, mock_ancillary_exclusions, mock_ancillary_parameters, @@ -290,7 +294,7 @@ def test_process_histogram( 0, 0, 0, - 0, + 3600, 0, encoded_val, encoded_val, @@ -336,9 +340,11 @@ 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, "update_spice_parameters", autospec=True) def test_glows_l1b( mock_spice_function, + mock_flag_uv_source, de_dataset, hist_dataset, mock_ancillary_exclusions, @@ -429,9 +435,11 @@ 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, "update_spice_parameters", autospec=True) def test_generate_histogram_dataset( mock_spice_function, + mock_flag_uv_source, hist_dataset, mock_ancillary_exclusions, mock_pipeline_settings, From 7d0db0caa8fa2933d3122fa65acd75032d0269e6 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Wed, 25 Feb 2026 13:47:41 -0700 Subject: [PATCH 15/17] remove array for time --- imap_processing/glows/l1b/glows_l1b_data.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 67adb56c0..6ca62ebb4 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -1017,14 +1017,10 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: # 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 - ) # Transform unit cartesian vectors to ECLIPJ2000 frame. look_vecs_ecl = frame_transform( - et_bins, + data_start_time_et, look_vecs_dps, SpiceFrame.IMAP_DPS, SpiceFrame.ECLIPJ2000, From bf5eaca214e473625753194eee3ff871a7d00976 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Wed, 25 Feb 2026 13:56:14 -0700 Subject: [PATCH 16/17] add static dps --- imap_processing/glows/l1b/glows_l1b_data.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/imap_processing/glows/l1b/glows_l1b_data.py b/imap_processing/glows/l1b/glows_l1b_data.py index 6ca62ebb4..d0c679f16 100644 --- a/imap_processing/glows/l1b/glows_l1b_data.py +++ b/imap_processing/glows/l1b/glows_l1b_data.py @@ -15,7 +15,7 @@ SpiceBody, SpiceFrame, frame_transform, - instrument_pointing, + get_instrument_mounting_az_el, spherical_to_cartesian, ) from imap_processing.spice.spin import ( @@ -1003,12 +1003,8 @@ def flag_uv_source(self, exclusions: AncillaryExclusions) -> np.ndarray: 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] + az_el = get_instrument_mounting_az_el(SpiceFrame.IMAP_GLOWS) + elevation = az_el[1] spherical = np.stack( [np.ones_like(azimuth), azimuth, np.full_like(azimuth, elevation)], From 145067efc57fd7b4de9662b51b36cd28f25db11d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:58:25 +0000 Subject: [PATCH 17/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- imap_processing/quality_flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imap_processing/quality_flags.py b/imap_processing/quality_flags.py index ba28b754c..cb679d73a 100644 --- a/imap_processing/quality_flags.py +++ b/imap_processing/quality_flags.py @@ -148,7 +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. + IS_CLOSE_TO_UV_SOURCE = 2**0 # Is the bin close to a UV source. class ImapHiL1bDeFlags(FlagNameMixin):