imap-processing 0.16.2__py3-none-any.whl → 0.18.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 (110) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ccsds/excel_to_xtce.py +12 -0
  3. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -6
  4. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +35 -0
  5. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +35 -0
  6. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +24 -0
  7. imap_processing/cdf/config/imap_hi_variable_attrs.yaml +8 -8
  8. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +1 -1
  9. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +163 -100
  10. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +398 -415
  11. imap_processing/cdf/config/imap_ialirt_l1_variable_attrs.yaml +97 -54
  12. imap_processing/cdf/config/imap_idex_global_cdf_attrs.yaml +9 -9
  13. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +233 -57
  14. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +16 -90
  15. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +30 -0
  16. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +15 -1
  17. imap_processing/cdf/config/imap_swapi_variable_attrs.yaml +19 -0
  18. imap_processing/cdf/config/imap_swe_l1b_variable_attrs.yaml +20 -0
  19. imap_processing/cdf/config/imap_swe_l2_variable_attrs.yaml +39 -0
  20. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +168 -0
  21. imap_processing/cdf/config/imap_ultra_l1a_variable_attrs.yaml +103 -2
  22. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +91 -11
  23. imap_processing/cdf/utils.py +7 -1
  24. imap_processing/cli.py +42 -13
  25. imap_processing/codice/codice_l1a.py +125 -78
  26. imap_processing/codice/codice_l1b.py +1 -1
  27. imap_processing/codice/codice_l2.py +0 -9
  28. imap_processing/codice/constants.py +481 -498
  29. imap_processing/hi/hi_l1a.py +4 -4
  30. imap_processing/hi/hi_l1b.py +2 -2
  31. imap_processing/hi/packet_definitions/TLM_HI_COMBINED_SCI.xml +218 -38
  32. imap_processing/hit/hit_utils.py +2 -2
  33. imap_processing/hit/l0/decom_hit.py +4 -3
  34. imap_processing/hit/l1a/hit_l1a.py +64 -24
  35. imap_processing/hit/l1b/constants.py +5 -0
  36. imap_processing/hit/l1b/hit_l1b.py +18 -16
  37. imap_processing/hit/l2/constants.py +1 -1
  38. imap_processing/hit/l2/hit_l2.py +4 -4
  39. imap_processing/ialirt/constants.py +21 -0
  40. imap_processing/ialirt/generate_coverage.py +188 -0
  41. imap_processing/ialirt/l0/parse_mag.py +62 -5
  42. imap_processing/ialirt/l0/process_swapi.py +1 -1
  43. imap_processing/ialirt/l0/process_swe.py +23 -7
  44. imap_processing/ialirt/utils/constants.py +22 -16
  45. imap_processing/ialirt/utils/create_xarray.py +42 -19
  46. imap_processing/idex/idex_constants.py +8 -5
  47. imap_processing/idex/idex_l2b.py +554 -58
  48. imap_processing/idex/idex_l2c.py +30 -196
  49. imap_processing/lo/l0/lo_apid.py +1 -0
  50. imap_processing/lo/l0/lo_star_sensor.py +48 -0
  51. imap_processing/lo/l1a/lo_l1a.py +74 -30
  52. imap_processing/lo/packet_definitions/lo_xtce.xml +5359 -106
  53. imap_processing/mag/constants.py +1 -0
  54. imap_processing/mag/l0/decom_mag.py +9 -6
  55. imap_processing/mag/l0/mag_l0_data.py +46 -0
  56. imap_processing/mag/l1d/__init__.py +0 -0
  57. imap_processing/mag/l1d/mag_l1d.py +133 -0
  58. imap_processing/mag/l1d/mag_l1d_data.py +588 -0
  59. imap_processing/mag/l2/__init__.py +0 -0
  60. imap_processing/mag/l2/mag_l2.py +25 -20
  61. imap_processing/mag/l2/mag_l2_data.py +191 -130
  62. imap_processing/quality_flags.py +20 -2
  63. imap_processing/spice/geometry.py +25 -3
  64. imap_processing/spice/pointing_frame.py +1 -1
  65. imap_processing/spice/spin.py +4 -0
  66. imap_processing/spice/time.py +51 -0
  67. imap_processing/swapi/l1/swapi_l1.py +12 -2
  68. imap_processing/swapi/l2/swapi_l2.py +59 -14
  69. imap_processing/swapi/swapi_utils.py +1 -1
  70. imap_processing/swe/l1b/swe_l1b.py +11 -4
  71. imap_processing/swe/l2/swe_l2.py +111 -17
  72. imap_processing/ultra/constants.py +49 -1
  73. imap_processing/ultra/l0/decom_tools.py +28 -14
  74. imap_processing/ultra/l0/decom_ultra.py +225 -15
  75. imap_processing/ultra/l0/ultra_utils.py +281 -8
  76. imap_processing/ultra/l1a/ultra_l1a.py +77 -8
  77. imap_processing/ultra/l1b/cullingmask.py +3 -3
  78. imap_processing/ultra/l1b/de.py +53 -15
  79. imap_processing/ultra/l1b/extendedspin.py +26 -2
  80. imap_processing/ultra/l1b/lookup_utils.py +171 -50
  81. imap_processing/ultra/l1b/quality_flag_filters.py +14 -0
  82. imap_processing/ultra/l1b/ultra_l1b_culling.py +198 -5
  83. imap_processing/ultra/l1b/ultra_l1b_extended.py +304 -66
  84. imap_processing/ultra/l1c/helio_pset.py +54 -7
  85. imap_processing/ultra/l1c/spacecraft_pset.py +9 -1
  86. imap_processing/ultra/l1c/ultra_l1c.py +2 -0
  87. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +106 -109
  88. imap_processing/ultra/packet_definitions/ULTRA_SCI_COMBINED.xml +3 -3
  89. imap_processing/ultra/utils/ultra_l1_utils.py +13 -1
  90. imap_processing/utils.py +20 -42
  91. {imap_processing-0.16.2.dist-info → imap_processing-0.18.0.dist-info}/METADATA +2 -2
  92. {imap_processing-0.16.2.dist-info → imap_processing-0.18.0.dist-info}/RECORD +95 -103
  93. imap_processing/lo/l0/data_classes/star_sensor.py +0 -98
  94. imap_processing/lo/l0/utils/lo_base.py +0 -57
  95. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +0 -526
  96. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +0 -526
  97. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +0 -526
  98. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +0 -524
  99. imap_processing/ultra/lookup_tables/EgyNorm.mem.csv +0 -32769
  100. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  101. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  102. imap_processing/ultra/lookup_tables/dps_grid45_compressed.cdf +0 -0
  103. imap_processing/ultra/lookup_tables/ultra45_back-pos-luts.csv +0 -4097
  104. imap_processing/ultra/lookup_tables/ultra45_tdc_norm.csv +0 -2050
  105. imap_processing/ultra/lookup_tables/ultra90_back-pos-luts.csv +0 -4097
  106. imap_processing/ultra/lookup_tables/ultra90_tdc_norm.csv +0 -2050
  107. imap_processing/ultra/lookup_tables/yadjust.csv +0 -257
  108. {imap_processing-0.16.2.dist-info → imap_processing-0.18.0.dist-info}/LICENSE +0 -0
  109. {imap_processing-0.16.2.dist-info → imap_processing-0.18.0.dist-info}/WHEEL +0 -0
  110. {imap_processing-0.16.2.dist-info → imap_processing-0.18.0.dist-info}/entry_points.txt +0 -0
