imap-processing 0.17.0__py3-none-any.whl → 0.19.0__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 (141) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/ccsds/excel_to_xtce.py +12 -0
  4. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -6
  5. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +312 -274
  6. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +39 -28
  7. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1048 -183
  8. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +12 -0
  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_hit_l1a_variable_attrs.yaml +163 -100
  13. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +4 -4
  14. imap_processing/cdf/config/imap_ialirt_l1_variable_attrs.yaml +97 -54
  15. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  16. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +44 -44
  17. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +77 -61
  18. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +30 -0
  19. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  20. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  21. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +99 -2
  22. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  23. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +60 -0
  24. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +99 -11
  25. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +50 -7
  26. imap_processing/cli.py +121 -44
  27. imap_processing/codice/codice_l1a.py +165 -77
  28. imap_processing/codice/codice_l1b.py +1 -1
  29. imap_processing/codice/codice_l2.py +118 -19
  30. imap_processing/codice/constants.py +1217 -1089
  31. imap_processing/decom.py +1 -4
  32. imap_processing/ena_maps/ena_maps.py +32 -25
  33. imap_processing/ena_maps/utils/naming.py +8 -2
  34. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  35. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  37. imap_processing/glows/ancillary/imap_glows_pipeline_settings_20250923_v002.json +54 -0
  38. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  39. imap_processing/glows/l1b/glows_l1b.py +99 -9
  40. imap_processing/glows/l1b/glows_l1b_data.py +350 -38
  41. imap_processing/glows/l2/glows_l2.py +11 -0
  42. imap_processing/hi/hi_l1a.py +124 -3
  43. imap_processing/hi/hi_l1b.py +154 -71
  44. imap_processing/hi/hi_l2.py +84 -51
  45. imap_processing/hi/utils.py +153 -8
  46. imap_processing/hit/l0/constants.py +3 -0
  47. imap_processing/hit/l0/decom_hit.py +5 -8
  48. imap_processing/hit/l1a/hit_l1a.py +375 -45
  49. imap_processing/hit/l1b/constants.py +5 -0
  50. imap_processing/hit/l1b/hit_l1b.py +61 -131
  51. imap_processing/hit/l2/constants.py +1 -1
  52. imap_processing/hit/l2/hit_l2.py +10 -11
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +32 -1
  55. imap_processing/ialirt/generate_coverage.py +201 -0
  56. imap_processing/ialirt/l0/ialirt_spice.py +5 -2
  57. imap_processing/ialirt/l0/parse_mag.py +337 -29
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/l0/process_swe.py +23 -7
  61. imap_processing/ialirt/process_ephemeris.py +70 -14
  62. imap_processing/ialirt/utils/constants.py +22 -16
  63. imap_processing/ialirt/utils/create_xarray.py +42 -19
  64. imap_processing/idex/idex_constants.py +1 -5
  65. imap_processing/idex/idex_l0.py +2 -2
  66. imap_processing/idex/idex_l1a.py +2 -3
  67. imap_processing/idex/idex_l1b.py +2 -3
  68. imap_processing/idex/idex_l2a.py +130 -4
  69. imap_processing/idex/idex_l2b.py +313 -119
  70. imap_processing/idex/idex_utils.py +1 -3
  71. imap_processing/lo/l0/lo_apid.py +1 -0
  72. imap_processing/lo/l0/lo_science.py +25 -24
  73. imap_processing/lo/l1a/lo_l1a.py +44 -0
  74. imap_processing/lo/l1b/lo_l1b.py +3 -3
  75. imap_processing/lo/l1c/lo_l1c.py +116 -50
  76. imap_processing/lo/l2/lo_l2.py +29 -29
  77. imap_processing/lo/lo_ancillary.py +55 -0
  78. imap_processing/lo/packet_definitions/lo_xtce.xml +5359 -106
  79. imap_processing/mag/constants.py +1 -0
  80. imap_processing/mag/l1a/mag_l1a.py +1 -0
  81. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  82. imap_processing/mag/l1b/mag_l1b.py +3 -2
  83. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  84. imap_processing/mag/l1c/mag_l1c.py +23 -6
  85. imap_processing/mag/l1d/__init__.py +0 -0
  86. imap_processing/mag/l1d/mag_l1d.py +176 -0
  87. imap_processing/mag/l1d/mag_l1d_data.py +725 -0
  88. imap_processing/mag/l2/__init__.py +0 -0
  89. imap_processing/mag/l2/mag_l2.py +25 -20
  90. imap_processing/mag/l2/mag_l2_data.py +199 -130
  91. imap_processing/quality_flags.py +28 -2
  92. imap_processing/spice/geometry.py +101 -36
  93. imap_processing/spice/pointing_frame.py +1 -7
  94. imap_processing/spice/repoint.py +29 -2
  95. imap_processing/spice/spin.py +32 -8
  96. imap_processing/spice/time.py +60 -19
  97. imap_processing/swapi/l1/swapi_l1.py +10 -4
  98. imap_processing/swapi/l2/swapi_l2.py +66 -24
  99. imap_processing/swapi/swapi_utils.py +1 -1
  100. imap_processing/swe/l1b/swe_l1b.py +3 -6
  101. imap_processing/ultra/constants.py +28 -3
  102. imap_processing/ultra/l0/decom_tools.py +15 -8
  103. imap_processing/ultra/l0/decom_ultra.py +35 -11
  104. imap_processing/ultra/l0/ultra_utils.py +102 -12
  105. imap_processing/ultra/l1a/ultra_l1a.py +26 -6
  106. imap_processing/ultra/l1b/cullingmask.py +6 -3
  107. imap_processing/ultra/l1b/de.py +122 -26
  108. imap_processing/ultra/l1b/extendedspin.py +29 -2
  109. imap_processing/ultra/l1b/lookup_utils.py +424 -50
  110. imap_processing/ultra/l1b/quality_flag_filters.py +23 -0
  111. imap_processing/ultra/l1b/ultra_l1b_culling.py +356 -5
  112. imap_processing/ultra/l1b/ultra_l1b_extended.py +534 -90
  113. imap_processing/ultra/l1c/helio_pset.py +127 -7
  114. imap_processing/ultra/l1c/l1c_lookup_utils.py +256 -0
  115. imap_processing/ultra/l1c/spacecraft_pset.py +90 -15
  116. imap_processing/ultra/l1c/ultra_l1c.py +6 -0
  117. imap_processing/ultra/l1c/ultra_l1c_culling.py +85 -0
  118. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +446 -341
  119. imap_processing/ultra/l2/ultra_l2.py +0 -1
  120. imap_processing/ultra/utils/ultra_l1_utils.py +40 -3
  121. imap_processing/utils.py +3 -4
  122. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/METADATA +3 -3
  123. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/RECORD +126 -126
  124. imap_processing/idex/idex_l2c.py +0 -250
  125. imap_processing/spice/kernels.py +0 -187
  126. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +0 -526
  127. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +0 -526
  128. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +0 -526
  129. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +0 -524
  130. imap_processing/ultra/lookup_tables/EgyNorm.mem.csv +0 -32769
  131. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  132. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  133. imap_processing/ultra/lookup_tables/dps_grid45_compressed.cdf +0 -0
  134. imap_processing/ultra/lookup_tables/ultra45_back-pos-luts.csv +0 -4097
  135. imap_processing/ultra/lookup_tables/ultra45_tdc_norm.csv +0 -2050
  136. imap_processing/ultra/lookup_tables/ultra90_back-pos-luts.csv +0 -4097
  137. imap_processing/ultra/lookup_tables/ultra90_tdc_norm.csv +0 -2050
  138. imap_processing/ultra/lookup_tables/yadjust.csv +0 -257
  139. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/LICENSE +0 -0
  140. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/WHEEL +0 -0
  141. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.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
