99
1010from imap_processing .glows import FLAG_LENGTH
1111from imap_processing .glows .utils .constants import TimeTuple
12+ from imap_processing .quality_flags import GLOWSL1bFlags
1213from 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+ )
1421from 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
0 commit comments