@@ -20,6 +20,7 @@ from imap_processing.ultra.l1b.lookup_utils import (
20
20
  get_energy_norm,
21
21
  get_image_params,
22
22
  get_norm,
23
+ get_ph_corrected,
23
24
  get_y_adjust,
24
25
  )
25
26
 
@@ -50,7 +51,7 @@ class CoinType(Enum):
50
51
 
51
52
 
52
53
  def get_front_x_position(
53
- start_type: ndarray, start_position_tdc: ndarray, sensor: str
54
+ start_type: ndarray, start_position_tdc: ndarray, sensor: str, ancillary_files: dict
54
55
  ) -> ndarray:
55
56
  """
56
57
  Calculate the front xf position.
@@ -68,6 +69,8 @@ def get_front_x_position(
68
69
  Start Position Time to Digital Converter (TDC).
69
70
  sensor : str
70
71
  Sensor name.
72
+ ancillary_files : dict[Path]
73
+ Ancillary files containing the lookup tables.
71
74
 
72
75
  Returns
73
76
  -------
@@ -77,9 +80,9 @@ def get_front_x_position(
77
80
  # Left and right start types.
78
81
  indices = np.nonzero((start_type == 1) | (start_type == 2))
79
82
 
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)
83
+ xftsc = get_image_params("XFTSC", sensor, ancillary_files)
84
+ xft_lt_off = get_image_params("XFTLTOFF", sensor, ancillary_files)
85
+ xft_rt_off = get_image_params("XFTRTOFF", sensor, ancillary_files)
83
86
  xft_off = np.where(start_type[indices] == 1, xft_lt_off, xft_rt_off)
84
87
 
85
88
  # Calculate xf and convert to hundredths of a millimeter
@@ -88,7 +91,9 @@ def get_front_x_position(
88
91
  return xf
89
92
 
90
93
 
91
- def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, ndarray]:
94
+ def get_front_y_position(
95
+ start_type: ndarray, yb: ndarray, ancillary_files: dict
96
+ ) -> tuple[ndarray, ndarray]:
92
97
  """