@@ -16,15 +17,21 @@ from imap_processing.ultra.constants import UltraConstants
16
17
  from imap_processing.ultra.l1b.lookup_utils import (
17
18
  get_angular_profiles,
18
19
  get_back_position,
20
+ get_ebins,
19
21
  get_energy_efficiencies,
20
22
  get_energy_norm,
21
23
  get_image_params,
22
24
  get_norm,
25
+ get_ph_corrected,
23
26
  get_y_adjust,
24
27
  )
25
28
 
26
29
  logger = logging.getLogger(__name__)
27
30
 
31
+ FILLVAL_UINT8 = 255
32
+ FILLVAL_FLOAT32 = -1.0e31
33
+ FILLVAL_FLOAT64 = -1.0e31
34
+
28
35
 
29
36
  class StartType(Enum):
30
37
  """Start Type: 1=Left, 2=Right."""
@@ -49,8 +56,11 @@ class CoinType(Enum):
49
56
  Bottom = 2
50
57
 
51
58
 
59
+ PHTOFResult = namedtuple("PHTOFResult", ["tof", "t2", "xb", "yb", "tofx", "tofy"])
60
+
61
+
52
62
  def get_front_x_position(
53
- start_type: ndarray, start_position_tdc: ndarray, sensor: str
63
+ start_type: ndarray, start_position_tdc: ndarray, sensor: str, ancillary_files: dict
54
64
  ) -> ndarray:
55
65
  """
56
66
  Calculate the front xf position.
@@ -68,6 +78,8 @@ def get_front_x_position(
68
78
  Start Position Time to Digital Converter (TDC).
69
79
  sensor : str
70
80
  Sensor name.
81
+ ancillary_files : dict[Path]
82
+ Ancillary files containing the lookup tables.
71
83
 
72
84
  Returns
73
85
  -------
@@ -77,9 +89,9 @@ def get_front_x_position(
77
89
  # Left and right start types.
78
90
  indices = np.nonzero((start_type == 1) | (start_type == 2))
79
91
 
80
- xftsc = get_image_params("XFTSC", sensor)
81
- xft_lt_off = get_image_params("XFTLTOFF", sensor)
82
- xft_rt_off = get_image_params("XFTRTOFF", sensor)
92
+ xftsc = get_image_params("XFTSC", sensor, ancillary_files)
93
+ xft_lt_off = get_image_params("XFTLTOFF", sensor, ancillary_files)
94
+ xft_rt_off = get_image_params("XFTRTOFF", sensor, ancillary_files)
83
95
  xft_off = np.where(start_type[indices] == 1, xft_lt_off, xft_rt_off)
84
96
 
85
97
  # Calculate xf and convert to hundredths of a millimeter
@@ -88,7 +100,9 @@ def get_front_x_position(
88
100
  return xf
89
101
 
90
102
 
91
- def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, ndarray]:
103
+ def get_front_y_position(
104
+ start_type: ndarray, yb: ndarray, ancillary_files: dict
105
+ ) -> tuple[ndarray, ndarray]:
92
106
  """
93
107
  Compute the adjustments for the front y position and distance front to back.
94
108
 
@@ -102,6 +116,8 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
102
116
  Start Type: 1=Left, 2=Right.
103
117
  yb : np.array
104
118
  Y back position in hundredths of a millimeter.
119
+ ancillary_files : dict[Path]
120
+ Ancillary files containing the lookup tables.
105
121
 
106
122
  Returns
107
123
  -------
@@ -125,7 +141,7 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
125
141
  + 0.5
126
142
  )
127
143
  # y adjustment in mm
128
- y_adjust_left = get_y_adjust(dy_lut_left) / 100
144
+ y_adjust_left = get_y_adjust(dy_lut_left, ancillary_files) / 100
129
145
  # hundredths of a millimeter
130
146
  yf[index_left] = (UltraConstants.YF_ESTIMATE_LEFT - y_adjust_left) * 100
131
147
  # distance adjustment in mm
@@ -141,7 +157,7 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
141
157
  + 0.5
142
158
  )
143
159
  # y adjustment in mm
144
- y_adjust_right = get_y_adjust(dy_lut_right) / 100
160
+ y_adjust_right = get_y_adjust(dy_lut_right, ancillary_files) / 100
145
161
  # hundredths of a millimeter
146
162
  yf[index_right] = (UltraConstants.YF_ESTIMATE_RIGHT + y_adjust_right) * 100
147
163
  # distance adjustment in mm
@@ -153,8 +169,8 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
153
169
 
154
170
 
155
171
  def get_ph_tof_and_back_positions(
156
- de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str
157
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
172
+ de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str, ancillary_files: dict
173
+ ) -> PHTOFResult:
158
174
  """
159
175
  Calculate back xb, yb position and tof.
160
176
 
@@ -176,6 +192,8 @@ def get_ph_tof_and_back_positions(
176
192
  Has same length as de_dataset.
177
193
  sensor : str
178
194
  Sensor name.
195
+ ancillary_files : dict[Path]
196
+ Ancillary files containing the lookup tables.
179
197
 
180
198
  Returns
181
199
  -------
@@ -187,6 +205,10 @@ def get_ph_tof_and_back_positions(
187
205
  Back positions in x direction (hundredths of a millimeter).
188
206
  yb : np.array
189
207
  Back positions in y direction (hundredths of a millimeter).
208
+ tofx : np.array
209
+ X front position tof offset (tenths of a nanosecond).
210
+ tofy : np.array
211
+ Y front position tof offset (tenths of a nanosecond).
190
212
  """
