imap-processing 0.18.0__py3-none-any.whl → 0.19.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of imap-processing might be problematic. Click here for more details.

Files changed (122) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -0
  4. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +221 -1057
  5. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +307 -283
  6. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1044 -203
  7. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  8. imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +11 -0
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +15 -1
  10. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +5 -0
  11. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +10 -4
  12. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  13. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +8 -91
  14. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +106 -16
  15. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +5 -4
  16. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  17. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  18. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +85 -2
  19. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  20. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +20 -8
  21. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +45 -35
  22. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +110 -7
  23. imap_processing/cli.py +138 -93
  24. imap_processing/codice/codice_l0.py +2 -1
  25. imap_processing/codice/codice_l1a.py +167 -69
  26. imap_processing/codice/codice_l1b.py +42 -32
  27. imap_processing/codice/codice_l2.py +215 -9
  28. imap_processing/codice/constants.py +790 -603
  29. imap_processing/codice/data/lo_stepping_values.csv +1 -1
  30. imap_processing/decom.py +1 -4
  31. imap_processing/ena_maps/ena_maps.py +71 -43
  32. imap_processing/ena_maps/utils/corrections.py +291 -0
  33. imap_processing/ena_maps/utils/map_utils.py +20 -4
  34. imap_processing/ena_maps/utils/naming.py +8 -2
  35. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  37. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  38. imap_processing/glows/ancillary/imap_glows_pipeline-settings_20250923_v002.json +54 -0
  39. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  40. imap_processing/glows/l1b/glows_l1b.py +123 -18
  41. imap_processing/glows/l1b/glows_l1b_data.py +358 -47
  42. imap_processing/glows/l2/glows_l2.py +11 -0
  43. imap_processing/hi/hi_l1a.py +124 -3
  44. imap_processing/hi/hi_l1b.py +154 -71
  45. imap_processing/hi/hi_l1c.py +4 -109
  46. imap_processing/hi/hi_l2.py +104 -60
  47. imap_processing/hi/utils.py +262 -8
  48. imap_processing/hit/l0/constants.py +3 -0
  49. imap_processing/hit/l0/decom_hit.py +3 -6
  50. imap_processing/hit/l1a/hit_l1a.py +311 -21
  51. imap_processing/hit/l1b/hit_l1b.py +54 -126
  52. imap_processing/hit/l2/hit_l2.py +6 -6
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +12 -2
  55. imap_processing/ialirt/generate_coverage.py +15 -2
  56. imap_processing/ialirt/l0/ialirt_spice.py +6 -2
  57. imap_processing/ialirt/l0/parse_mag.py +293 -42
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/process_ephemeris.py +70 -14
  61. imap_processing/ialirt/utils/create_xarray.py +1 -1
  62. imap_processing/idex/idex_l0.py +2 -2
  63. imap_processing/idex/idex_l1a.py +2 -3
  64. imap_processing/idex/idex_l1b.py +2 -3
  65. imap_processing/idex/idex_l2a.py +130 -4
  66. imap_processing/idex/idex_l2b.py +158 -143
  67. imap_processing/idex/idex_utils.py +1 -3
  68. imap_processing/lo/ancillary_data/imap_lo_hydrogen-geometric-factor_v001.csv +75 -0
  69. imap_processing/lo/ancillary_data/imap_lo_oxygen-geometric-factor_v001.csv +75 -0
  70. imap_processing/lo/l0/lo_science.py +25 -24
  71. imap_processing/lo/l1b/lo_l1b.py +93 -19
  72. imap_processing/lo/l1c/lo_l1c.py +273 -93
  73. imap_processing/lo/l2/lo_l2.py +949 -135
  74. imap_processing/lo/lo_ancillary.py +55 -0
  75. imap_processing/mag/l1a/mag_l1a.py +1 -0
  76. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  77. imap_processing/mag/l1b/mag_l1b.py +3 -2
  78. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  79. imap_processing/mag/l1c/mag_l1c.py +23 -6
  80. imap_processing/mag/l1d/mag_l1d.py +57 -14
  81. imap_processing/mag/l1d/mag_l1d_data.py +202 -32
  82. imap_processing/mag/l2/mag_l2.py +2 -0
  83. imap_processing/mag/l2/mag_l2_data.py +14 -5
  84. imap_processing/quality_flags.py +23 -1
  85. imap_processing/spice/geometry.py +89 -39
  86. imap_processing/spice/pointing_frame.py +4 -8
  87. imap_processing/spice/repoint.py +78 -2
  88. imap_processing/spice/spin.py +28 -8
  89. imap_processing/spice/time.py +12 -22
  90. imap_processing/swapi/l1/swapi_l1.py +10 -4
  91. imap_processing/swapi/l2/swapi_l2.py +15 -17
  92. imap_processing/swe/l1b/swe_l1b.py +1 -2
  93. imap_processing/ultra/constants.py +30 -24
  94. imap_processing/ultra/l0/ultra_utils.py +9 -11
  95. imap_processing/ultra/l1a/ultra_l1a.py +1 -2
  96. imap_processing/ultra/l1b/badtimes.py +35 -11
  97. imap_processing/ultra/l1b/de.py +95 -31
  98. imap_processing/ultra/l1b/extendedspin.py +31 -16
  99. imap_processing/ultra/l1b/goodtimes.py +112 -0
  100. imap_processing/ultra/l1b/lookup_utils.py +281 -28
  101. imap_processing/ultra/l1b/quality_flag_filters.py +10 -1
  102. imap_processing/ultra/l1b/ultra_l1b.py +7 -7
  103. imap_processing/ultra/l1b/ultra_l1b_culling.py +169 -7
  104. imap_processing/ultra/l1b/ultra_l1b_extended.py +311 -69
  105. imap_processing/ultra/l1c/helio_pset.py +139 -37
  106. imap_processing/ultra/l1c/l1c_lookup_utils.py +289 -0
  107. imap_processing/ultra/l1c/spacecraft_pset.py +140 -29
  108. imap_processing/ultra/l1c/ultra_l1c.py +33 -24
  109. imap_processing/ultra/l1c/ultra_l1c_culling.py +92 -0
  110. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +400 -292
  111. imap_processing/ultra/l2/ultra_l2.py +54 -11
  112. imap_processing/ultra/utils/ultra_l1_utils.py +37 -7
  113. imap_processing/utils.py +3 -4
  114. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/METADATA +2 -2
  115. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/RECORD +118 -109
  116. imap_processing/idex/idex_l2c.py +0 -84
  117. imap_processing/spice/kernels.py +0 -187
  118. imap_processing/ultra/l1b/cullingmask.py +0 -87
  119. imap_processing/ultra/l1c/histogram.py +0 -36
  120. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/LICENSE +0 -0
  121. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/WHEEL +0 -0
  122. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  # TODO: Come back and add in FSW logic.
