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
@@ -1,21 +1,45 @@
1
1
  """Culls Events for ULTRA L1b."""
2
2
 
3
3
  import logging
4
+ from collections import namedtuple
4
5
 
5
6
  import numpy as np
6
7
  import pandas as pd
7
8
  import xarray as xr
8
9
  from numpy.typing import NDArray
9
10
 
10
- from imap_processing.quality_flags import ImapAttitudeUltraFlags, ImapRatesUltraFlags
11
+ from imap_processing.quality_flags import (
12
+ ImapAttitudeUltraFlags,
13
+ ImapDEScatteringUltraFlags,
14
+ ImapHkUltraFlags,
15
+ ImapInstrumentUltraFlags,
16
+ ImapRatesUltraFlags,
17
+ )
11
18
  from imap_processing.spice.spin import get_spin_data
12
19
  from imap_processing.ultra.constants import UltraConstants
20
+ from imap_processing.ultra.l1b.lookup_utils import (
21
+ get_scattering_coefficients,
22
+ get_scattering_thresholds,
23
+ )
24
+ from imap_processing.ultra.l1b.quality_flag_filters import DE_QUALITY_FLAG_FILTERS
13
25
 
14
26
  logging.basicConfig(level=logging.INFO)
15
27
  logger = logging.getLogger(__name__)
16
28
 
17
29
  SPIN_DURATION = 15 # Default spin duration in seconds.
18
30
 
31
+ RateResult = namedtuple(
32
+ "RateResult",
33
+ [
34
+ "start_per_spin",
35
+ "stop_per_spin",
36
+ "coin_per_spin",
37
+ "start_pulses",
38
+ "stop_pulses",
39
+ "coin_pulses",
40
+ ],
41
+ )
42
+
19
43
 