93
98
  Compute the adjustments for the front y position and distance front to back.
94
99
 
@@ -102,6 +107,8 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
102
107
  Start Type: 1=Left, 2=Right.
103
108
  yb : np.array
104
109
  Y back position in hundredths of a millimeter.
110
+ ancillary_files : dict[Path]
111
+ Ancillary files containing the lookup tables.
105
112
 
106
113
  Returns
107
114
  -------
@@ -125,7 +132,7 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
125
132
  + 0.5
126
133
  )
127
134
  # y adjustment in mm
128
- y_adjust_left = get_y_adjust(dy_lut_left) / 100
135
+ y_adjust_left = get_y_adjust(dy_lut_left, ancillary_files) / 100
129
136
  # hundredths of a millimeter
130
137
  yf[index_left] = (UltraConstants.YF_ESTIMATE_LEFT - y_adjust_left) * 100
131
138
  # distance adjustment in mm
@@ -141,7 +148,7 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
141
148
  + 0.5
142
149
  )
143
150
  # y adjustment in mm
144
- y_adjust_right = get_y_adjust(dy_lut_right) / 100
151
+ y_adjust_right = get_y_adjust(dy_lut_right, ancillary_files) / 100
145
152
  # hundredths of a millimeter
146
153
  yf[index_right] = (UltraConstants.YF_ESTIMATE_RIGHT + y_adjust_right) * 100
147
154
  # distance adjustment in mm