191
213
  indices = np.nonzero(
192
214
  np.isin(de_dataset["stop_type"], [StopType.Top.value, StopType.Bottom.value])
@@ -197,10 +219,18 @@ def get_ph_tof_and_back_positions(
197
219
 
198
220
  # There are mismatches between the stop TDCs, i.e., SpN, SpS, SpE, and SpW.
199
221
  # This normalizes the TDCs
200
- sp_n_norm = get_norm(de_filtered["stop_north_tdc"].data, "SpN", sensor)
201
- sp_s_norm = get_norm(de_filtered["stop_south_tdc"].data, "SpS", sensor)
202
- sp_e_norm = get_norm(de_filtered["stop_east_tdc"].data, "SpE", sensor)
203
- sp_w_norm = get_norm(de_filtered["stop_west_tdc"].data, "SpW", sensor)
222
+ sp_n_norm = get_norm(
223
+ de_filtered["stop_north_tdc"].data, "SpN", sensor, ancillary_files
224
+ )
225
+ sp_s_norm = get_norm(
226
+ de_filtered["stop_south_tdc"].data, "SpS", sensor, ancillary_files
227
+ )
228
+ sp_e_norm = get_norm(
229
+ de_filtered["stop_east_tdc"].data, "SpE", sensor, ancillary_files
230
+ )
231
+ sp_w_norm = get_norm(
232
+ de_filtered["stop_west_tdc"].data, "SpW", sensor, ancillary_files
233
+ )
204
234
 
205
235
  # Convert normalized TDC values into units of hundredths of a
206
236
  # millimeter using lookup tables.
@@ -227,37 +257,41 @@ def get_ph_tof_and_back_positions(
227
257
  # Convert converts normalized TDC values into units of
228
258
  # hundredths of a millimeter using lookup tables.
229
259
  stop_type_top = de_filtered["stop_type"].data == StopType.Top.value
230
- xb[stop_type_top] = get_back_position(xb_index[stop_type_top], "XBkTp", sensor)
231
- yb[stop_type_top] = get_back_position(yb_index[stop_type_top], "YBkTp", sensor)
260
+ xb[stop_type_top] = get_back_position(
261
+ xb_index[stop_type_top], "XBkTp", sensor, ancillary_files
262
+ )
263
+ yb[stop_type_top] = get_back_position(
264
+ yb_index[stop_type_top], "YBkTp", sensor, ancillary_files
265
+ )
232
266
 
233
267
  # Correction for the propagation delay of the start anode and other effects.
234
- t2[stop_type_top] = get_image_params("TOFSC", sensor) * t1[
268
+ t2[stop_type_top] = get_image_params("TOFSC", sensor, ancillary_files) * t1[
235
269
  stop_type_top
236
- ] + get_image_params("TOFTPOFF", sensor)
270
+ ] + get_image_params("TOFTPOFF", sensor, ancillary_files)
237
271
  # Variable xf_ph divided by 10 to convert to mm.
238
272
  tof[stop_type_top] = t2[stop_type_top] + xf_ph[
239
273
  stop_type_top
240
- ] / 10 * get_image_params("XFTTOF", sensor)
274
+ ] / 10 * get_image_params("XFTTOF", sensor, ancillary_files)
241
275
 
242
276
  stop_type_bottom = de_filtered["stop_type"].data == StopType.Bottom.value
243
277
  xb[stop_type_bottom] = get_back_position(
244
- xb_index[stop_type_bottom], "XBkBt", sensor
278
+ xb_index[stop_type_bottom], "XBkBt", sensor, ancillary_files
245
279
  )
246
280
  yb[stop_type_bottom] = get_back_position(
247
- yb_index[stop_type_bottom], "YBkBt", sensor
281
+ yb_index[stop_type_bottom], "YBkBt", sensor, ancillary_files
248
282
  )
249
283
 
250
284
  # Correction for the propagation delay of the start anode and other effects.
251
- t2[stop_type_bottom] = get_image_params("TOFSC", sensor) * t1[
285
+ t2[stop_type_bottom] = get_image_params("TOFSC", sensor, ancillary_files) * t1[
252
286
  stop_type_bottom
253
- ] + get_image_params("TOFBTOFF", sensor) # 10*ns
287
+ ] + get_image_params("TOFBTOFF", sensor, ancillary_files) # 10*ns
254
288
 
255
289
  # Variable xf_ph divided by 10 to convert to mm.
256
290
  tof[stop_type_bottom] = t2[stop_type_bottom] + xf_ph[
257
291
  stop_type_bottom
258
- ] / 10 * get_image_params("XFTTOF", sensor)
292
+ ] / 10 * get_image_params("XFTTOF", sensor, ancillary_files)
259
293
 
260
- return tof, t2, xb, yb
294
+ return PHTOFResult(tof=tof, t2=t2, xb=xb, yb=yb, tofx=tofx, tofy=tofy)
261
295
 
262
296
 
263
297
  def get_path_length(
@@ -290,7 +324,7 @@ def get_path_length(
290
324
 
291
325
 
292
326
  def get_ssd_back_position_and_tof_offset(
293
- de_dataset: xarray.Dataset, sensor: str
327
+ de_dataset: xarray.Dataset, sensor: str, ancillary_files: dict
294
328
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
295
329
  """
296
330
  Lookup the Y SSD positions (yb), TOF Offset, and SSD number.
@@ -301,6 +335,8 @@ def get_ssd_back_position_and_tof_offset(
301
335
  The input dataset containing STOP_TYPE and SSD_FLAG data.
302
336
  sensor : str
303
337
  Sensor name.
338
+ ancillary_files : dict[Path]
339
+ Ancillary files containing the lookup tables.
304
340
 
305
341
  Returns
306
342
  -------
@@ -326,21 +362,27 @@ def get_ssd_back_position_and_tof_offset(
326
362
  ssd_flag_mask = de_filtered[f"ssd_flag_{i}"].data == 1
327
363
 
328
364
  # Multiply ybs times 100 to convert to hundredths of a millimeter.
329
- yb[ssd_flag_mask] = get_image_params(f"YBKSSD{i}", sensor) * 100
365
+ yb[ssd_flag_mask] = (
366
+ get_image_params(f"YBKSSD{i}", sensor, ancillary_files) * 100
367
+ )
330
368
  ssd_number[ssd_flag_mask] = i
331
369
 
332
370
  tof_offset[
333
371
  (de_filtered["start_type"] == StartType.Left.value) & ssd_flag_mask
334
- ] = get_image_params(f"TOFSSDLTOFF{i}", sensor)
372
+ ] = get_image_params(f"TOFSSDLTOFF{i}", sensor, ancillary_files)
335
373
  tof_offset[
336
374
  (de_filtered["start_type"] == StartType.Right.value) & ssd_flag_mask
337
- ] = get_image_params(f"TOFSSDRTOFF{i}", sensor)
375
+ ] = get_image_params(f"TOFSSDRTOFF{i}", sensor, ancillary_files)
338
376
 
339
377
  return yb, tof_offset, ssd_number
340
378
 
341
379
 
342
380
  def calculate_etof_xc(
343
- de_subset: xarray.Dataset, particle_tof: np.ndarray, sensor: str, location: str
381
+ de_subset: xarray.Dataset,
382
+ particle_tof: np.ndarray,
383
+ sensor: str,
384
+ location: str,
385
+ ancillary_files: dict,
344
386
  ) -> tuple[np.ndarray, np.ndarray]:
345
387
  """
346
388
  Calculate the etof and xc values for the given subset.
@@ -355,6 +397,8 @@ def calculate_etof_xc(
355
397
  Sensor name.
356
398
  location : str
357
399
  Location indicator, either 'TP' (Top) or 'BT' (Bottom).
400
+ ancillary_files : dict[Path]
401
+ Ancillary files containing the lookup tables.
358
402
 
359
403
  Returns
360
404
  -------
@@ -365,17 +409,21 @@ def calculate_etof_xc(
365
409
  X coincidence position (millimeters).
366
410
  """
367
411
  # CoinNNorm
368
- coin_n_norm = get_norm(de_subset["coin_north_tdc"], "CoinN", sensor)
412
+ coin_n_norm = get_norm(
413
+ de_subset["coin_north_tdc"], "CoinN", sensor, ancillary_files
414
+ )
369
415
  # CoinSNorm
370
- coin_s_norm = get_norm(de_subset["coin_south_tdc"], "CoinS", sensor)
371
- xc = get_image_params(f"XCOIN{location}SC", sensor) * (
416
+ coin_s_norm = get_norm(
417
+ de_subset["coin_south_tdc"], "CoinS", sensor, ancillary_files
418
+ )
419
+ xc = get_image_params(f"XCOIN{location}SC", sensor, ancillary_files) * (
372
420
  coin_s_norm - coin_n_norm
373
- ) + get_image_params(f"XCOIN{location}OFF", sensor) # millimeter
421
+ ) + get_image_params(f"XCOIN{location}OFF", sensor, ancillary_files) # millimeter
374
422
 
375
423
  # Time for the electrons to travel back to coincidence anode.
376
- t2 = get_image_params("ETOFSC", sensor) * (
424
+ t2 = get_image_params("ETOFSC", sensor, ancillary_files) * (
377
425
  coin_n_norm + coin_s_norm
378
- ) + get_image_params(f"ETOF{location}OFF", sensor)
426
+ ) + get_image_params(f"ETOF{location}OFF", sensor, ancillary_files)
379
427
 
380
428
  # Multiply by 10 to convert to tenths of a nanosecond.
381
429
  etof = t2 * 10 - particle_tof
@@ -384,7 +432,10 @@ def calculate_etof_xc(
384
432
 
385
433
 
386
434
  def get_coincidence_positions(
387
- de_dataset: xarray.Dataset, particle_tof: np.ndarray, sensor: str
435
+ de_dataset: xarray.Dataset,
436
+ particle_tof: np.ndarray,
437
+ sensor: str,
438
+ ancillary_files: dict,
388
439
  ) -> tuple[np.ndarray, np.ndarray]:
389
440
  """
390
441
  Calculate coincidence positions.
@@ -408,6 +459,8 @@ def get_coincidence_positions(
408
459
  (tenths of a nanosecond).
409
460
  sensor : str
410
461
  Sensor name.
462
+ ancillary_files : dict[Path]
463
+ Ancillary files containing the lookup tables.
411
464
 
412
465
  Returns
413
466
  -------
@@ -431,12 +484,14 @@ def get_coincidence_positions(
431
484
  # Normalized TDCs
432
485
  # For the stop anode, there are mismatches between the coincidence TDCs,
433
486
  # i.e., CoinN and CoinS. They must be normalized via lookup tables.
434
- etof_top, xc_top = calculate_etof_xc(de_top, particle_tof[index_top], sensor, "TP")
487
+ etof_top, xc_top = calculate_etof_xc(
488
+ de_top, particle_tof[index_top], sensor, "TP", ancillary_files
489
+ )
435
490
  etof[index_top] = etof_top
436
491
  xc_array[index_top] = xc_top
437
492
 
438
493
  etof_bottom, xc_bottom = calculate_etof_xc(
439
- de_bottom, particle_tof[index_bottom], sensor, "BT"
494
+ de_bottom, particle_tof[index_bottom], sensor, "BT", ancillary_files
440
495
  )
441
496
  etof[index_bottom] = etof_bottom
442
497
  xc_array[index_bottom] = xc_bottom
@@ -488,9 +543,9 @@ def get_de_velocity(
488
543
  v_y = delta_v[:, 1] / tof * 1e3
489
544
  v_z = delta_v[:, 2] / tof * 1e3
490
545
 
491
- v_x[tof < 0] = np.nan # used as fillvals
492
- v_y[tof < 0] = np.nan
493
- v_z[tof < 0] = np.nan
546
+ v_x[tof < 0] = FILLVAL_FLOAT32 # used as fillvals
547
+ v_y[tof < 0] = FILLVAL_FLOAT32
548
+ v_z[tof < 0] = FILLVAL_FLOAT32
494
549
 
495
550
  velocities = np.vstack((v_x, v_y, v_z)).T
496
551
 
@@ -501,7 +556,7 @@ def get_de_velocity(
501
556
 
502
557
 
503
558
  def get_ssd_tof(
504
- de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str
559
+ de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str, ancillary_files: dict
505
560
  ) -> NDArray[np.float64]:
506
561
  """
507
562
  Calculate back xb, yb position for the SSDs.
@@ -527,25 +582,32 @@ def get_ssd_tof(
527
582
  Front x position (hundredths of a millimeter).
528
583
  sensor : str
529
584
  Sensor name.
585
+ ancillary_files : dict[Path]
586
+ Ancillary files containing the lookup tables.
530
587
 
531
588
  Returns
532
589
  -------
533
590
  tof : np.ndarray
534
591
  Time of flight (tenths of a nanosecond).
535
592
  """
536
- _, tof_offset, ssd_number = get_ssd_back_position_and_tof_offset(de_dataset, sensor)
593
+ _, tof_offset, ssd_number = get_ssd_back_position_and_tof_offset(
594
+ de_dataset, sensor, ancillary_files
595
+ )
537
596
  indices = np.nonzero(np.isin(de_dataset["stop_type"], [StopType.SSD.value]))[0]
538
597
 
539
598
  de_discrete = de_dataset.isel(epoch=indices)["coin_discrete_tdc"]
540
599
 
541
- time = get_image_params("TOFSSDSC", sensor) * de_discrete.values + tof_offset
600
+ time = (
601
+ get_image_params("TOFSSDSC", sensor, ancillary_files) * de_discrete.values
602
+ + tof_offset
603
+ )
542
604
 
543
605
  # The scale factor and offsets, and a multiplier to convert xf to a tof offset.
544
606
  # Convert xf to mm by dividing by 100.
545
607
  tof = (
546
608
  time
547
- + get_image_params("TOFSSDTOTOFF", sensor)
548
- + xf[indices] / 100 * get_image_params("XFTTOF", sensor)
609
+ + get_image_params("TOFSSDTOTOFF", sensor, ancillary_files)
610
+ + xf[indices] / 100 * get_image_params("XFTTOF", sensor, ancillary_files)
549
611
  ) * 10
550
612
 
551
613
  # Convert TOF to tenths of a nanosecond.
@@ -572,12 +634,17 @@ def get_de_energy_kev(v: np.ndarray, species: np.ndarray) -> NDArray:
572
634
  # Compute the sum of squares.
573
635
  v2 = np.sum(vv**2, axis=1)
574
636
 
575
- index_hydrogen = np.where(species == 1)
576
- energy = np.full_like(v2, np.nan)
637
+ # Only compute where species == 1 and v is valid
638
+ index_hydrogen = species == 1
639
+ valid_velocity = np.isfinite(v2)
640
+ valid_mask = index_hydrogen & valid_velocity
641
+
642
+ energy = np.full_like(v2, FILLVAL_FLOAT32)
577
643
 
644
+ # TODO: we will calculate the energies of the different species here.
578
645
  # 1/2 mv^2 in Joules, convert to keV
579
- energy[index_hydrogen] = (
580
- 0.5 * UltraConstants.MASS_H * v2[index_hydrogen] * UltraConstants.J_KEV
646
+ energy[valid_mask] = (
647
+ 0.5 * UltraConstants.MASS_H * v2[valid_mask] * UltraConstants.J_KEV
581
648
  )
582
649
 
583
650
  return energy
@@ -589,7 +656,9 @@ def get_energy_pulse_height(
589
656
  xb: np.ndarray,
590
657
  yb: np.ndarray,
591
658
  sensor: str,
592
- ) -> NDArray[np.float64]:
659
+ ancillary_files: dict,
660
+ quality_flags: NDArray,
661
+ ) -> tuple[NDArray, NDArray]:
593
662
  """
594
663
  Calculate the pulse-height energy.
595
664
 
@@ -611,6 +680,10 @@ def get_energy_pulse_height(
611
680
  Y back position (hundredths of a millimeter).
612
681
  sensor : str
613
682
  Sensor name.
683
+ ancillary_files : dict[Path]
684
+ Ancillary files containing the lookup tables.
685
+ quality_flags : NDArray
686
+ Quality flag to set when there is an outlier.
614
687
 
615
688
  Returns
616
689
  -------
@@ -625,28 +698,59 @@ def get_energy_pulse_height(
625
698
  ylut = np.zeros(len(stop_type), dtype=np.float64)
626
699
  energy_ph = np.zeros(len(stop_type), dtype=np.float64)
627
700
 
701
+ # Full-length correction arrays
702
+ ph_correction = np.zeros(len(stop_type), dtype=np.float64)
703
+
628
704
  # Stop type 1
629
- xlut[indices_top] = (xb[indices_top] / 100 - 25 / 2) * 20 / 50 # mm
705
+ xlut[indices_top] = (xb[indices_top] / 100 - 24.5 / 2) * 20 / 50 # mm
630
706
  ylut[indices_top] = (yb[indices_top] / 100 + 82 / 2) * 32 / 82 # mm
631
707
  # Stop type 2
632
- xlut[indices_bottom] = (xb[indices_bottom] / 100 + 50 + 25 / 2) * 20 / 50 # mm
708
+ xlut[indices_bottom] = (xb[indices_bottom] / 100 + 50 + 24.5 / 2) * 20 / 50 # mm
633
709
  ylut[indices_bottom] = (yb[indices_bottom] / 100 + 82 / 2) * 32 / 82 # mm
634
710
 
635
- # TODO: waiting on these lookup tables: SpTpPHCorr, SpBtPHCorr
636
- energy_ph[indices_top] = energy[indices_top] - get_image_params(
637
- "SPTPPHOFF", sensor
638
- ) # * SpTpPHCorr[
639
- # xlut[indices_top], ylut[indices_top]] / 1024
711
+ ph_correction_top, updated_flags_top = get_ph_corrected(
712
+ "ultra45",
713
+ "tp",
714
+ ancillary_files,
715
+ np.round(xlut[indices_top]),
716
+ np.round(ylut[indices_top]),
717
+ quality_flags[indices_top].copy(),
718
+ )
719
+ quality_flags[indices_top] = updated_flags_top
720
+ ph_correction_bottom, updated_flags_bottom = get_ph_corrected(
721
+ "ultra45",
722
+ "bt",
723
+ ancillary_files,
724
+ np.round(xlut[indices_bottom]),
725
+ np.round(ylut[indices_bottom]),
726
+ quality_flags[indices_bottom].copy(),
727
+ )
728
+ quality_flags[indices_bottom] = updated_flags_bottom
729
+
730
+ ph_correction[indices_top] = ph_correction_top / 1024
731
+ ph_correction[indices_bottom] = ph_correction_bottom / 1024
732
+
733
+ energy_ph[indices_top] = (
734
+ (energy[indices_top] - get_image_params("SPTPPHOFF", sensor, ancillary_files))
735
+ * ph_correction_top
736
+ / 1024
737
+ )
640
738
 
641
- energy_ph[indices_bottom] = energy[indices_bottom] - get_image_params(
642
- "SPBTPHOFF", sensor
643
- ) # * SpBtPHCorr[
644
- # xlut[indices_bottom], ylut[indices_bottom]] / 1024
739
+ energy_ph[indices_bottom] = (
740
+ (
741
+ energy[indices_bottom]
742
+ - get_image_params("SPBTPHOFF", sensor, ancillary_files)
743
+ )
744
+ * ph_correction_bottom
745
+ / 1024.0
746
+ )
645
747
 
646
- return energy_ph
748
+ return energy_ph, ph_correction
647
749
 
648
750
 
649
- def get_energy_ssd(de_dataset: xarray.Dataset, ssd: np.ndarray) -> NDArray[np.float64]:
751
+ def get_energy_ssd(
752
+ de_dataset: xarray.Dataset, ssd: np.ndarray, ancillary_files: dict
753
+ ) -> NDArray[np.float64]:
650
754
  """
651
755
  Get SSD energy.
652
756
 
@@ -664,6 +768,8 @@ def get_energy_ssd(de_dataset: xarray.Dataset, ssd: np.ndarray) -> NDArray[np.fl
664
768
  Events dataset.
665
769
  ssd : np.ndarray
666
770
  SSD number.
771
+ ancillary_files : dict[Path]
772
+ Ancillary files containing the lookup tables.
667
773
 
668
774
  Returns
669
775
  -------
@@ -685,7 +791,7 @@ def get_energy_ssd(de_dataset: xarray.Dataset, ssd: np.ndarray) -> NDArray[np.fl
685
791
  energy < UltraConstants.COMPOSITE_ENERGY_THRESHOLD
686
792
  ]
687
793
 
688
- energy_norm = get_energy_norm(ssd, composite_energy)
794
+ energy_norm = get_energy_norm(ssd, composite_energy, ancillary_files)
689
795
 
690
796
  return energy_norm
691
797
 
@@ -760,14 +866,9 @@ def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> ND
760
866
  """
761
867
  # Event TOF normalization to Z axis
762
868
  ctof, _ = get_ctof(tof, path_length, type)
763
- # Initialize bin array
764
- species_bin = np.full(len(ctof), 255, dtype=np.uint8)
765
-
766
- # Assign Species 1 ("H") to bins where cTOF is within the specified range
767
- species_bin[
768
- (ctof > UltraConstants.CTOF_SPECIES_MIN)
769
- & (ctof < UltraConstants.CTOF_SPECIES_MAX)
770
- ] = 1
869
+ # Assign Species 1 ("H") to bins
870
+ # TODO: this is a placeholder for future species assignments.
871
+ species_bin = np.full(len(ctof), 1, dtype=np.uint8)
771
872
 
772
873
  return species_bin
773
874
 
@@ -805,6 +906,68 @@ def get_phi_theta(
805
906
  return np.degrees(phi), np.degrees(theta)
806
907
 
807
908
 
909
+ def get_spin_number(de_met: NDArray, de_spin: NDArray) -> NDArray:
910
+ """
911
+ Get the spin number.
912
+
913
+ Parameters
914
+ ----------
915
+ de_met : NDArray
916
+ Mission elapsed time.
917
+ de_spin : NDArray
918
+ Spin number 0-255.
919
+
920
+ Returns
921
+ -------
922
+ assigned_spin_number : NDArray
923
+ Spin number for DE data product.
924
+ """
925
+ # DE packet data.
926
+ # Since the spin number in the direct events packet
927
+ # is only 8 bits it goes from 0-255.
928
+ # Within a pointing that means we will always have duplicate spin numbers.
929
+ # In other words, different spins will be represented by the same spin number.
930
+ # Just to make certain that we won't accidentally combine
931
+ # multiple spins we need to sort by time here.
932
+ sort_idx = np.argsort(de_met)
933
+ de_met_sorted = de_met[sort_idx]
934
+ de_spin_sorted = de_spin[sort_idx]
935
+ # Here we are finding the start and end indices of each spin in the sorted array.
936
+ is_new_spin = np.concatenate([[True], de_spin_sorted[1:] != de_spin_sorted[:-1]])
937
+ spin_start_indices = np.where(is_new_spin)[0]
938
+ spin_end_indices = np.append(spin_start_indices[1:], len(de_met_sorted))
939
+
940
+ # Universal Spin Table.
941
+ spin_df = get_spin_data()
942
+ # Retrieve the met values of the start of the spin.
943
+ spin_start_mets = spin_df["spin_start_met"].values
944
+ # Retrieve the corresponding spin numbers.
945
+ spin_numbers = spin_df["spin_number"].values
946
+ assigned_spin_number_sorted = np.empty(de_spin_sorted.shape, dtype=np.uint32)
947
+ # These last 8 bits are the same as the spin number in the DE packet.
948
+ # So this will give us choices of which spins are
949
+ # available to assign to the DE data.
950
+ possible_spins = spin_numbers & 0xFF
951
+
952
+ # Assign each group based on time.
953
+ for start, end in zip(spin_start_indices, spin_end_indices, strict=False):
954
+ # Now that we have the possible spins from the Universal Spin Table,
955
+ # we match the times of those spins to the nearest times in the DE data.
956
+ possible_times = spin_start_mets[possible_spins == de_spin_sorted[start]]
957
+ # Get nearest time for matching spins.
958
+ nearest_idx = np.abs(possible_times - de_met_sorted[start]).argmin()
959
+ nearest_value = possible_times[nearest_idx]
960
+ assigned_spin_number_sorted[start:end] = spin_numbers[
961
+ spin_start_mets == nearest_value
962
+ ]
963
+
964
+ # Undo the sort to match original order.
965
+ assigned_spin_number = np.empty_like(assigned_spin_number_sorted)
966
+ assigned_spin_number[sort_idx] = assigned_spin_number_sorted
967
+
968
+ return assigned_spin_number
969
+
970
+
808
971
  def get_eventtimes(
809
972
  spin: NDArray, phase_angle: NDArray
810
973
  ) -> tuple[NDArray, NDArray, NDArray]:
@@ -884,8 +1047,11 @@ def interpolate_fwhm(
884
1047
  )
885
1048
 
886
1049
  # Note: will return nan for those out-of-bounds inputs.
887
- phi_interp = interp_phi((energy, phi_inst))
888
- theta_interp = interp_theta((energy, theta_inst))
1050
+ phi_vals = interp_phi((energy, phi_inst))
1051
+ theta_vals = interp_theta((energy, theta_inst))
1052
+
1053
+ phi_interp = np.where(np.isnan(phi_vals), FILLVAL_FLOAT32, phi_vals)
1054
+ theta_interp = np.where(np.isnan(theta_vals), FILLVAL_FLOAT32, theta_vals)
889
1055
 
890
1056
  return phi_interp, theta_interp
891
1057
 
@@ -896,6 +1062,7 @@ def get_fwhm(
896
1062
  energy: NDArray,
897
1063
  phi_inst: NDArray,
898
1064
  theta_inst: NDArray,
1065
+ ancillary_files: dict,
899
1066
  ) -> tuple[NDArray, NDArray]:
900
1067
  """
901
1068
  Interpolate phi and theta FWHM values for each event based on start type.
@@ -912,6 +1079,8 @@ def get_fwhm(
912
1079
  Instrument-frame azimuth angle for each event.
913
1080
  theta_inst : NDArray
914
1081
  Instrument-frame elevation angle for each event.
1082
+ ancillary_files : dict
1083
+ Ancillary files containing lookup tables for angular profiles.
915
1084
 
916
1085
  Returns
917
1086
  -------
@@ -920,10 +1089,10 @@ def get_fwhm(
920
1089
  theta_interp : NDArray
921
1090
  Interpolated theta FWHM values.
922
1091
  """
923
- phi_interp = np.full_like(phi_inst, np.nan, dtype=np.float64)
924
- theta_interp = np.full_like(theta_inst, np.nan, dtype=np.float64)
925
- lt_table = get_angular_profiles("left", sensor)
926
- rt_table = get_angular_profiles("right", sensor)
1092
+ phi_interp = np.full_like(phi_inst, FILLVAL_FLOAT64, dtype=np.float64)
1093
+ theta_interp = np.full_like(theta_inst, FILLVAL_FLOAT64, dtype=np.float64)
1094
+ lt_table = get_angular_profiles("left", sensor, ancillary_files)
1095
+ rt_table = get_angular_profiles("right", sensor, ancillary_files)
927
1096
 
928
1097
  # Left start type
929
1098
  idx_left = start_type == StartType.Left.value
@@ -940,20 +1109,12 @@ def get_fwhm(
940
1109
  return phi_interp, theta_interp
941
1110
 
942
1111
 
943
- def get_efficiency(
944
- energy: NDArray, phi_inst: NDArray, theta_inst: NDArray, ancillary_files: dict
945
- ) -> NDArray:
1112
+ def get_efficiency_interpolator(ancillary_files: dict) -> RegularGridInterpolator:
946
1113
  """
947
- Interpolate efficiency values for each event.
1114
+ Return a callable function that interpolates efficiency values for each event.
948
1115
 
949
1116
  Parameters
950
1117
  ----------
951
- energy : NDArray
952
- Energy values for each event.
953
- phi_inst : NDArray
954
- Instrument-frame azimuth angle for each event.
955
- theta_inst : NDArray
956
- Instrument-frame elevation angle for each event.
957
1118
  ancillary_files : dict
958
1119
  Ancillary files.
959
1120
 
@@ -978,7 +1139,290 @@ def get_efficiency(
978
1139
  (theta_vals, phi_vals, energy_vals),
979
1140
  efficiency_grid,
980
1141
  bounds_error=False,
981
- fill_value=np.nan,
1142
+ fill_value=FILLVAL_FLOAT32,
982
1143
  )
983
1144
 
1145
+ return interpolator
1146
+
1147
+
1148
+ def get_efficiency(
1149
+ energy: NDArray,
1150
+ phi_inst: NDArray,
1151
+ theta_inst: NDArray,
1152
+ ancillary_files: dict,
1153
+ interpolator: RegularGridInterpolator = None,
1154
+ ) -> np.ndarray:
1155
+ """
1156
+ Return interpolated efficiency values for each event.
1157
+
1158
+ Parameters
1159
+ ----------
1160
+ energy : NDArray
1161
+ Energy values for each event.
1162
+ phi_inst : NDArray
1163
+ Instrument-frame azimuth angle for each event.
1164
+ theta_inst : NDArray
1165
+ Instrument-frame elevation angle for each event.
1166
+ ancillary_files : dict
1167
+ Ancillary files.
1168
+ interpolator : RegularGridInterpolator, optional
1169
+ Precomputed interpolator to use for efficiency lookup.
1170
+ If None, a new interpolator will be created from the ancillary files.
1171
+
1172
+ Returns
1173
+ -------
1174
+ efficiency : NDArray
1175
+ Interpolated efficiency values.
1176
+ """
1177
+ if not interpolator:
1178
+ interpolator = get_efficiency_interpolator(ancillary_files)
1179
+
984
1180
  return interpolator((theta_inst, phi_inst, energy))
1181
+
1182
+
1183
+ def determine_ebin_pulse_height(
1184
+ energy: NDArray,
1185
+ tof: NDArray,
1186
+ path_length: NDArray,
1187
+ backtofvalid: NDArray,
1188
+ coinphvalid: NDArray,
1189
+ ancillary_files: dict,
1190
+ ) -> NDArray:
1191
+ """
1192
+ Determine the species for pulse-height events.
1193
+
1194
+ Species is determined from the particle energy and velocity.
1195
+ For velocity, the particle TOF is normalized with respect
1196
+ to a fixed distance dmin between the front and back detectors.
1197
+ The normalized TOF is termed the corrected TOF (ctof).
1198
+ Particle species are determined from
1199
+ the energy and ctof using a lookup table.
1200
+
1201
+ Further description is available on pages 42-44 of
1202
+ IMAP-Ultra Flight Software Specification document
1203
+ (7523-9009_Rev_-.pdf).
1204
+
1205
+ Parameters
1206
+ ----------
1207
+ energy : NDArray
1208
+ Energy from the PH event (keV).
1209
+ tof : NDArray
1210
+ Time of flight of the PH event (tenths of a nanosecond).
1211
+ path_length : NDArray
1212
+ Path length (r) (hundredths of a millimeter).
1213
+ backtofvalid : NDArray
1214
+ Boolean array indicating if the back TOF is valid.
1215
+ coinphvalid : NDArray
1216
+ Boolean array indicating if the Coincidence PH is valid.
1217
+ ancillary_files : dict
1218
+ Ancillary files containing the lookup tables.
1219
+
1220
+ Returns
1221
+ -------
1222
+ bin : np.array
1223
+ Species bin.
1224
+ """
1225
+ # PH event TOF normalization to Z axis
1226
+ ctof, _ = get_ctof(tof, path_length, type="PH")
1227
+
1228
+ ebins = np.full(path_length.shape, FILLVAL_UINT8, dtype=np.uint8)
1229
+ valid = backtofvalid & coinphvalid
1230
+ ebins[valid] = get_ebins(
1231
+ "l1b-tofxph", energy[valid], ctof[valid], ebins[valid], ancillary_files
1232
+ )
1233
+
1234
+ return ebins
1235
+
1236
+
1237
+ def determine_ebin_ssd(
1238
+ energy: NDArray,
1239
+ tof: NDArray,
1240
+ path_length: NDArray,
1241
+ sensor: str,
1242
+ ancillary_files: dict,
1243
+ ) -> NDArray:
1244
+ """
1245
+ Determine the species for SSD events.
1246
+
1247
+ Species is determined from the particle's energy and velocity.
1248
+ For velocity, the particle's TOF is normalized with respect
1249
+ to a fixed distance dmin between the front and back detectors.
1250
+ For SSD events, an adjustment is also made to the path length
1251
+ to account for the shorter distances that such events
1252
+ travel to reach the detector. The normalized TOF is termed
1253
+ the corrected tof (ctof). Particle species are determined from
1254
+ the energy and cTOF using a lookup table.
1255
+
1256
+ Further description is available on pages 42-44 of
1257
+ IMAP-Ultra Flight Software Specification document
1258
+ (7523-9009_Rev_-.pdf).
1259
+
1260
+ Parameters
1261
+ ----------
1262
+ energy : NDArray
1263
+ Energy from the SSD event (keV).
1264
+ tof : NDArray
1265
+ Time of flight of the SSD event (tenths of a nanosecond).
1266
+ path_length : NDArray
1267
+ Path length (r) (hundredths of a millimeter).
1268
+ sensor : str
1269
+ Sensor name: "ultra45" or "ultra90".
1270
+ ancillary_files : dict
1271
+ Ancillary files containing the lookup tables.
1272
+
1273
+ Returns
1274
+ -------
1275
+ bin : NDArray
1276
+ Species bin.
1277
+ """
1278
+ # SSD event TOF normalization to Z axis
1279
+ ctof, _ = get_ctof(tof, path_length, type="SSD")
1280
+
1281
+ ebins = np.full(path_length.shape, FILLVAL_UINT8, dtype=np.uint8)
1282
+ steep_path_length = get_image_params("PathSteepThresh", sensor, ancillary_files)
1283
+ medium_path_length = get_image_params("PathMediumThresh", sensor, ancillary_files)
1284
+
1285
+ steep_mask = path_length < steep_path_length
1286
+ medium_mask = (path_length >= steep_path_length) & (
1287
+ path_length < medium_path_length
1288
+ )
1289
+ flat_mask = path_length >= medium_path_length
1290
+
1291
+ ebins[steep_mask] = get_ebins(
1292
+ f"l1b-{sensor[5::]}sensor-tofxesteep",
1293
+ energy[steep_mask],
1294
+ ctof[steep_mask],
1295
+ ebins[steep_mask],
1296
+ ancillary_files,
1297
+ )
1298
+ ebins[medium_mask] = get_ebins(
1299
+ f"l1b-{sensor[5::]}sensor-tofxemedium",
1300
+ energy[medium_mask],
1301
+ ctof[medium_mask],
1302
+ ebins[medium_mask],
1303
+ ancillary_files,
1304
+ )
1305
+ ebins[flat_mask] = get_ebins(
1306
+ f"l1b-{sensor[5::]}sensor-tofxeflat",
1307
+ energy[flat_mask],
1308
+ ctof[flat_mask],
1309
+ ebins[flat_mask],
1310
+ ancillary_files,
1311
+ )
1312
+
1313
+ return ebins
1314
+
1315
+
1316
+ def is_back_tof_valid(
1317
+ de_dataset: xarray.Dataset,
1318
+ xf: NDArray,
1319
+ sensor: str,
1320
+ ancillary_files: dict,
1321
+ ) -> NDArray:
1322
+ """
1323
+ Determine whether back TOF is valid based on stop type.
1324
+
1325
+ Parameters
1326
+ ----------
1327
+ de_dataset : xarray.Dataset
1328
+ Data in xarray format.
1329
+ xf : NDArray
1330
+ X front position in (hundredths of a millimeter).
1331
+ Has same length as de_dataset.
1332
+ sensor : str
1333
+ Sensor name: "ultra45" or "ultra90".
1334
+ ancillary_files : dict
1335
+ Ancillary files for lookup.
1336
+
1337
+ Returns
1338
+ -------
1339
+ valid_mask : NDArray
1340
+ Boolean array indicating whether back TOF is valid.
1341
+
1342
+ Notes
1343
+ -----
1344
+ From page 33 of the IMAP-Ultra Flight Software Specification document.
1345
+ """
1346
+ _, _, _, _, tofx, tofy = get_ph_tof_and_back_positions(
1347
+ de_dataset, xf, "ultra45", ancillary_files
1348
+ )
1349
+ diff = tofy - tofx
1350
+
1351
+ indices = np.nonzero(
1352
+ np.isin(de_dataset["stop_type"], [StopType.Top.value, StopType.Bottom.value])
1353
+ )[0]
1354
+ de_ph = de_dataset.isel(epoch=indices)
1355
+
1356
+ top_mask = de_ph["stop_type"] == StopType.Top.value
1357
+ bottom_mask = de_ph["stop_type"] == StopType.Bottom.value
1358
+
1359
+ valid = np.zeros_like(diff, dtype=bool)
1360
+
1361
+ diff_tp_min = get_image_params("TOFDiffTpMin", sensor, ancillary_files)
1362
+ diff_tp_max = get_image_params("TOFDiffTpMax", sensor, ancillary_files)
1363
+ diff_bt_min = get_image_params("TOFDiffBtMin", sensor, ancillary_files)
1364
+ diff_bt_max = get_image_params("TOFDiffBtMax", sensor, ancillary_files)
1365
+
1366
+ valid[top_mask] = (diff[top_mask] >= diff_tp_min) & (diff[top_mask] <= diff_tp_max)
1367
+ valid[bottom_mask] = (diff[bottom_mask] >= diff_bt_min) & (
1368
+ diff[bottom_mask] <= diff_bt_max
1369
+ )
1370
+
1371
+ return valid
1372
+
1373
+
1374
+ def is_coin_ph_valid(
1375
+ etof: NDArray,
1376
+ xc: NDArray,
1377
+ xb: NDArray,
1378
+ sensor: str,
1379
+ ancillary_files: dict,
1380
+ ) -> NDArray:
1381
+ """
1382
+ Determine whether Coincidence-PH data are valid.
1383
+
1384
+ This is based on thresholds defined in the IMAP-Ultra Flight Software Specification
1385
+ (see page 36).
1386
+
1387
+ Parameters
1388
+ ----------
1389
+ etof : NDArray
1390
+ Electron TOF (tenths of a nanosecond).
1391
+ xc : NDArray
1392
+ Coincidence X position (hundredths of a mm).
1393
+ xb : NDArray
1394
+ Back X position (hundredths of a mm).
1395
+ sensor : str
1396
+ Sensor name: "ultra45" or "ultra90".
1397
+ ancillary_files : dict
1398
+ Ancillary files for lookup.
1399
+
1400
+ Returns
1401
+ -------
1402
+ valid_mask : NDArray
1403
+ Boolean array indicating Coin-PH validity.
1404
+
1405
+ Notes
1406
+ -----
1407
+ Logic derived from page 36 of the IMAP-Ultra Flight Software Specification document.
1408
+ """
1409
+ etof_min = get_image_params("eTOFMin", sensor, ancillary_files)
1410
+ etof_max = get_image_params("eTOFMax", sensor, ancillary_files)
1411
+
1412
+ etof_valid = (etof >= etof_min) & (etof <= etof_max)
1413
+
1414
+ diff_x = xc - xb
1415
+ etof_offset1 = get_image_params("eTOFOff1", sensor, ancillary_files)
1416
+ etof_offset2 = get_image_params("eTOFOff2", sensor, ancillary_files)
1417
+ etof_slope1 = get_image_params("eTOFSlope1", sensor, ancillary_files)
1418
+ etof_slope2 = get_image_params("eTOFSlope2", sensor, ancillary_files)
1419
+
1420
+ t1 = (etof - etof_offset1) * etof_slope1 / 1024
1421
+ t2 = (etof - etof_offset2) * etof_slope2 / 1024
1422
+
1423
+ condition_1 = (diff_x >= t1) & (diff_x <= t2)
1424
+ condition_2 = (diff_x >= -t2) & (diff_x <= -t1)
1425
+
1426
+ spatial_valid = condition_1 | condition_2
1427
+
1428
+ return etof_valid & spatial_valid