20
44
  def get_energy_histogram(
21
45
  spin_number: NDArray, energy: NDArray
@@ -106,6 +130,10 @@ def flag_attitude(
106
130
 
107
131
  spin_period = spin_df.loc[spin_df.spin_number.isin(spins), "spin_period_sec"]
108
132
  spin_starttime = spin_df.loc[spin_df.spin_number.isin(spins), "spin_start_met"]
133
+ spin_phase_valid = spin_df.loc[spin_df.spin_number.isin(spins), "spin_phase_valid"]
134
+ spin_period_valid = spin_df.loc[
135
+ spin_df.spin_number.isin(spins), "spin_period_valid"
136
+ ]
109
137
  spin_rates = 60 / spin_period # 60 seconds in a minute
110
138
  bad_spin_rate_indices = (spin_rates < UltraConstants.CULLING_RPM_MIN) | (
111
139
  spin_rates > UltraConstants.CULLING_RPM_MAX
@@ -118,9 +146,59 @@ def flag_attitude(
118
146
  mismatch_indices = compare_aux_univ_spin_table(aux_dataset, spins, spin_df)
119
147
  quality_flags[mismatch_indices] |= ImapAttitudeUltraFlags.AUXMISMATCH.value
120
148
 
149
+ # Spin phase validity flag
150
+ phase_invalid_indices = spin_phase_valid == 0
151
+ quality_flags[phase_invalid_indices] |= ImapAttitudeUltraFlags.SPINPHASE.value
152
+
153
+ # Spin period validity flag
154
+ period_invalid_indices = ~spin_period_valid
155
+ quality_flags[period_invalid_indices] |= ImapAttitudeUltraFlags.SPINPERIOD.value
156
+
121
157
  return quality_flags, spin_rates, spin_period, spin_starttime
122
158
 
123
159
 
160
+ def flag_hk(spin_number: NDArray) -> NDArray:
161
+ """
162
+ Flag data based on hk.
163
+
164
+ Parameters
165
+ ----------
166
+ spin_number : NDArray
167
+ Spin number at each direct event.
168
+
169
+ Returns
170
+ -------
171
+ quality_flags : NDArray
172
+ Quality flags..
173
+ """
174
+ spins = np.unique(spin_number) # Get unique spins
175
+ quality_flags = np.full(spins.shape, ImapHkUltraFlags.NONE.value, dtype=np.uint16)
176
+
177
+ return quality_flags
178
+
179
+
180
+ def flag_imap_instruments(spin_number: NDArray) -> NDArray:
181
+ """
182
+ Flag data based on other IMAP instruments.
183
+
184
+ Parameters
185
+ ----------
186
+ spin_number : NDArray
187
+ Spin number at each direct event.
188
+
189
+ Returns
190
+ -------
191
+ quality_flags : NDArray
192
+ Quality flags..
193
+ """
194
+ spins = np.unique(spin_number) # Get unique spins
195
+ quality_flags = np.full(
196
+ spins.shape, ImapInstrumentUltraFlags.NONE.value, dtype=np.uint16
197
+ )
198
+
199
+ return quality_flags
200
+
201
+
124
202
  def get_n_sigma(count_rates: NDArray, mean_duration: float, sigma: int = 6) -> NDArray:
125
203
  """
126
204
  Calculate the threshold for the HIGHRATES flag.
@@ -140,7 +218,8 @@ def get_n_sigma(count_rates: NDArray, mean_duration: float, sigma: int = 6) -> N
140
218
  threshold : NDArray
141
219
  Threshold for applying HIGHRATES flag.
142
220
  """
143
- sigma_per_energy = np.std(count_rates, axis=1)
221
+ # Take the Sample Standard Deviation.
222
+ sigma_per_energy = np.std(count_rates, axis=1, ddof=1)
144
223
  n_sigma_per_energy = sigma * sigma_per_energy
145
224
  mean_per_energy = np.mean(count_rates, axis=1)
146
225
  # Must have a HIGHRATES threshold of at least 3 counts per spin.
@@ -149,7 +228,7 @@ def get_n_sigma(count_rates: NDArray, mean_duration: float, sigma: int = 6) -> N
149
228
  return threshold
150
229
 
151
230
 
152
- def flag_spin(
231
+ def flag_rates(
153
232
  spin_number: NDArray, energy: NDArray, sigma: int = 6
154
233
  ) -> tuple[NDArray, NDArray, NDArray, NDArray]:
155
234
  """
@@ -182,8 +261,6 @@ def flag_spin(
182
261
  count_rates.shape, ImapRatesUltraFlags.NONE.value, dtype=np.uint16
183
262
  )
184
263
 
185
- # Zero counts/spin/energy level
186
- quality_flags[counts == 0] |= ImapRatesUltraFlags.ZEROCOUNTS.value
187
264
  threshold = get_n_sigma(count_rates, duration, sigma=sigma)
188
265
 
189
266
  bin_edges = np.array(UltraConstants.CULLING_ENERGY_BIN_EDGES)
@@ -194,6 +271,10 @@ def flag_spin(
194
271
  indices_n_sigma = count_rates > threshold[:, np.newaxis]
195
272
  quality_flags[indices_n_sigma] |= ImapRatesUltraFlags.HIGHRATES.value
196
273
 
274
+ # Flags the first and last spin
275
+ quality_flags[:, 0] |= ImapRatesUltraFlags.FIRSTSPIN.value
276
+ quality_flags[:, -1] |= ImapRatesUltraFlags.LASTSPIN.value
277
+
197
278
  return quality_flags, spin, energy_midpoints, threshold
198
279
 
199
280
 
@@ -256,3 +337,273 @@ def compare_aux_univ_spin_table(
256
337
  mismatch_indices[missing_spin_mask] = True
257
338
 
258
339
  return mismatch_indices
340
+
341
+
342
+ # TODO: Make this a common util since it is being used for the de and rates packets.
343
+ def get_spin_and_duration(met: NDArray, spin: NDArray) -> tuple[NDArray, NDArray]:
344
+ """
345
+ Get the spin number and duration.
346
+
347
+ Parameters
348
+ ----------
349
+ met : NDArray
350
+ Mission elapsed time.
351
+ spin : NDArray
352
+ Spin number 0-255.
353
+
354
+ Returns
355
+ -------
356
+ assigned_spin_number : NDArray
357
+ Spin number for packet data product.
358
+ """
359
+ # Packet data.
360
+ # Since the spin number in the direct events packet
361
+ # is only 8 bits it goes from 0-255.
362
+ # Within a pointing that means we will always have duplicate spin numbers.
363
+ # In other words, different spins will be represented by the same spin number.
364
+ # Just to make certain that we won't accidentally combine
365
+ # multiple spins we need to sort by time here.
366
+ sort_idx = np.argsort(met)
367
+ packet_met_sorted = met[sort_idx]
368
+ packet_spin_sorted = spin[sort_idx]
369
+ # Here we are finding the start and end indices of each spin in the sorted array.
370
+ is_new_spin = np.concatenate(
371
+ [[True], packet_spin_sorted.values[1:] != packet_spin_sorted.values[:-1]]
372
+ )
373
+ spin_start_indices = np.where(is_new_spin)[0]
374
+ spin_end_indices = np.append(spin_start_indices[1:], len(packet_met_sorted))
375
+
376
+ # Universal Spin Table.
377
+ spin_df = get_spin_data()
378
+ # Retrieve the met values of the start of the spin.
379
+ spin_start_mets = spin_df["spin_start_met"].values
380
+ # Retrieve the corresponding spin numbers.
381
+ spin_numbers = spin_df["spin_number"].values
382
+ spin_period_sec = spin_df["spin_period_sec"].values
383
+ assigned_spin_number_sorted = np.empty(packet_spin_sorted.shape, dtype=np.uint32)
384
+ assigned_spin_duration_sorted = np.empty(packet_spin_sorted.shape, dtype=np.float32)
385
+ # These last 8 bits are the same as the spin number in the DE packet.
386
+ # So this will give us choices of which spins are
387
+ # available to assign to the packet data.
388
+ possible_spins = spin_numbers & 0xFF
389
+
390
+ # Assign each group based on time.
391
+ for start, end in zip(spin_start_indices, spin_end_indices, strict=False):
392
+ # Now that we have the possible spins from the Universal Spin Table,
393
+ # we match the times of those spins to the nearest times in the DE data.
394
+ possible_times = spin_start_mets[
395
+ possible_spins == packet_spin_sorted.values[start]
396
+ ]
397
+ # Get nearest time for matching spins.
398
+ nearest_idx = np.abs(possible_times - packet_met_sorted.values[start]).argmin()
399
+ nearest_value = possible_times[nearest_idx]
400
+ assigned_spin_number_sorted[start:end] = spin_numbers[
401
+ spin_start_mets == nearest_value
402
+ ]
403
+ assigned_spin_duration_sorted[start:end] = spin_period_sec[
404
+ spin_start_mets == nearest_value
405
+ ]
406
+
407
+ # Undo the sort to match original order.
408
+ assigned_spin_number = np.empty_like(assigned_spin_number_sorted)
409
+ assigned_spin_number[sort_idx] = assigned_spin_number_sorted
410
+ assigned_duration = np.empty_like(assigned_spin_duration_sorted)
411
+ assigned_duration[sort_idx] = assigned_spin_duration_sorted
412
+
413
+ return assigned_spin_number, assigned_duration
414
+
415
+
416
+ def get_pulses_per_spin(rates: xr.Dataset) -> RateResult:
417
+ """
418
+ Get the total number of pulses per spin.
419
+
420
+ Parameters
421
+ ----------
422
+ rates : xr.Dataset
423
+ Rates dataset.
424
+
425
+ Returns
426
+ -------
427
+ start_per_spin : NDArray
428
+ Total start pulses per spin.
429
+ stop_per_spin : NDArray
430
+ Total stop pulses per spin.
431
+ coin_per_spin : NDArray
432
+ Total coincidence pulses per spin.
433
+ start_pulses : NDArray
434
+ Total start pulses.
435
+ stop_pulses : NDArray
436
+ Total stop pulses.
437
+ coin_pulses : NDArray
438
+ Total coincidence pulses.
439
+ """
440
+ spin_number, duration = get_spin_and_duration(rates["shcoarse"], rates["spin"])
441
+
442
+ # Top coin pulses
443
+ top_coin_pulses = np.stack(
444
+ [v for k, v in rates.items() if k.startswith("coin_t")], axis=1
445
+ )
446
+ max_top_coin_pulse = np.max(top_coin_pulses, axis=1)
447
+
448
+ # Bottom coin pulses
449
+ bottom_coin_pulses = np.stack(
450
+ [v for k, v in rates.items() if k.startswith("coin_b")], axis=1
451
+ )
452
+ max_bottom_coin_pulse = np.max(bottom_coin_pulses, axis=1)
453
+
454
+ # Top stop pulses
455
+ top_stop_pulses = np.stack(
456
+ [v for k, v in rates.items() if k.startswith("stop_t")], axis=1
457
+ )
458
+ max_top_stop_pulse = np.max(top_stop_pulses, axis=1)
459
+
460
+ # Bottom stop pulses
461
+ bottom_stop_pulses = np.stack(
462
+ [v for k, v in rates.items() if k.startswith("stop_b")], axis=1
463
+ )
464
+ max_bottom_stop_pulse = np.max(bottom_stop_pulses, axis=1)
465
+
466
+ stop_pulses = max_top_stop_pulse + max_bottom_stop_pulse
467
+ start_pulses = rates["start_rf"] + rates["start_lf"]
468
+ coin_pulses = max_top_coin_pulse + max_bottom_coin_pulse
469
+
470
+ unique_spins, spin_idx = np.unique(spin_number, return_inverse=True)
471
+
472
+ start_per_spin = np.bincount(spin_idx, weights=start_pulses)
473
+ stop_per_spin = np.bincount(spin_idx, weights=stop_pulses)
474
+ coin_per_spin = np.bincount(spin_idx, weights=coin_pulses)
475
+
476
+ return RateResult(
477
+ start_per_spin=start_per_spin,
478
+ stop_per_spin=stop_per_spin,
479
+ coin_per_spin=coin_per_spin,
480
+ start_pulses=start_pulses,
481
+ stop_pulses=stop_pulses,
482
+ coin_pulses=coin_pulses,
483
+ )
484
+
485
+
486
+ def flag_scattering(
487
+ tof_energy: NDArray,
488
+ theta: NDArray,
489
+ phi: NDArray,
490
+ ancillary_files: dict,
491
+ sensor: str,
492
+ quality_flags: NDArray,
493
+ ) -> None:
494
+ """
495
+ Flag events where either theta or phi FWHM exceed the threshold or equal nan.
496
+
497
+ Parameters
498
+ ----------
499
+ tof_energy : NDArray
500
+ TOF energy for each event in keV.
501
+ theta : NDArray
502
+ Elevation angles in degrees.
503
+ phi : NDArray
504
+ Azimuth angles in degrees.
505
+ ancillary_files : dict[Path]
506
+ Ancillary files.
507
+ sensor : str
508
+ Sensor name: "ultra45" or "ultra90".
509
+ quality_flags : NDArray
510
+ Quality flags.
511
+ """
512
+ scattering_thresholds = get_scattering_thresholds(ancillary_files)
513
+
514
+ for (e_min, e_max), threshold in scattering_thresholds.items():
515
+ event_mask = (tof_energy >= e_min) & (tof_energy < e_max)
516
+ # Input the theta and phi values for the current energy range.
517
+ # Returns a_theta_val, g_theta_val, a_phi_val, g_phi_val
518
+ theta_coeffs, phi_coeffs = get_scattering_coefficients(
519
+ theta[event_mask],
520
+ phi[event_mask],
521
+ lookup_tables=None,
522
+ ancillary_files=ancillary_files,
523
+ instrument_id=int(sensor[-2:]),
524
+ )
525
+ # FWHM_PHI = A_PHI * E^G_PHI
526
+ # FWHM_THETA = A_THETA * E^G_THETA
527
+ fwhm_theta = theta_coeffs[:, 0] * tof_energy[event_mask] ** theta_coeffs[:, 1]
528
+ fwhm_phi = phi_coeffs[:, 0] * tof_energy[event_mask] ** phi_coeffs[:, 1]
529
+ is_nan = np.isnan(fwhm_theta) | np.isnan(fwhm_phi)
530
+ quality_flags[np.where(event_mask)[0][is_nan]] |= (
531
+ ImapDEScatteringUltraFlags.NAN_PHI_OR_THETA.value
532
+ )
533
+
534
+ theta_exceeds = fwhm_theta > threshold
535
+ phi_exceeds = fwhm_phi > threshold
536
+ either_exceeds = theta_exceeds | phi_exceeds
537
+
538
+ # Set flags for events where either theta or phi FWHM exceed the threshold
539
+ quality_flags[np.where(event_mask)[0][either_exceeds]] |= (
540
+ ImapDEScatteringUltraFlags.ABOVE_THRESHOLD.value
541
+ )
542
+
543
+
544
+ def get_de_rejection_mask(
545
+ quality_scattering: NDArray, quality_outliers: NDArray
546
+ ) -> NDArray:
547
+ """
548
+ Create boolean mask where event is rejected due to relevant flags.
549
+
550
+ Parameters
551
+ ----------
552
+ quality_scattering : NDArray
553
+ Quality scattering flags.
554
+ quality_outliers : NDArray
555
+ Quality outliers flags.
556
+
557
+ Returns
558
+ -------
559
+ rejected : NDArray
560
+ Rejected events where True = rejected.
561
+ """
562
+ # Bitmasks from the DE_QUALITY_FLAG_FILTERS
563
+ scattering_mask = sum(
564
+ flag.value for flag in DE_QUALITY_FLAG_FILTERS["quality_scattering"]
565
+ )
566
+ outliers_mask = sum(
567
+ flag.value for flag in DE_QUALITY_FLAG_FILTERS["quality_outliers"]
568
+ )
569
+
570
+ # Boolean mask where event is rejected due to relevant flags
571
+ rejected = ((quality_scattering & scattering_mask) != 0) | (
572
+ (quality_outliers & outliers_mask) != 0
573
+ )
574
+
575
+ return rejected
576
+
577
+
578
+ def count_rejected_events_per_spin(
579
+ spins: NDArray, quality_scattering: NDArray, quality_outliers: NDArray
580
+ ) -> NDArray:
581
+ """
582
+ Count rejected events per spin based on DE_QUALITY_FLAG_FILTERS.
583
+
584
+ Parameters
585
+ ----------
586
+ spins : NDArray
587
+ Spins in which each direct event is within.
588
+ quality_scattering : NDArray
589
+ Quality scattering flags.
590
+ quality_outliers : NDArray
591
+ Quality outliers flags.
592
+
593
+ Returns
594
+ -------
595
+ rejected_counts : NDArray
596
+ Rejected counts per spin.
597
+ """
598
+ # Boolean mask where event is rejected due to relevant flags
599
+ rejected = get_de_rejection_mask(quality_scattering, quality_outliers)
600
+
601
+ # Unique spin numbers
602
+ unique_spins = np.unique(spins)
603
+
604
+ # Count rejected events per spin
605
+ rejected_counts = np.array(
606
+ [np.count_nonzero(rejected[spins == spin]) for spin in unique_spins], dtype=int
607
+ )
608
+
609
+ return rejected_counts