4
4
  import logging
5
+ from collections import namedtuple
5
6
  from enum import Enum
6
7
 
7
8
  import numpy as np
@@ -11,11 +12,14 @@ from numpy import ndarray
11
12
  from numpy.typing import NDArray
12
13
  from scipy.interpolate import LinearNDInterpolator, RegularGridInterpolator
13
14
 
15
+ from imap_processing.quality_flags import ImapDEOutliersUltraFlags
14
16
  from imap_processing.spice.spin import get_spin_data
17
+ from imap_processing.spice.time import sct_to_et
15
18
  from imap_processing.ultra.constants import UltraConstants
16
19
  from imap_processing.ultra.l1b.lookup_utils import (
17
20
  get_angular_profiles,
18
21
  get_back_position,
22
+ get_ebins,
19
23
  get_energy_efficiencies,
20
24
  get_energy_norm,
21
25
  get_image_params,
@@ -26,6 +30,10 @@ from imap_processing.ultra.l1b.lookup_utils import (
26
30
 
27
31
  logger = logging.getLogger(__name__)
28
32
 
33
+ FILLVAL_UINT8 = 255
34
+ FILLVAL_FLOAT32 = -1.0e31
35
+ FILLVAL_FLOAT64 = -1.0e31
36
+
29
37
 
30
38
  class StartType(Enum):
31
39
  """Start Type: 1=Left, 2=Right."""
@@ -50,6 +58,9 @@ class CoinType(Enum):
50
58
  Bottom = 2
51
59
 
52
60
 
61
+ PHTOFResult = namedtuple("PHTOFResult", ["tof", "t2", "xb", "yb", "tofx", "tofy"])
62
+
63
+
53
64
  def get_front_x_position(
54
65
  start_type: ndarray, start_position_tdc: ndarray, sensor: str, ancillary_files: dict
55
66
  ) -> ndarray:
@@ -161,7 +172,7 @@ def get_front_y_position(
161
172
 
162
173
  def get_ph_tof_and_back_positions(
163
174
  de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str, ancillary_files: dict
164
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
175
+ ) -> PHTOFResult:
165
176
  """
166
177
  Calculate back xb, yb position and tof.
167
178
 
@@ -196,6 +207,10 @@ def get_ph_tof_and_back_positions(
196
207
  Back positions in x direction (hundredths of a millimeter).
197
208
  yb : np.array
198
209
  Back positions in y direction (hundredths of a millimeter).
210
+ tofx : np.array
211
+ X front position tof offset (tenths of a nanosecond).
212
+ tofy : np.array
213
+ Y front position tof offset (tenths of a nanosecond).
199
214
  """
200
215
  indices = np.nonzero(
201
216
  np.isin(de_dataset["stop_type"], [StopType.Top.value, StopType.Bottom.value])
@@ -278,7 +293,7 @@ def get_ph_tof_and_back_positions(
278
293
  stop_type_bottom
279
294
  ] / 10 * get_image_params("XFTTOF", sensor, ancillary_files)
280
295
 
281
- return tof, t2, xb, yb
296
+ return PHTOFResult(tof=tof, t2=t2, xb=xb, yb=yb, tofx=tofx, tofy=tofy)
282
297
 
283
298
 
284
299
  def get_path_length(
@@ -530,16 +545,16 @@ def get_de_velocity(
530
545
  v_y = delta_v[:, 1] / tof * 1e3
531
546
  v_z = delta_v[:, 2] / tof * 1e3
532
547
 
533
- v_x[tof < 0] = np.nan # used as fillvals
534
- v_y[tof < 0] = np.nan
535
- v_z[tof < 0] = np.nan
548
+ v_x[tof < 0] = FILLVAL_FLOAT32 # used as fillvals
549
+ v_y[tof < 0] = FILLVAL_FLOAT32
550
+ v_z[tof < 0] = FILLVAL_FLOAT32
536
551
 
537
552
  velocities = np.vstack((v_x, v_y, v_z)).T
538
553
 
539
554
  v_hat = velocities / np.linalg.norm(velocities, axis=1)[:, None]
540
555
  r_hat = -v_hat
541
556
 
542
- return velocities, v_hat, r_hat
557
+ return velocities, -v_hat, -r_hat
543
558
 
544
559
 
545
560
  def get_ssd_tof(
@@ -621,13 +636,17 @@ def get_de_energy_kev(v: np.ndarray, species: np.ndarray) -> NDArray:
621
636
  # Compute the sum of squares.
622
637
  v2 = np.sum(vv**2, axis=1)
623
638
 
624
- index_hydrogen = np.where(species == 1)
625
- energy = np.full_like(v2, np.nan)
639
+ # Only compute where species == 1 and v is valid
640
+ index_hydrogen = species == 1
641
+ valid_velocity = np.isfinite(v2)
642
+ valid_mask = index_hydrogen & valid_velocity
643
+
644
+ energy = np.full_like(v2, FILLVAL_FLOAT32)
626
645
 
627
646
  # TODO: we will calculate the energies of the different species here.
628
647
  # 1/2 mv^2 in Joules, convert to keV
629
- energy[index_hydrogen] = (
630
- 0.5 * UltraConstants.MASS_H * v2[index_hydrogen] * UltraConstants.J_KEV
648
+ energy[valid_mask] = (
649
+ 0.5 * UltraConstants.MASS_H * v2[valid_mask] * UltraConstants.J_KEV
631
650
  )
632
651
 
633
652
  return energy
@@ -820,25 +839,16 @@ def get_ctof(
820
839
  return ctof, magnitude_v
821
840
 
822
841
 
823
- def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> NDArray:
842
+ def determine_species(e_bin: np.ndarray, type: str) -> NDArray:
824
843
  """
825
844
  Determine the species for pulse-height events.
826
845
 
827
- Species is determined from the particle velocity.
828
- For velocity, the particle TOF is normalized with respect
829
- to a fixed distance dmin between the front and back detectors.
830
- The normalized TOF is termed the corrected TOF (ctof).
831
- Particle species are determined from ctof using thresholds.
832
-
833
- Further description is available on pages 42-44 of
834
- IMAP-Ultra Flight Software Specification document.
846
+ Species is determined using the computed e_bin.
835
847
 
836
848
  Parameters
837
849
  ----------
838
- tof : np.ndarray
839
- Time of flight of the SSD event (tenths of a nanosecond).
840
- path_length : np.ndarray
841
- Path length (r) (hundredths of a millimeter).
850
+ e_bin : np.ndarray
851
+ Computed e_bin.
842
852
  type : str
843
853
  Type of data (PH or SSD).
844
854
 
@@ -847,11 +857,17 @@ def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> ND
847
857
  species_bin : np.array
848
858
  Species bin.
849
859
  """
850
- # Event TOF normalization to Z axis
851
- ctof, _ = get_ctof(tof, path_length, type)
852
- # Assign Species 1 ("H") to bins
853
- # TODO: this is a placeholder for future species assignments.
854
- species_bin = np.full(len(ctof), 1, dtype=np.uint8)
860
+ if type == "PH":
861
+ species_groups = UltraConstants.TOFXPH_SPECIES_GROUPS
862
+ if type == "SSD":
863
+ species_groups = UltraConstants.TOFXE_SPECIES_GROUPS
864
+
865
+ non_proton_bins = species_groups["non_proton"]
866
+ proton_bins = species_groups["proton"]
867
+
868
+ species_bin = np.full(e_bin.shape, fill_value=2, dtype=int)
869
+ species_bin[np.isin(e_bin, non_proton_bins)] = 0
870
+ species_bin[np.isin(e_bin, proton_bins)] = 1
855
871
 
856
872
  return species_bin
857
873
 
@@ -933,7 +949,7 @@ def get_spin_number(de_met: NDArray, de_spin: NDArray) -> NDArray:
933
949
  possible_spins = spin_numbers & 0xFF
934
950
 
935
951
  # Assign each group based on time.
936
- for start, end in zip(spin_start_indices, spin_end_indices):
952
+ for start, end in zip(spin_start_indices, spin_end_indices, strict=False):
937
953
  # Now that we have the possible spins from the Universal Spin Table,
938
954
  # we match the times of those spins to the nearest times in the DE data.
939
955
  possible_times = spin_start_mets[possible_spins == de_spin_sorted[start]]
@@ -967,9 +983,9 @@ def get_eventtimes(
967
983
  Returns
968
984
  -------
969
985
  event_times : np.ndarray
970
- Event times.
986
+ Event times in et.
971
987
  spin_starts : np.ndarray
972
- Spin start times.
988
+ Spin start times in et.
973
989
  spin_period_sec : np.ndarray
974
990
  Spin period in seconds.
975
991
 
@@ -990,7 +1006,7 @@ def get_eventtimes(
990
1006
 
991
1007
  event_times = spin_starts + spin_period_sec * (phase_angle / 720)
992
1008
 
993
- return event_times, spin_starts, spin_period_sec
1009
+ return sct_to_et(event_times), sct_to_et(spin_starts), spin_period_sec
994
1010
 
995
1011
 
996
1012
  def interpolate_fwhm(
@@ -1030,8 +1046,11 @@ def interpolate_fwhm(
1030
1046
  )
1031
1047
 
1032
1048
  # Note: will return nan for those out-of-bounds inputs.
1033
- phi_interp = interp_phi((energy, phi_inst))
1034
- theta_interp = interp_theta((energy, theta_inst))
1049
+ phi_vals = interp_phi((energy, phi_inst))
1050
+ theta_vals = interp_theta((energy, theta_inst))
1051
+
1052
+ phi_interp = np.where(np.isnan(phi_vals), FILLVAL_FLOAT32, phi_vals)
1053
+ theta_interp = np.where(np.isnan(theta_vals), FILLVAL_FLOAT32, theta_vals)
1035
1054
 
1036
1055
  return phi_interp, theta_interp
1037
1056
 
@@ -1069,8 +1088,8 @@ def get_fwhm(
1069
1088
  theta_interp : NDArray
1070
1089
  Interpolated theta FWHM values.
1071
1090
  """
1072
- phi_interp = np.full_like(phi_inst, np.nan, dtype=np.float64)
1073
- theta_interp = np.full_like(theta_inst, np.nan, dtype=np.float64)
1091
+ phi_interp = np.full_like(phi_inst, FILLVAL_FLOAT64, dtype=np.float64)
1092
+ theta_interp = np.full_like(theta_inst, FILLVAL_FLOAT64, dtype=np.float64)
1074
1093
  lt_table = get_angular_profiles("left", sensor, ancillary_files)
1075
1094
  rt_table = get_angular_profiles("right", sensor, ancillary_files)
1076
1095
 
@@ -1089,20 +1108,12 @@ def get_fwhm(
1089
1108
  return phi_interp, theta_interp
1090
1109
 
1091
1110
 
1092
- def get_efficiency(
1093
- energy: NDArray, phi_inst: NDArray, theta_inst: NDArray, ancillary_files: dict
1094
- ) -> NDArray:
1111
+ def get_efficiency_interpolator(ancillary_files: dict) -> RegularGridInterpolator:
1095
1112
  """
1096
- Interpolate efficiency values for each event.
1113
+ Return a callable function that interpolates efficiency values for each event.
1097
1114
 
1098
1115
  Parameters
1099
1116
  ----------
1100
- energy : NDArray
1101
- Energy values for each event.
1102
- phi_inst : NDArray
1103
- Instrument-frame azimuth angle for each event.
1104
- theta_inst : NDArray
1105
- Instrument-frame elevation angle for each event.
1106
1117
  ancillary_files : dict
1107
1118
  Ancillary files.
1108
1119
 
@@ -1127,14 +1138,54 @@ def get_efficiency(
1127
1138
  (theta_vals, phi_vals, energy_vals),
1128
1139
  efficiency_grid,
1129
1140
  bounds_error=False,
1130
- fill_value=np.nan,
1141
+ fill_value=FILLVAL_FLOAT32,
1131
1142
  )
1132
1143
 
1144
+ return interpolator
1145
+
1146
+
1147
+ def get_efficiency(
1148
+ energy: NDArray,
1149
+ phi_inst: NDArray,
1150
+ theta_inst: NDArray,
1151
+ ancillary_files: dict,
1152
+ interpolator: RegularGridInterpolator = None,
1153
+ ) -> np.ndarray:
1154
+ """
1155
+ Return interpolated efficiency values for each event.
1156
+
1157
+ Parameters
1158
+ ----------
1159
+ energy : NDArray
1160
+ Energy values for each event.
1161
+ phi_inst : NDArray
1162
+ Instrument-frame azimuth angle for each event.
1163
+ theta_inst : NDArray
1164
+ Instrument-frame elevation angle for each event.
1165
+ ancillary_files : dict
1166
+ Ancillary files.
1167
+ interpolator : RegularGridInterpolator, optional
1168
+ Precomputed interpolator to use for efficiency lookup.
1169
+ If None, a new interpolator will be created from the ancillary files.
1170
+
1171
+ Returns
1172
+ -------
1173
+ efficiency : NDArray
1174
+ Interpolated efficiency values.
1175
+ """
1176
+ if not interpolator:
1177
+ interpolator = get_efficiency_interpolator(ancillary_files)
1178
+
1133
1179
  return interpolator((theta_inst, phi_inst, energy))
1134
1180
 
1135
1181
 
1136
1182
  def determine_ebin_pulse_height(
1137
- energy: np.ndarray, tof: np.ndarray, path_length: np.ndarray
1183
+ energy: NDArray,
1184
+ tof: NDArray,
1185
+ path_length: NDArray,
1186
+ backtofvalid: NDArray,
1187
+ coinphvalid: NDArray,
1188
+ ancillary_files: dict,
1138
1189
  ) -> NDArray:
1139
1190
  """
1140
1191
  Determine the species for pulse-height events.
@@ -1152,12 +1203,18 @@ def determine_ebin_pulse_height(
1152
1203
 
1153
1204
  Parameters
1154
1205
  ----------
1155
- energy : np.ndarray
1206
+ energy : NDArray
1156
1207
  Energy from the PH event (keV).
1157
- tof : np.ndarray
1208
+ tof : NDArray
1158
1209
  Time of flight of the PH event (tenths of a nanosecond).
1159
- path_length : np.ndarray
1210
+ path_length : NDArray
1160
1211
  Path length (r) (hundredths of a millimeter).
1212
+ backtofvalid : NDArray
1213
+ Boolean array indicating if the back TOF is valid.
1214
+ coinphvalid : NDArray
1215
+ Boolean array indicating if the Coincidence PH is valid.
1216
+ ancillary_files : dict
1217
+ Ancillary files containing the lookup tables.
1161
1218
 
1162
1219
  Returns
1163
1220
  -------
@@ -1166,15 +1223,22 @@ def determine_ebin_pulse_height(
1166
1223
  """
1167
1224
  # PH event TOF normalization to Z axis
1168
1225
  ctof, _ = get_ctof(tof, path_length, type="PH")
1169
- # TODO: need lookup tables
1170
- # placeholder
1171
- ebin = np.full(len(ctof), 255, dtype=np.uint8)
1172
1226
 
1173
- return ebin
1227
+ ebins = np.full(path_length.shape, FILLVAL_UINT8, dtype=np.uint8)
1228
+ valid = backtofvalid & coinphvalid
1229
+ ebins[valid] = get_ebins(
1230
+ "l1b-tofxph", energy[valid], ctof[valid], ebins[valid], ancillary_files
1231
+ )
1232
+
1233
+ return ebins
1174
1234
 
1175
1235
 
1176
1236
  def determine_ebin_ssd(
1177
- energy: np.ndarray, tof: np.ndarray, path_length: np.ndarray
1237
+ energy: NDArray,
1238
+ tof: NDArray,
1239
+ path_length: NDArray,
1240
+ sensor: str,
1241
+ ancillary_files: dict,
1178
1242
  ) -> NDArray:
1179
1243
  """
1180
1244
  Determine the species for SSD events.
@@ -1194,29 +1258,207 @@ def determine_ebin_ssd(
1194
1258
 
1195
1259
  Parameters
1196
1260
  ----------
1197
- energy : np.ndarray
1261
+ energy : NDArray
1198
1262
  Energy from the SSD event (keV).
1199
- tof : np.ndarray
1263
+ tof : NDArray
1200
1264
  Time of flight of the SSD event (tenths of a nanosecond).
1201
- path_length : np.ndarray
1265
+ path_length : NDArray
1202
1266
  Path length (r) (hundredths of a millimeter).
1267
+ sensor : str
1268
+ Sensor name: "ultra45" or "ultra90".
1269
+ ancillary_files : dict
1270
+ Ancillary files containing the lookup tables.
1203
1271
 
1204
1272
  Returns
1205
1273
  -------
1206
- bin : np.ndarray
1274
+ bin : NDArray
1207
1275
  Species bin.
1208
1276
  """
1209
1277
  # SSD event TOF normalization to Z axis
1210
1278
  ctof, _ = get_ctof(tof, path_length, type="SSD")
1211
1279
 
1212
- ebin = np.full(len(ctof), 255, dtype=np.uint8) # placeholder
1280
+ ebins = np.full(path_length.shape, FILLVAL_UINT8, dtype=np.uint8)
1281
+ steep_path_length = get_image_params("PathSteepThresh", sensor, ancillary_files)
1282
+ medium_path_length = get_image_params("PathMediumThresh", sensor, ancillary_files)
1283
+
1284
+ steep_mask = path_length < steep_path_length
1285
+ medium_mask = (path_length >= steep_path_length) & (
1286
+ path_length < medium_path_length
1287
+ )
1288
+ flat_mask = path_length >= medium_path_length
1289
+
1290
+ ebins[steep_mask] = get_ebins(
1291
+ f"l1b-{sensor[5::]}sensor-tofxesteep",
1292
+ energy[steep_mask],
1293
+ ctof[steep_mask],
1294
+ ebins[steep_mask],
1295
+ ancillary_files,
1296
+ )
1297
+ ebins[medium_mask] = get_ebins(
1298
+ f"l1b-{sensor[5::]}sensor-tofxemedium",
1299
+ energy[medium_mask],
1300
+ ctof[medium_mask],
1301
+ ebins[medium_mask],
1302
+ ancillary_files,
1303
+ )
1304
+ ebins[flat_mask] = get_ebins(
1305
+ f"l1b-{sensor[5::]}sensor-tofxeflat",
1306
+ energy[flat_mask],
1307
+ ctof[flat_mask],
1308
+ ebins[flat_mask],
1309
+ ancillary_files,
1310
+ )
1311
+
1312
+ return ebins
1313
+
1314
+
1315
+ def is_back_tof_valid(
1316
+ de_dataset: xarray.Dataset,
1317
+ xf: NDArray,
1318
+ sensor: str,
1319
+ ancillary_files: dict,
1320
+ ) -> NDArray:
1321
+ """
1322
+ Determine whether back TOF is valid based on stop type.
1323
+
1324
+ Parameters
1325
+ ----------
1326
+ de_dataset : xarray.Dataset
1327
+ Data in xarray format.
1328
+ xf : NDArray
1329
+ X front position in (hundredths of a millimeter).
1330
+ Has same length as de_dataset.
1331
+ sensor : str
1332
+ Sensor name: "ultra45" or "ultra90".
1333
+ ancillary_files : dict
1334
+ Ancillary files for lookup.
1335
+
1336
+ Returns
1337
+ -------
1338
+ valid_mask : NDArray
1339
+ Boolean array indicating whether back TOF is valid.
1340
+
1341
+ Notes
1342
+ -----
1343
+ From page 33 of the IMAP-Ultra Flight Software Specification document.
1344
+ """
1345
+ _, _, _, _, tofx, tofy = get_ph_tof_and_back_positions(
1346
+ de_dataset, xf, "ultra45", ancillary_files
1347
+ )
1348
+ diff = tofy - tofx
1349
+
1350
+ indices = np.nonzero(
1351
+ np.isin(de_dataset["stop_type"], [StopType.Top.value, StopType.Bottom.value])
1352
+ )[0]
1353
+ de_ph = de_dataset.isel(epoch=indices)
1354
+
1355
+ top_mask = de_ph["stop_type"] == StopType.Top.value
1356
+ bottom_mask = de_ph["stop_type"] == StopType.Bottom.value
1357
+
1358
+ valid = np.zeros_like(diff, dtype=bool)
1359
+
1360
+ diff_tp_min = get_image_params("TOFDiffTpMin", sensor, ancillary_files)
1361
+ diff_tp_max = get_image_params("TOFDiffTpMax", sensor, ancillary_files)
1362
+ diff_bt_min = get_image_params("TOFDiffBtMin", sensor, ancillary_files)
1363
+ diff_bt_max = get_image_params("TOFDiffBtMax", sensor, ancillary_files)
1364
+
1365
+ valid[top_mask] = (diff[top_mask] >= diff_tp_min) & (diff[top_mask] <= diff_tp_max)
1366
+ valid[bottom_mask] = (diff[bottom_mask] >= diff_bt_min) & (
1367
+ diff[bottom_mask] <= diff_bt_max
1368
+ )
1369
+
1370
+ return valid
1371
+
1372
+
1373
+ def is_coin_ph_valid(
1374
+ etof: NDArray,
1375
+ xc: NDArray,
1376
+ xb: NDArray,
1377
+ stop_north_tdc: NDArray,
1378
+ stop_south_tdc: NDArray,
1379
+ stop_east_tdc: NDArray,
1380
+ stop_west_tdc: NDArray,
1381
+ sensor: str,
1382
+ ancillary_files: dict,
1383
+ quality_flags: NDArray,
1384
+ ) -> NDArray:
1385
+ """
1386
+ Determine event validity.
1387
+
1388
+ Parameters
1389
+ ----------
1390
+ etof : NDArray
1391
+ Time for the electrons to travel back to the coincidence
1392
+ anode (tenths of a nanosecond).
1393
+ xc : NDArray
1394
+ X coincidence position (hundredths of a millimeter).
1395
+ xb : NDArray
1396
+ Back positions in x direction (hundredths of a millimeter).
1397
+ stop_north_tdc : NDArray
1398
+ Stop North Time to Digital Converter.
1399
+ stop_south_tdc : NDArray
1400
+ Stop South Time to Digital Converter.
1401
+ stop_east_tdc : NDArray
1402
+ Stop East Time to Digital Converter.
1403
+ stop_west_tdc : NDArray
1404
+ Stop West Time to Digital Converter.
1405
+ sensor : str
1406
+ Sensor name: "ultra45" or "ultra90".
1407
+ ancillary_files : dict
1408
+ Ancillary files for lookup.
1409
+ quality_flags : NDArray
1410
+ Quality flag to set when there is an outlier.
1411
+
1412
+ Returns
1413
+ -------
1414
+ combined_mask : NDArray
1415
+ Boolean array indicating whether back TOF is valid.
1416
+
1417
+ Notes
1418
+ -----
1419
+ From page 36 of the IMAP-Ultra Flight Software Specification document.
1420
+ """
1421
+ # Make certain etof is within range for tenths of a nanosecond.
1422
+ etof_valid = (etof >= UltraConstants.ETOFMIN_EVENTFILTER) & (
1423
+ etof <= UltraConstants.ETOFMAX_EVENTFILTER
1424
+ )
1425
+
1426
+ # Hundredths of a mm.
1427
+ diff_x = xc - xb
1428
+
1429
+ t1 = (
1430
+ (etof - UltraConstants.ETOFOFF1_EVENTFILTER)
1431
+ * UltraConstants.ETOFSLOPE1_EVENTFILTER
1432
+ / 1024
1433
+ )
1434
+ t2 = (
1435
+ (etof - UltraConstants.ETOFOFF2_EVENTFILTER)
1436
+ * UltraConstants.ETOFSLOPE2_EVENTFILTER
1437
+ / 1024
1438
+ )
1439
+
1440
+ condition_1 = (diff_x >= t1) & (diff_x <= t2)
1441
+ condition_2 = (diff_x >= -t2) & (diff_x <= -t1)
1442
+
1443
+ spatial_valid = condition_1 | condition_2
1444
+
1445
+ sp_n_norm = get_norm(stop_north_tdc, "SpN", sensor, ancillary_files)
1446
+ sp_s_norm = get_norm(stop_south_tdc, "SpS", sensor, ancillary_files)
1447
+ sp_e_norm = get_norm(stop_east_tdc, "SpE", sensor, ancillary_files)
1448
+ sp_w_norm = get_norm(stop_west_tdc, "SpW", sensor, ancillary_files)
1449
+
1450
+ tofx = sp_n_norm + sp_s_norm
1451
+ tofy = sp_e_norm + sp_w_norm
1452
+
1453
+ # Units in tenths of a nanosecond
1454
+ delta_tof = tofy - tofx
1455
+
1456
+ delta_tof_mask = (delta_tof >= UltraConstants.TOFDIFFTPMIN_EVENTFILTER) & (
1457
+ delta_tof <= UltraConstants.TOFDIFFTPMAX_EVENTFILTER
1458
+ )
1459
+
1460
+ combined_mask = etof_valid & spatial_valid & delta_tof_mask
1213
1461
 
1214
- # TODO: get these lookup tables
1215
- # if r < get_image_params("PathSteepThresh"):
1216
- # # bin = ExTOFSpeciesSteep[energy, ctof]
1217
- # elif r < get_image_params("PathMediumThresh"):
1218
- # # bin = ExTOFSpeciesMedium[energy, ctof]
1219
- # else:
1220
- # # bin = ExTOFSpeciesFlat[energy, ctof]
1462
+ quality_flags[~combined_mask] |= ImapDEOutliersUltraFlags.COINPH.value
1221
1463
 
1222
- return ebin
1464
+ return combined_mask