@@ -153,7 +160,7 @@ def get_front_y_position(start_type: ndarray, yb: ndarray) -> tuple[ndarray, nda
153
160
 
154
161
 
155
162
  def get_ph_tof_and_back_positions(
156
- de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str
163
+ de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str, ancillary_files: dict
157
164
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
158
165
  """
159
166
  Calculate back xb, yb position and tof.
@@ -176,6 +183,8 @@ def get_ph_tof_and_back_positions(
176
183
  Has same length as de_dataset.
177
184
  sensor : str
178
185
  Sensor name.
186
+ ancillary_files : dict[Path]
187
+ Ancillary files containing the lookup tables.
179
188
 
180
189
  Returns
181
190
  -------
@@ -197,10 +206,18 @@ def get_ph_tof_and_back_positions(
197
206
 
198
207
  # There are mismatches between the stop TDCs, i.e., SpN, SpS, SpE, and SpW.
199
208
  # 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)
209
+ sp_n_norm = get_norm(
210
+ de_filtered["stop_north_tdc"].data, "SpN", sensor, ancillary_files
211
+ )
212
+ sp_s_norm = get_norm(
213
+ de_filtered["stop_south_tdc"].data, "SpS", sensor, ancillary_files
214
+ )
215
+ sp_e_norm = get_norm(
216
+ de_filtered["stop_east_tdc"].data, "SpE", sensor, ancillary_files
217
+ )
218
+ sp_w_norm = get_norm(
219
+ de_filtered["stop_west_tdc"].data, "SpW", sensor, ancillary_files
220
+ )
204
221
 
205
222
  # Convert normalized TDC values into units of hundredths of a
206
223
  # millimeter using lookup tables.
@@ -227,35 +244,39 @@ def get_ph_tof_and_back_positions(
227
244
  # Convert converts normalized TDC values into units of
228
245
  # hundredths of a millimeter using lookup tables.
229
246
  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)
247
+ xb[stop_type_top] = get_back_position(
248
+ xb_index[stop_type_top], "XBkTp", sensor, ancillary_files
249
+ )
250
+ yb[stop_type_top] = get_back_position(
251
+ yb_index[stop_type_top], "YBkTp", sensor, ancillary_files
252
+ )
232
253
 
233
254
  # Correction for the propagation delay of the start anode and other effects.
234
- t2[stop_type_top] = get_image_params("TOFSC", sensor) * t1[
255
+ t2[stop_type_top] = get_image_params("TOFSC", sensor, ancillary_files) * t1[
235
256
  stop_type_top
236
- ] + get_image_params("TOFTPOFF", sensor)
257
+ ] + get_image_params("TOFTPOFF", sensor, ancillary_files)
237
258
  # Variable xf_ph divided by 10 to convert to mm.
238
259
  tof[stop_type_top] = t2[stop_type_top] + xf_ph[
239
260
  stop_type_top
240
- ] / 10 * get_image_params("XFTTOF", sensor)
261
+ ] / 10 * get_image_params("XFTTOF", sensor, ancillary_files)
241
262
 
242
263
  stop_type_bottom = de_filtered["stop_type"].data == StopType.Bottom.value
243
264
  xb[stop_type_bottom] = get_back_position(
244
- xb_index[stop_type_bottom], "XBkBt", sensor
265
+ xb_index[stop_type_bottom], "XBkBt", sensor, ancillary_files
245
266
  )
246
267
  yb[stop_type_bottom] = get_back_position(
247
- yb_index[stop_type_bottom], "YBkBt", sensor
268
+ yb_index[stop_type_bottom], "YBkBt", sensor, ancillary_files
248
269
  )
249
270
 
250
271
  # Correction for the propagation delay of the start anode and other effects.
251
- t2[stop_type_bottom] = get_image_params("TOFSC", sensor) * t1[
272
+ t2[stop_type_bottom] = get_image_params("TOFSC", sensor, ancillary_files) * t1[
252
273
  stop_type_bottom
253
- ] + get_image_params("TOFBTOFF", sensor) # 10*ns
274
+ ] + get_image_params("TOFBTOFF", sensor, ancillary_files) # 10*ns
254
275
 
255
276
  # Variable xf_ph divided by 10 to convert to mm.
256
277
  tof[stop_type_bottom] = t2[stop_type_bottom] + xf_ph[
257
278
  stop_type_bottom
258
- ] / 10 * get_image_params("XFTTOF", sensor)
279
+ ] / 10 * get_image_params("XFTTOF", sensor, ancillary_files)
259
280
 
260
281
  return tof, t2, xb, yb
261
282
 
@@ -290,7 +311,7 @@ def get_path_length(
290
311
 
291
312
 
292
313
  def get_ssd_back_position_and_tof_offset(
293
- de_dataset: xarray.Dataset, sensor: str
314
+ de_dataset: xarray.Dataset, sensor: str, ancillary_files: dict
294
315
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
295
316
  """
296
317
  Lookup the Y SSD positions (yb), TOF Offset, and SSD number.
@@ -301,6 +322,8 @@ def get_ssd_back_position_and_tof_offset(
301
322
  The input dataset containing STOP_TYPE and SSD_FLAG data.
302
323
  sensor : str
303
324
  Sensor name.
325
+ ancillary_files : dict[Path]
326
+ Ancillary files containing the lookup tables.
304
327
 
305
328
  Returns
306
329
  -------
@@ -326,21 +349,27 @@ def get_ssd_back_position_and_tof_offset(
326
349
  ssd_flag_mask = de_filtered[f"ssd_flag_{i}"].data == 1
327
350
 
328
351
  # Multiply ybs times 100 to convert to hundredths of a millimeter.
329
- yb[ssd_flag_mask] = get_image_params(f"YBKSSD{i}", sensor) * 100
352
+ yb[ssd_flag_mask] = (
353
+ get_image_params(f"YBKSSD{i}", sensor, ancillary_files) * 100
354
+ )
330
355
  ssd_number[ssd_flag_mask] = i
331
356
 
332
357
  tof_offset[
333
358
  (de_filtered["start_type"] == StartType.Left.value) & ssd_flag_mask
334
- ] = get_image_params(f"TOFSSDLTOFF{i}", sensor)
359
+ ] = get_image_params(f"TOFSSDLTOFF{i}", sensor, ancillary_files)
335
360
  tof_offset[
336
361
  (de_filtered["start_type"] == StartType.Right.value) & ssd_flag_mask
337
- ] = get_image_params(f"TOFSSDRTOFF{i}", sensor)
362
+ ] = get_image_params(f"TOFSSDRTOFF{i}", sensor, ancillary_files)
338
363
 
339
364
  return yb, tof_offset, ssd_number
340
365
 
341
366
 
342
367
  def calculate_etof_xc(
343
- de_subset: xarray.Dataset, particle_tof: np.ndarray, sensor: str, location: str
368
+ de_subset: xarray.Dataset,
369
+ particle_tof: np.ndarray,
370
+ sensor: str,
371
+ location: str,
372
+ ancillary_files: dict,
344
373
  ) -> tuple[np.ndarray, np.ndarray]:
345
374
  """
346
375
  Calculate the etof and xc values for the given subset.
@@ -355,6 +384,8 @@ def calculate_etof_xc(
355
384
  Sensor name.
356
385
  location : str
357
386
  Location indicator, either 'TP' (Top) or 'BT' (Bottom).
387
+ ancillary_files : dict[Path]
388
+ Ancillary files containing the lookup tables.
358
389
 
359
390
  Returns
360
391
  -------
@@ -365,17 +396,21 @@ def calculate_etof_xc(
365
396
  X coincidence position (millimeters).
366
397
  """
367
398
  # CoinNNorm
368
- coin_n_norm = get_norm(de_subset["coin_north_tdc"], "CoinN", sensor)
399
+ coin_n_norm = get_norm(
400
+ de_subset["coin_north_tdc"], "CoinN", sensor, ancillary_files
401
+ )
369
402
  # CoinSNorm
370
- coin_s_norm = get_norm(de_subset["coin_south_tdc"], "CoinS", sensor)
371
- xc = get_image_params(f"XCOIN{location}SC", sensor) * (
403
+ coin_s_norm = get_norm(
404
+ de_subset["coin_south_tdc"], "CoinS", sensor, ancillary_files
405
+ )
406
+ xc = get_image_params(f"XCOIN{location}SC", sensor, ancillary_files) * (
372
407
  coin_s_norm - coin_n_norm
373
- ) + get_image_params(f"XCOIN{location}OFF", sensor) # millimeter
408
+ ) + get_image_params(f"XCOIN{location}OFF", sensor, ancillary_files) # millimeter
374
409
 
375
410
  # Time for the electrons to travel back to coincidence anode.
376
- t2 = get_image_params("ETOFSC", sensor) * (
411
+ t2 = get_image_params("ETOFSC", sensor, ancillary_files) * (
377
412
  coin_n_norm + coin_s_norm
378
- ) + get_image_params(f"ETOF{location}OFF", sensor)
413
+ ) + get_image_params(f"ETOF{location}OFF", sensor, ancillary_files)
379
414
 
380
415
  # Multiply by 10 to convert to tenths of a nanosecond.
381
416
  etof = t2 * 10 - particle_tof
@@ -384,7 +419,10 @@ def calculate_etof_xc(
384
419
 
385
420
 
386
421
  def get_coincidence_positions(
387
- de_dataset: xarray.Dataset, particle_tof: np.ndarray, sensor: str
422
+ de_dataset: xarray.Dataset,
423
+ particle_tof: np.ndarray,
424
+ sensor: str,
425
+ ancillary_files: dict,
388
426
  ) -> tuple[np.ndarray, np.ndarray]:
389
427
  """
390
428
  Calculate coincidence positions.
@@ -408,6 +446,8 @@ def get_coincidence_positions(
408
446
  (tenths of a nanosecond).
409
447
  sensor : str
410
448
  Sensor name.
449
+ ancillary_files : dict[Path]
450
+ Ancillary files containing the lookup tables.
411
451
 
412
452
  Returns
413
453
  -------
@@ -431,12 +471,14 @@ def get_coincidence_positions(
431
471
  # Normalized TDCs
432
472
  # For the stop anode, there are mismatches between the coincidence TDCs,
433
473
  # 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")
474
+ etof_top, xc_top = calculate_etof_xc(
475
+ de_top, particle_tof[index_top], sensor, "TP", ancillary_files
476
+ )
435
477
  etof[index_top] = etof_top
436
478
  xc_array[index_top] = xc_top
437
479
 
438
480
  etof_bottom, xc_bottom = calculate_etof_xc(
439
- de_bottom, particle_tof[index_bottom], sensor, "BT"
481
+ de_bottom, particle_tof[index_bottom], sensor, "BT", ancillary_files
440
482
  )
441
483
  etof[index_bottom] = etof_bottom
442
484
  xc_array[index_bottom] = xc_bottom
@@ -501,7 +543,7 @@ def get_de_velocity(
501
543
 
502
544
 
503
545
  def get_ssd_tof(
504
- de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str
546
+ de_dataset: xarray.Dataset, xf: np.ndarray, sensor: str, ancillary_files: dict
505
547
  ) -> NDArray[np.float64]:
506
548
  """
507
549
  Calculate back xb, yb position for the SSDs.
@@ -527,25 +569,32 @@ def get_ssd_tof(
527
569
  Front x position (hundredths of a millimeter).
528
570
  sensor : str
529
571
  Sensor name.
572
+ ancillary_files : dict[Path]
573
+ Ancillary files containing the lookup tables.
530
574
 
531
575
  Returns
532
576
  -------
533
577
  tof : np.ndarray
534
578
  Time of flight (tenths of a nanosecond).
535
579
  """
536
- _, tof_offset, ssd_number = get_ssd_back_position_and_tof_offset(de_dataset, sensor)
580
+ _, tof_offset, ssd_number = get_ssd_back_position_and_tof_offset(
581
+ de_dataset, sensor, ancillary_files
582
+ )
537
583
  indices = np.nonzero(np.isin(de_dataset["stop_type"], [StopType.SSD.value]))[0]
538
584
 
539
585
  de_discrete = de_dataset.isel(epoch=indices)["coin_discrete_tdc"]
540
586
 
541
- time = get_image_params("TOFSSDSC", sensor) * de_discrete.values + tof_offset
587
+ time = (
588
+ get_image_params("TOFSSDSC", sensor, ancillary_files) * de_discrete.values
589
+ + tof_offset
590
+ )
542
591
 
543
592
  # The scale factor and offsets, and a multiplier to convert xf to a tof offset.
544
593
  # Convert xf to mm by dividing by 100.
545
594
  tof = (
546
595
  time
547
- + get_image_params("TOFSSDTOTOFF", sensor)
548
- + xf[indices] / 100 * get_image_params("XFTTOF", sensor)
596
+ + get_image_params("TOFSSDTOTOFF", sensor, ancillary_files)
597
+ + xf[indices] / 100 * get_image_params("XFTTOF", sensor, ancillary_files)
549
598
  ) * 10
550
599
 
551
600
  # Convert TOF to tenths of a nanosecond.
@@ -575,6 +624,7 @@ def get_de_energy_kev(v: np.ndarray, species: np.ndarray) -> NDArray:
575
624
  index_hydrogen = np.where(species == 1)
576
625
  energy = np.full_like(v2, np.nan)
577
626
 
627
+ # TODO: we will calculate the energies of the different species here.
578
628
  # 1/2 mv^2 in Joules, convert to keV
579
629
  energy[index_hydrogen] = (
580
630
  0.5 * UltraConstants.MASS_H * v2[index_hydrogen] * UltraConstants.J_KEV
@@ -589,7 +639,9 @@ def get_energy_pulse_height(
589
639
  xb: np.ndarray,
590
640
  yb: np.ndarray,
591
641
  sensor: str,
592
- ) -> NDArray[np.float64]:
642
+ ancillary_files: dict,
643
+ quality_flags: NDArray,
644
+ ) -> tuple[NDArray, NDArray]:
593
645
  """
594
646
  Calculate the pulse-height energy.
595
647
 
@@ -611,6 +663,10 @@ def get_energy_pulse_height(
611
663
  Y back position (hundredths of a millimeter).
612
664
  sensor : str
613
665
  Sensor name.
666
+ ancillary_files : dict[Path]
667
+ Ancillary files containing the lookup tables.
668
+ quality_flags : NDArray
669
+ Quality flag to set when there is an outlier.
614
670
 
615
671
  Returns
616
672
  -------
@@ -625,28 +681,59 @@ def get_energy_pulse_height(
625
681
  ylut = np.zeros(len(stop_type), dtype=np.float64)
626
682
  energy_ph = np.zeros(len(stop_type), dtype=np.float64)
627
683
 
684
+ # Full-length correction arrays
685
+ ph_correction = np.zeros(len(stop_type), dtype=np.float64)
686
+
628
687
  # Stop type 1
629
- xlut[indices_top] = (xb[indices_top] / 100 - 25 / 2) * 20 / 50 # mm
688
+ xlut[indices_top] = (xb[indices_top] / 100 - 24.5 / 2) * 20 / 50 # mm
630
689
  ylut[indices_top] = (yb[indices_top] / 100 + 82 / 2) * 32 / 82 # mm
631
690
  # Stop type 2
632
- xlut[indices_bottom] = (xb[indices_bottom] / 100 + 50 + 25 / 2) * 20 / 50 # mm
691
+ xlut[indices_bottom] = (xb[indices_bottom] / 100 + 50 + 24.5 / 2) * 20 / 50 # mm
633
692
  ylut[indices_bottom] = (yb[indices_bottom] / 100 + 82 / 2) * 32 / 82 # mm
634
693
 
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
694
+ ph_correction_top, updated_flags_top = get_ph_corrected(
695
+ "ultra45",
696
+ "tp",
697
+ ancillary_files,
698
+ np.round(xlut[indices_top]),
699
+ np.round(ylut[indices_top]),
700
+ quality_flags[indices_top].copy(),
701
+ )
702
+ quality_flags[indices_top] = updated_flags_top
703
+ ph_correction_bottom, updated_flags_bottom = get_ph_corrected(
704
+ "ultra45",
705
+ "bt",
706
+ ancillary_files,
707
+ np.round(xlut[indices_bottom]),
708
+ np.round(ylut[indices_bottom]),
709
+ quality_flags[indices_bottom].copy(),
710
+ )
711
+ quality_flags[indices_bottom] = updated_flags_bottom
712
+
713
+ ph_correction[indices_top] = ph_correction_top / 1024
714
+ ph_correction[indices_bottom] = ph_correction_bottom / 1024
640
715
 
641
- energy_ph[indices_bottom] = energy[indices_bottom] - get_image_params(
642
- "SPBTPHOFF", sensor
643
- ) # * SpBtPHCorr[
644
- # xlut[indices_bottom], ylut[indices_bottom]] / 1024
716
+ energy_ph[indices_top] = (
717
+ (energy[indices_top] - get_image_params("SPTPPHOFF", sensor, ancillary_files))
718
+ * ph_correction_top
719
+ / 1024
720
+ )
721
+
722
+ energy_ph[indices_bottom] = (
723
+ (
724
+ energy[indices_bottom]
725
+ - get_image_params("SPBTPHOFF", sensor, ancillary_files)
726
+ )
727
+ * ph_correction_bottom
728
+ / 1024.0
729
+ )
645
730
 
646
- return energy_ph
731
+ return energy_ph, ph_correction
647
732
 
648
733
 
649
- def get_energy_ssd(de_dataset: xarray.Dataset, ssd: np.ndarray) -> NDArray[np.float64]:
734
+ def get_energy_ssd(
735
+ de_dataset: xarray.Dataset, ssd: np.ndarray, ancillary_files: dict
736
+ ) -> NDArray[np.float64]:
650
737
  """
651
738
  Get SSD energy.
652
739
 
@@ -664,6 +751,8 @@ def get_energy_ssd(de_dataset: xarray.Dataset, ssd: np.ndarray) -> NDArray[np.fl
664
751
  Events dataset.
665
752
  ssd : np.ndarray
666
753
  SSD number.
754
+ ancillary_files : dict[Path]
755
+ Ancillary files containing the lookup tables.
667
756
 
668
757
  Returns
669
758
  -------
@@ -685,7 +774,7 @@ def get_energy_ssd(de_dataset: xarray.Dataset, ssd: np.ndarray) -> NDArray[np.fl
685
774
  energy < UltraConstants.COMPOSITE_ENERGY_THRESHOLD
686
775
  ]
687
776
 
688
- energy_norm = get_energy_norm(ssd, composite_energy)
777
+ energy_norm = get_energy_norm(ssd, composite_energy, ancillary_files)
689
778
 
690
779
  return energy_norm
691
780
 
@@ -760,14 +849,9 @@ def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> ND
760
849
  """
761
850
  # Event TOF normalization to Z axis
762
851
  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
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)
771
855
 
772
856
  return species_bin
773
857
 
@@ -805,6 +889,68 @@ def get_phi_theta(
805
889
  return np.degrees(phi), np.degrees(theta)
806
890
 
807
891
 
892
+ def get_spin_number(de_met: NDArray, de_spin: NDArray) -> NDArray:
893
+ """
894
+ Get the spin number.
895
+
896
+ Parameters
897
+ ----------
898
+ de_met : NDArray
899
+ Mission elapsed time.
900
+ de_spin : NDArray
901
+ Spin number 0-255.
902
+
903
+ Returns
904
+ -------
905
+ assigned_spin_number : NDArray
906
+ Spin number for DE data product.
907
+ """
908
+ # DE packet data.
909
+ # Since the spin number in the direct events packet
910
+ # is only 8 bits it goes from 0-255.
911
+ # Within a pointing that means we will always have duplicate spin numbers.
912
+ # In other words, different spins will be represented by the same spin number.
913
+ # Just to make certain that we won't accidentally combine
914
+ # multiple spins we need to sort by time here.
915
+ sort_idx = np.argsort(de_met)
916
+ de_met_sorted = de_met[sort_idx]
917
+ de_spin_sorted = de_spin[sort_idx]
918
+ # Here we are finding the start and end indices of each spin in the sorted array.
919
+ is_new_spin = np.concatenate([[True], de_spin_sorted[1:] != de_spin_sorted[:-1]])
920
+ spin_start_indices = np.where(is_new_spin)[0]
921
+ spin_end_indices = np.append(spin_start_indices[1:], len(de_met_sorted))
922
+
923
+ # Universal Spin Table.
924
+ spin_df = get_spin_data()
925
+ # Retrieve the met values of the start of the spin.
926
+ spin_start_mets = spin_df["spin_start_met"].values
927
+ # Retrieve the corresponding spin numbers.
928
+ spin_numbers = spin_df["spin_number"].values
929
+ assigned_spin_number_sorted = np.empty(de_spin_sorted.shape, dtype=np.uint32)
930
+ # These last 8 bits are the same as the spin number in the DE packet.
931
+ # So this will give us choices of which spins are
932
+ # available to assign to the DE data.
933
+ possible_spins = spin_numbers & 0xFF
934
+
935
+ # Assign each group based on time.
936
+ for start, end in zip(spin_start_indices, spin_end_indices):
937
+ # Now that we have the possible spins from the Universal Spin Table,
938
+ # we match the times of those spins to the nearest times in the DE data.
939
+ possible_times = spin_start_mets[possible_spins == de_spin_sorted[start]]
940
+ # Get nearest time for matching spins.
941
+ nearest_idx = np.abs(possible_times - de_met_sorted[start]).argmin()
942
+ nearest_value = possible_times[nearest_idx]
943
+ assigned_spin_number_sorted[start:end] = spin_numbers[
944
+ spin_start_mets == nearest_value
945
+ ]
946
+
947
+ # Undo the sort to match original order.
948
+ assigned_spin_number = np.empty_like(assigned_spin_number_sorted)
949
+ assigned_spin_number[sort_idx] = assigned_spin_number_sorted
950
+
951
+ return assigned_spin_number
952
+
953
+
808
954
  def get_eventtimes(
809
955
  spin: NDArray, phase_angle: NDArray
810
956
  ) -> tuple[NDArray, NDArray, NDArray]:
@@ -896,6 +1042,7 @@ def get_fwhm(
896
1042
  energy: NDArray,
897
1043
  phi_inst: NDArray,
898
1044
  theta_inst: NDArray,
1045
+ ancillary_files: dict,
899
1046
  ) -> tuple[NDArray, NDArray]:
900
1047
  """
901
1048
  Interpolate phi and theta FWHM values for each event based on start type.
@@ -912,6 +1059,8 @@ def get_fwhm(
912
1059
  Instrument-frame azimuth angle for each event.
913
1060
  theta_inst : NDArray
914
1061
  Instrument-frame elevation angle for each event.
1062
+ ancillary_files : dict
1063
+ Ancillary files containing lookup tables for angular profiles.
915
1064
 
916
1065
  Returns
917
1066
  -------
@@ -922,8 +1071,8 @@ def get_fwhm(
922
1071
  """
923
1072
  phi_interp = np.full_like(phi_inst, np.nan, dtype=np.float64)
924
1073
  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)
1074
+ lt_table = get_angular_profiles("left", sensor, ancillary_files)
1075
+ rt_table = get_angular_profiles("right", sensor, ancillary_files)
927
1076
 
928
1077
  # Left start type
929
1078
  idx_left = start_type == StartType.Left.value
@@ -982,3 +1131,92 @@ def get_efficiency(
982
1131
  )
983
1132
 
984
1133
  return interpolator((theta_inst, phi_inst, energy))
1134
+
1135
+
1136
+ def determine_ebin_pulse_height(
1137
+ energy: np.ndarray, tof: np.ndarray, path_length: np.ndarray
1138
+ ) -> NDArray:
1139
+ """
1140
+ Determine the species for pulse-height events.
1141
+
1142
+ Species is determined from the particle energy and velocity.
1143
+ For velocity, the particle TOF is normalized with respect
1144
+ to a fixed distance dmin between the front and back detectors.
1145
+ The normalized TOF is termed the corrected TOF (ctof).
1146
+ Particle species are determined from
1147
+ the energy and ctof using a lookup table.
1148
+
1149
+ Further description is available on pages 42-44 of
1150
+ IMAP-Ultra Flight Software Specification document
1151
+ (7523-9009_Rev_-.pdf).
1152
+
1153
+ Parameters
1154
+ ----------
1155
+ energy : np.ndarray
1156
+ Energy from the PH event (keV).
1157
+ tof : np.ndarray
1158
+ Time of flight of the PH event (tenths of a nanosecond).
1159
+ path_length : np.ndarray
1160
+ Path length (r) (hundredths of a millimeter).
1161
+
1162
+ Returns
1163
+ -------
1164
+ bin : np.array
1165
+ Species bin.
1166
+ """
1167
+ # PH event TOF normalization to Z axis
1168
+ 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
+
1173
+ return ebin
1174
+
1175
+
1176
+ def determine_ebin_ssd(
1177
+ energy: np.ndarray, tof: np.ndarray, path_length: np.ndarray
1178
+ ) -> NDArray:
1179
+ """
1180
+ Determine the species for SSD events.
1181
+
1182
+ Species is determined from the particle's energy and velocity.
1183
+ For velocity, the particle's TOF is normalized with respect
1184
+ to a fixed distance dmin between the front and back detectors.
1185
+ For SSD events, an adjustment is also made to the path length
1186
+ to account for the shorter distances that such events
1187
+ travel to reach the detector. The normalized TOF is termed
1188
+ the corrected tof (ctof). Particle species are determined from
1189
+ the energy and cTOF using a lookup table.
1190
+
1191
+ Further description is available on pages 42-44 of
1192
+ IMAP-Ultra Flight Software Specification document
1193
+ (7523-9009_Rev_-.pdf).
1194
+
1195
+ Parameters
1196
+ ----------
1197
+ energy : np.ndarray
1198
+ Energy from the SSD event (keV).
1199
+ tof : np.ndarray
1200
+ Time of flight of the SSD event (tenths of a nanosecond).
1201
+ path_length : np.ndarray
1202
+ Path length (r) (hundredths of a millimeter).
1203
+
1204
+ Returns
1205
+ -------
1206
+ bin : np.ndarray
1207
+ Species bin.
1208
+ """
1209
+ # SSD event TOF normalization to Z axis
1210
+ ctof, _ = get_ctof(tof, path_length, type="SSD")
1211
+
1212
+ ebin = np.full(len(ctof), 255, dtype=np.uint8) # placeholder
1213
+
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]
1221
+
1222
+ return ebin