imap-processing 0.19.0__py3-none-any.whl → 0.19.3__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 (73) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -0
  3. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +31 -894
  4. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +279 -255
  5. imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +55 -0
  6. imap_processing/cdf/config/imap_enamaps_l2-healpix_variable_attrs.yaml +29 -0
  7. imap_processing/cdf/config/imap_enamaps_l2-rectangular_variable_attrs.yaml +32 -0
  8. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +3 -1
  9. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +5 -4
  10. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +28 -16
  11. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +33 -31
  12. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +61 -1
  13. imap_processing/cli.py +62 -71
  14. imap_processing/codice/codice_l0.py +2 -1
  15. imap_processing/codice/codice_l1a.py +47 -49
  16. imap_processing/codice/codice_l1b.py +42 -32
  17. imap_processing/codice/codice_l2.py +105 -7
  18. imap_processing/codice/constants.py +50 -8
  19. imap_processing/codice/data/lo_stepping_values.csv +1 -1
  20. imap_processing/ena_maps/ena_maps.py +39 -18
  21. imap_processing/ena_maps/utils/corrections.py +291 -0
  22. imap_processing/ena_maps/utils/map_utils.py +20 -4
  23. imap_processing/glows/l1b/glows_l1b.py +38 -23
  24. imap_processing/glows/l1b/glows_l1b_data.py +10 -11
  25. imap_processing/hi/hi_l1c.py +4 -109
  26. imap_processing/hi/hi_l2.py +34 -23
  27. imap_processing/hi/utils.py +109 -0
  28. imap_processing/ialirt/l0/ialirt_spice.py +1 -1
  29. imap_processing/ialirt/l0/parse_mag.py +18 -4
  30. imap_processing/ialirt/l0/process_hit.py +9 -4
  31. imap_processing/ialirt/l0/process_swapi.py +9 -4
  32. imap_processing/ialirt/l0/process_swe.py +9 -4
  33. imap_processing/ialirt/utils/create_xarray.py +1 -1
  34. imap_processing/lo/ancillary_data/imap_lo_hydrogen-geometric-factor_v001.csv +75 -0
  35. imap_processing/lo/ancillary_data/imap_lo_oxygen-geometric-factor_v001.csv +75 -0
  36. imap_processing/lo/l1b/lo_l1b.py +90 -16
  37. imap_processing/lo/l1c/lo_l1c.py +164 -50
  38. imap_processing/lo/l2/lo_l2.py +941 -127
  39. imap_processing/mag/l1d/mag_l1d_data.py +36 -3
  40. imap_processing/mag/l2/mag_l2.py +2 -0
  41. imap_processing/mag/l2/mag_l2_data.py +4 -3
  42. imap_processing/quality_flags.py +14 -0
  43. imap_processing/spice/geometry.py +13 -8
  44. imap_processing/spice/pointing_frame.py +4 -2
  45. imap_processing/spice/repoint.py +49 -0
  46. imap_processing/ultra/constants.py +29 -0
  47. imap_processing/ultra/l0/decom_tools.py +58 -46
  48. imap_processing/ultra/l0/decom_ultra.py +21 -9
  49. imap_processing/ultra/l0/ultra_utils.py +4 -4
  50. imap_processing/ultra/l1b/badtimes.py +35 -11
  51. imap_processing/ultra/l1b/de.py +15 -9
  52. imap_processing/ultra/l1b/extendedspin.py +24 -12
  53. imap_processing/ultra/l1b/goodtimes.py +112 -0
  54. imap_processing/ultra/l1b/lookup_utils.py +1 -1
  55. imap_processing/ultra/l1b/ultra_l1b.py +7 -7
  56. imap_processing/ultra/l1b/ultra_l1b_culling.py +8 -4
  57. imap_processing/ultra/l1b/ultra_l1b_extended.py +79 -43
  58. imap_processing/ultra/l1c/helio_pset.py +68 -39
  59. imap_processing/ultra/l1c/l1c_lookup_utils.py +45 -12
  60. imap_processing/ultra/l1c/spacecraft_pset.py +81 -37
  61. imap_processing/ultra/l1c/ultra_l1c.py +27 -22
  62. imap_processing/ultra/l1c/ultra_l1c_culling.py +7 -0
  63. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +41 -41
  64. imap_processing/ultra/l2/ultra_l2.py +75 -18
  65. imap_processing/ultra/utils/ultra_l1_utils.py +10 -5
  66. {imap_processing-0.19.0.dist-info → imap_processing-0.19.3.dist-info}/METADATA +2 -2
  67. {imap_processing-0.19.0.dist-info → imap_processing-0.19.3.dist-info}/RECORD +71 -69
  68. imap_processing/ultra/l1b/cullingmask.py +0 -90
  69. imap_processing/ultra/l1c/histogram.py +0 -36
  70. /imap_processing/glows/ancillary/{imap_glows_pipeline_settings_20250923_v002.json → imap_glows_pipeline-settings_20250923_v002.json} +0 -0
  71. {imap_processing-0.19.0.dist-info → imap_processing-0.19.3.dist-info}/LICENSE +0 -0
  72. {imap_processing-0.19.0.dist-info → imap_processing-0.19.3.dist-info}/WHEEL +0 -0
  73. {imap_processing-0.19.0.dist-info → imap_processing-0.19.3.dist-info}/entry_points.txt +0 -0
@@ -42,6 +42,7 @@ from imap_processing.ultra.l1b.ultra_l1b_extended import (
42
42
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
43
43
 
44
44
  FILLVAL_UINT8 = 255
45
+ FILLVAL_UINT32 = 4294967295
45
46
  FILLVAL_FLOAT32 = -1.0e31
46
47
 
47
48
 
@@ -82,7 +83,6 @@ def calculate_de(
82
83
  "event_type",
83
84
  "de_event_met",
84
85
  "phase_angle",
85
- "spin",
86
86
  ]
87
87
  dataset_keys = [
88
88
  "coin_type",
@@ -90,7 +90,6 @@ def calculate_de(
90
90
  "stop_type",
91
91
  "shcoarse",
92
92
  "phase_angle",
93
- "spin",
94
93
  ]
95
94
 
96
95
  de_dict.update(
@@ -127,6 +126,7 @@ def calculate_de(
127
126
  magnitude_v = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
128
127
  energy = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
129
128
  e_bin = np.full(len(de_dataset["epoch"]), FILLVAL_UINT8, dtype=np.uint8)
129
+ e_bin_l1a = np.full(len(de_dataset["epoch"]), FILLVAL_UINT8, dtype=np.uint8)
130
130
  species_bin = np.full(len(de_dataset["epoch"]), FILLVAL_UINT8, dtype=np.uint8)
131
131
  t2 = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
132
132
  event_times = np.full(len(de_dataset["epoch"]), FILLVAL_FLOAT32, dtype=np.float32)
@@ -143,6 +143,7 @@ def calculate_de(
143
143
  quality_flags = np.full(
144
144
  de_dataset["epoch"].shape, ImapDEOutliersUltraFlags.NONE.value, dtype=np.uint16
145
145
  )
146
+
146
147
  scattering_quality_flags = np.full(
147
148
  de_dataset["epoch"].shape,
148
149
  ImapDEScatteringUltraFlags.NONE.value,
@@ -196,7 +197,6 @@ def calculate_de(
196
197
  (xb[ph_indices], yb[ph_indices]),
197
198
  d[ph_indices],
198
199
  )
199
- species_bin[ph_indices] = determine_species(tof[ph_indices], r[ph_indices], "PH")
200
200
  etof[ph_indices], xc[ph_indices] = get_coincidence_positions(
201
201
  de_dataset.isel(epoch=ph_indices),
202
202
  t2[ph_indices],
@@ -213,8 +213,13 @@ def calculate_de(
213
213
  etof[ph_indices],
214
214
  xc[ph_indices],
215
215
  xb[ph_indices],
216
+ de_dataset["stop_north_tdc"][ph_indices].values,
217
+ de_dataset["stop_south_tdc"][ph_indices].values,
218
+ de_dataset["stop_east_tdc"][ph_indices].values,
219
+ de_dataset["stop_west_tdc"][ph_indices].values,
216
220
  f"ultra{sensor}",
217
221
  ancillary_files,
222
+ quality_flags[ph_indices],
218
223
  )
219
224
  e_bin[ph_indices] = determine_ebin_pulse_height(
220
225
  energy[ph_indices],
@@ -224,6 +229,7 @@ def calculate_de(
224
229
  coinphvalid,
225
230
  ancillary_files,
226
231
  )
232
+ species_bin[ph_indices] = determine_species(e_bin[ph_indices], "PH")
227
233
  ctof[ph_indices], magnitude_v[ph_indices] = get_ctof(
228
234
  tof[ph_indices], r[ph_indices], "PH"
229
235
  )
@@ -257,9 +263,7 @@ def calculate_de(
257
263
  f"ultra{sensor}",
258
264
  ancillary_files,
259
265
  )
260
- species_bin[ssd_indices] = determine_species(
261
- tof[ssd_indices], r[ssd_indices], "SSD"
262
- )
266
+ species_bin[ssd_indices] = determine_species(e_bin[ssd_indices], "SSD")
263
267
  ctof[ssd_indices], magnitude_v[ssd_indices] = get_ctof(
264
268
  tof[ssd_indices], r[ssd_indices], "SSD"
265
269
  )
@@ -289,7 +293,6 @@ def calculate_de(
289
293
  de_dict["tof_start_stop"][valid_indices],
290
294
  )
291
295
  )
292
- de_dict["direct_event_velocity"] = velocities.astype(np.float32)
293
296
  de_dict["direct_event_unit_velocity"] = v_hat.astype(np.float32)
294
297
  de_dict["direct_event_unit_position"] = r_hat.astype(np.float32)
295
298
 
@@ -298,7 +301,10 @@ def calculate_de(
298
301
  )
299
302
  de_dict["tof_energy"] = tof_energy
300
303
  de_dict["energy"] = energy
301
- de_dict["ebin"] = e_bin
304
+ de_dict["computed_ebin"] = e_bin
305
+ valid_ebin = de_dataset["bin"].values != FILLVAL_UINT32
306
+ e_bin_l1a[valid_ebin] = de_dataset["bin"].values[valid_ebin]
307
+ de_dict["ebin"] = e_bin_l1a
302
308
  de_dict["species"] = species_bin
303
309
 
304
310
  # Annotated Events.
@@ -313,7 +319,7 @@ def calculate_de(
313
319
  helio_velocity[valid_events],
314
320
  ) = get_annotated_particle_velocity(
315
321
  event_times[valid_events],
316
- de_dict["direct_event_velocity"][valid_events],
322
+ velocities.astype(np.float32)[valid_events],
317
323
  ultra_frame,
318
324
  SpiceFrame.IMAP_DPS,
319
325
  SpiceFrame.IMAP_SPACECRAFT,
@@ -2,6 +2,7 @@
2
2
 
3
3
  import numpy as np
4
4
  import xarray as xr
5
+ from numpy.typing import NDArray
5
6
 
6
7
  from imap_processing.ultra.l1b.ultra_l1b_culling import (
7
8
  count_rejected_events_per_spin,
@@ -15,6 +16,7 @@ from imap_processing.ultra.l1b.ultra_l1b_culling import (
15
16
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
16
17
 
17
18
  FILLVAL_UINT16 = 65535
19
+ FILLVAL_FLOAT32 = -1.0e31
18
20
 
19
21
 
20
22
  def calculate_extendedspin(
@@ -44,7 +46,7 @@ def calculate_extendedspin(
44
46
  de_dataset = dict_datasets[f"imap_ultra_l1b_{instrument_id}sensor-de"]
45
47
 
46
48
  extendedspin_dict = {}
47
- rates_qf, spin, energy_midpoints, n_sigma_per_energy = flag_rates(
49
+ rates_qf, spin, energy_bin_geometric_mean, n_sigma_per_energy = flag_rates(
48
50
  de_dataset["spin"].values,
49
51
  de_dataset["energy"].values,
50
52
  )
@@ -58,12 +60,6 @@ def calculate_extendedspin(
58
60
  hk_qf = flag_hk(de_dataset["spin"].values)
59
61
  inst_qf = flag_imap_instruments(de_dataset["spin"].values)
60
62
 
61
- # Get the first epoch for each spin.
62
- mask = xr.DataArray(np.isin(de_dataset["spin"], spin), dims="epoch")
63
- filtered_dataset = de_dataset.where(mask, drop=True)
64
- _, first_indices = np.unique(filtered_dataset["spin"].values, return_index=True)
65
- first_epochs = filtered_dataset["epoch"].values[first_indices]
66
-
67
63
  # Get the number of pulses per spin.
68
64
  pulses = get_pulses_per_spin(rates_dataset)
69
65
 
@@ -75,18 +71,34 @@ def calculate_extendedspin(
75
71
  de_dataset["quality_outliers"].values,
76
72
  )
77
73
  # These will be the coordinates.
78
- extendedspin_dict["epoch"] = first_epochs
79
74
  extendedspin_dict["spin_number"] = spin
80
- extendedspin_dict["energy_bin_geometric_mean"] = energy_midpoints
75
+ extendedspin_dict["energy_bin_geometric_mean"] = energy_bin_geometric_mean
81
76
 
82
77
  extendedspin_dict["ena_rates"] = count_rates
83
78
  extendedspin_dict["ena_rates_threshold"] = n_sigma_per_energy
84
79
  extendedspin_dict["spin_start_time"] = spin_starttime
85
80
  extendedspin_dict["spin_period"] = spin_period
86
81
  extendedspin_dict["spin_rate"] = spin_rates
87
- extendedspin_dict["start_pulses_per_spin"] = pulses.start_per_spin
88
- extendedspin_dict["stop_pulses_per_spin"] = pulses.stop_per_spin
89
- extendedspin_dict["coin_pulses_per_spin"] = pulses.coin_per_spin
82
+
83
+ # Get index of pulses.unique_spins corresponding to each spin.
84
+ idx: NDArray[np.intp] = np.searchsorted(pulses.unique_spins, spin)
85
+
86
+ # Validate that the spin values match
87
+ valid = (idx < pulses.unique_spins.size) & (pulses.unique_spins[idx] == spin)
88
+
89
+ start_per_spin = np.full(len(spin), FILLVAL_FLOAT32, dtype=np.float32)
90
+ stop_per_spin = np.full(len(spin), FILLVAL_FLOAT32, dtype=np.float32)
91
+ coin_per_spin = np.full(len(spin), FILLVAL_FLOAT32, dtype=np.float32)
92
+
93
+ # Fill only the valid ones
94
+ start_per_spin[valid] = pulses.start_per_spin[idx[valid]]
95
+ stop_per_spin[valid] = pulses.stop_per_spin[idx[valid]]
96
+ coin_per_spin[valid] = pulses.coin_per_spin[idx[valid]]
97
+
98
+ # account for rates spins which are not in the direct event spins
99
+ extendedspin_dict["start_pulses_per_spin"] = start_per_spin
100
+ extendedspin_dict["stop_pulses_per_spin"] = stop_per_spin
101
+ extendedspin_dict["coin_pulses_per_spin"] = coin_per_spin
90
102
  extendedspin_dict["rejected_events_per_spin"] = rejected_counts
91
103
  extendedspin_dict["quality_attitude"] = attitude_qf
92
104
  extendedspin_dict["quality_ena_rates"] = rates_qf
@@ -0,0 +1,112 @@
1
+ """Calculate Goodtimes."""
2
+
3
+ import numpy as np
4
+ import xarray as xr
5
+
6
+ from imap_processing.ultra.l1b.quality_flag_filters import SPIN_QUALITY_FLAG_FILTERS
7
+ from imap_processing.ultra.utils.ultra_l1_utils import create_dataset, extract_data_dict
8
+
9
+ FILLVAL_UINT16 = 65535
10
+ FILLVAL_FLOAT32 = -1.0e31
11
+ FILLVAL_FLOAT64 = -1.0e31
12
+ FILLVAL_UINT32 = 4294967295
13
+
14
+
15
+ def calculate_goodtimes(extendedspin_dataset: xr.Dataset, name: str) -> xr.Dataset:
16
+ """
17
+ Create dataset with defined datatype for Goodtimes Data.
18
+
19
+ Parameters
20
+ ----------
21
+ extendedspin_dataset : xarray.Dataset
22
+ Dataset containing the data.
23
+ name : str
24
+ Name of the dataset.
25
+
26
+ Returns
27
+ -------
28
+ goodtimes_dataset : xarray.Dataset
29
+ Dataset containing the extendedspin data that remains after culling.
30
+ """
31
+ n_bins = extendedspin_dataset.dims["energy_bin_geometric_mean"]
32
+ # If the spin rate was too high or low then the spin should be thrown out.
33
+ # If the rates at any energy level are too high then throw out the entire spin.
34
+ good_mask = (
35
+ (
36
+ extendedspin_dataset["quality_attitude"]
37
+ & sum(flag.value for flag in SPIN_QUALITY_FLAG_FILTERS["quality_attitude"])
38
+ )
39
+ == 0
40
+ ) & (
41
+ (
42
+ (
43
+ extendedspin_dataset["quality_ena_rates"]
44
+ & sum(
45
+ flag.value
46
+ for flag in SPIN_QUALITY_FLAG_FILTERS["quality_ena_rates"]
47
+ )
48
+ )
49
+ == 0
50
+ ).all(dim="energy_bin_geometric_mean")
51
+ )
52
+ filtered_dataset = extendedspin_dataset.sel(
53
+ spin_number=extendedspin_dataset["spin_number"][good_mask]
54
+ )
55
+
56
+ data_dict = extract_data_dict(filtered_dataset)
57
+
58
+ goodtimes_dataset = create_dataset(data_dict, name, "l1b")
59
+
60
+ if goodtimes_dataset["spin_number"].size == 0:
61
+ goodtimes_dataset = goodtimes_dataset.drop_dims("spin_number")
62
+ goodtimes_dataset = goodtimes_dataset.expand_dims(spin_number=[FILLVAL_UINT32])
63
+ goodtimes_dataset["spin_start_time"] = xr.DataArray(
64
+ np.array([FILLVAL_FLOAT64], dtype="float64"), dims=["spin_number"]
65
+ )
66
+ goodtimes_dataset["spin_period"] = xr.DataArray(
67
+ np.array([FILLVAL_FLOAT64], dtype="float64"), dims=["spin_number"]
68
+ )
69
+ goodtimes_dataset["spin_rate"] = xr.DataArray(
70
+ np.array([FILLVAL_FLOAT64], dtype="float64"), dims=["spin_number"]
71
+ )
72
+ goodtimes_dataset["start_pulses_per_spin"] = xr.DataArray(
73
+ np.array([FILLVAL_FLOAT32], dtype="float32"),
74
+ dims=["spin_number"],
75
+ )
76
+ goodtimes_dataset["stop_pulses_per_spin"] = xr.DataArray(
77
+ np.array([FILLVAL_FLOAT32], dtype="float32"),
78
+ dims=["spin_number"],
79
+ )
80
+ goodtimes_dataset["coin_pulses_per_spin"] = xr.DataArray(
81
+ np.array([FILLVAL_FLOAT32], dtype="float32"),
82
+ dims=["spin_number"],
83
+ )
84
+ goodtimes_dataset["rejected_events_per_spin"] = xr.DataArray(
85
+ np.array([FILLVAL_UINT32], dtype="uint32"),
86
+ dims=["spin_number"],
87
+ )
88
+ goodtimes_dataset["quality_attitude"] = xr.DataArray(
89
+ np.array([FILLVAL_UINT16], dtype="uint16"), dims=["spin_number"]
90
+ )
91
+ goodtimes_dataset["quality_hk"] = xr.DataArray(
92
+ np.array([FILLVAL_UINT16], dtype="uint16"),
93
+ dims=["spin_number"],
94
+ )
95
+ goodtimes_dataset["quality_instruments"] = xr.DataArray(
96
+ np.array([FILLVAL_UINT16], dtype="uint16"),
97
+ dims=["spin_number"],
98
+ )
99
+ goodtimes_dataset["quality_ena_rates"] = (
100
+ ("energy_bin_geometric_mean", "spin_number"),
101
+ np.full((n_bins, 1), FILLVAL_UINT16, dtype="uint16"),
102
+ )
103
+ goodtimes_dataset["ena_rates"] = (
104
+ ("energy_bin_geometric_mean", "spin_number"),
105
+ np.full((n_bins, 1), FILLVAL_FLOAT64, dtype="float64"),
106
+ )
107
+ goodtimes_dataset["ena_rates_threshold"] = (
108
+ ("energy_bin_geometric_mean", "spin_number"),
109
+ np.full((n_bins, 1), FILLVAL_FLOAT32, dtype="float32"),
110
+ )
111
+
112
+ return goodtimes_dataset
@@ -345,7 +345,7 @@ def load_scattering_lookup_tables(ancillary_files: dict, instrument_id: int) ->
345
345
  # TODO remove the line below when the 45 sensor scattering coefficients are
346
346
  # delivered.
347
347
  instrument_id = 90
348
- descriptor = f"l1b-{instrument_id}sensor-scattering-calibration"
348
+ descriptor = f"l1b-{instrument_id}sensor-scattering-calibration-data"
349
349
  theta_grid = pd.read_csv(
350
350
  ancillary_files[descriptor], header=None, skiprows=7, nrows=241
351
351
  ).to_numpy(dtype=float)
@@ -3,9 +3,9 @@
3
3
  import xarray as xr
4
4
 
5
5
  from imap_processing.ultra.l1b.badtimes import calculate_badtimes
6
- from imap_processing.ultra.l1b.cullingmask import calculate_cullingmask
7
6
  from imap_processing.ultra.l1b.de import calculate_de
8
7
  from imap_processing.ultra.l1b.extendedspin import calculate_extendedspin
8
+ from imap_processing.ultra.l1b.goodtimes import calculate_goodtimes
9
9
 
10
10
 
11
11
  def ultra_l1b(data_dict: dict, ancillary_files: dict) -> list[xr.Dataset]:
@@ -29,7 +29,7 @@ def ultra_l1b(data_dict: dict, ancillary_files: dict) -> list[xr.Dataset]:
29
29
  General flow:
30
30
  1. l1a data products are created (upstream to this code)
31
31
  2. l1b de is created here and dropped in s3 kicking off processing again
32
- 3. l1b extended, culling, badtimes created here
32
+ 3. l1b extended, goodtimes, badtimes created here
33
33
  """
34
34
  output_datasets = []
35
35
 
@@ -72,22 +72,22 @@ def ultra_l1b(data_dict: dict, ancillary_files: dict) -> list[xr.Dataset]:
72
72
  output_datasets.append(extendedspin_dataset)
73
73
  elif (
74
74
  f"imap_ultra_l1b_{instrument_id}sensor-extendedspin" in data_dict
75
- and f"imap_ultra_l1b_{instrument_id}sensor-cullingmask" in data_dict
75
+ and f"imap_ultra_l1b_{instrument_id}sensor-goodtimes" in data_dict
76
76
  ):
77
77
  badtimes_dataset = calculate_badtimes(
78
78
  data_dict[f"imap_ultra_l1b_{instrument_id}sensor-extendedspin"],
79
- data_dict[f"imap_ultra_l1b_{instrument_id}sensor-cullingmask"][
79
+ data_dict[f"imap_ultra_l1b_{instrument_id}sensor-goodtimes"][
80
80
  "spin_number"
81
81
  ].values,
82
82
  f"imap_ultra_l1b_{instrument_id}sensor-badtimes",
83
83
  )
84
84
  output_datasets.append(badtimes_dataset)
85
85
  elif f"imap_ultra_l1b_{instrument_id}sensor-extendedspin" in data_dict:
86
- cullingmask_dataset = calculate_cullingmask(
86
+ goodtimes_dataset = calculate_goodtimes(
87
87
  data_dict[f"imap_ultra_l1b_{instrument_id}sensor-extendedspin"],
88
- f"imap_ultra_l1b_{instrument_id}sensor-cullingmask",
88
+ f"imap_ultra_l1b_{instrument_id}sensor-goodtimes",
89
89
  )
90
- output_datasets.append(cullingmask_dataset)
90
+ output_datasets.append(goodtimes_dataset)
91
91
  if not output_datasets:
92
92
  raise ValueError("Data dictionary does not contain the expected keys.")
93
93
 
@@ -31,6 +31,7 @@ SPIN_DURATION = 15 # Default spin duration in seconds.
31
31
  RateResult = namedtuple(
32
32
  "RateResult",
33
33
  [
34
+ "unique_spins",
34
35
  "start_per_spin",
35
36
  "stop_per_spin",
36
37
  "coin_per_spin",
@@ -249,8 +250,8 @@ def flag_rates(
249
250
  Quality flags.
250
251
  spin : NDArray
251
252
  Spin data.
252
- energy_midpoints : NDArray
253
- Energy midpoint data.
253
+ energy_bin_geometric_mean : NDArray
254
+ Energy bin geometric mean.
254
255
  n_sigma_per_energy_reshape : NDArray
255
256
  N sigma per energy.
256
257
  """
@@ -264,7 +265,7 @@ def flag_rates(
264
265
  threshold = get_n_sigma(count_rates, duration, sigma=sigma)
265
266
 
266
267
  bin_edges = np.array(UltraConstants.CULLING_ENERGY_BIN_EDGES)
267
- energy_midpoints = np.sqrt(bin_edges[:-1] * bin_edges[1:])
268
+ energy_bin_geometric_mean = np.sqrt(bin_edges[:-1] * bin_edges[1:])
268
269
  spin = np.unique(spin_number)
269
270
 
270
271
  # Indices where the counts exceed the threshold
@@ -275,7 +276,7 @@ def flag_rates(
275
276
  quality_flags[:, 0] |= ImapRatesUltraFlags.FIRSTSPIN.value
276
277
  quality_flags[:, -1] |= ImapRatesUltraFlags.LASTSPIN.value
277
278
 
278
- return quality_flags, spin, energy_midpoints, threshold
279
+ return quality_flags, spin, energy_bin_geometric_mean, threshold
279
280
 
280
281
 
281
282
  def compare_aux_univ_spin_table(
@@ -424,6 +425,8 @@ def get_pulses_per_spin(rates: xr.Dataset) -> RateResult:
424
425
 
425
426
  Returns
426
427
  -------
428
+ unique_spins : NDArray
429
+ Unique spin numbers.
427
430
  start_per_spin : NDArray
428
431
  Total start pulses per spin.
429
432
  stop_per_spin : NDArray
@@ -474,6 +477,7 @@ def get_pulses_per_spin(rates: xr.Dataset) -> RateResult:
474
477
  coin_per_spin = np.bincount(spin_idx, weights=coin_pulses)
475
478
 
476
479
  return RateResult(
480
+ unique_spins=unique_spins,
477
481
  start_per_spin=start_per_spin,
478
482
  stop_per_spin=stop_per_spin,
479
483
  coin_per_spin=coin_per_spin,
@@ -12,7 +12,9 @@ from numpy import ndarray
12
12
  from numpy.typing import NDArray
13
13
  from scipy.interpolate import LinearNDInterpolator, RegularGridInterpolator
14
14
 
15
+ from imap_processing.quality_flags import ImapDEOutliersUltraFlags
15
16
  from imap_processing.spice.spin import get_spin_data
17
+ from imap_processing.spice.time import sct_to_et
16
18
  from imap_processing.ultra.constants import UltraConstants
17
19
  from imap_processing.ultra.l1b.lookup_utils import (
18
20
  get_angular_profiles,
@@ -552,7 +554,7 @@ def get_de_velocity(
552
554
  v_hat = velocities / np.linalg.norm(velocities, axis=1)[:, None]
553
555
  r_hat = -v_hat
554
556
 
555
- return velocities, v_hat, r_hat
557
+ return velocities, -v_hat, -r_hat
556
558
 
557
559
 
558
560
  def get_ssd_tof(
@@ -837,25 +839,16 @@ def get_ctof(
837
839
  return ctof, magnitude_v
838
840
 
839
841
 
840
- def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> NDArray:
842
+ def determine_species(e_bin: np.ndarray, type: str) -> NDArray:
841
843
  """
842
844
  Determine the species for pulse-height events.
843
845
 
844
- Species is determined from the particle velocity.
845
- For velocity, the particle TOF is normalized with respect
846
- to a fixed distance dmin between the front and back detectors.
847
- The normalized TOF is termed the corrected TOF (ctof).
848
- Particle species are determined from ctof using thresholds.
849
-
850
- Further description is available on pages 42-44 of
851
- IMAP-Ultra Flight Software Specification document.
846
+ Species is determined using the computed e_bin.
852
847
 
853
848
  Parameters
854
849
  ----------
855
- tof : np.ndarray
856
- Time of flight of the SSD event (tenths of a nanosecond).
857
- path_length : np.ndarray
858
- Path length (r) (hundredths of a millimeter).
850
+ e_bin : np.ndarray
851
+ Computed e_bin.
859
852
  type : str
860
853
  Type of data (PH or SSD).
861
854
 
@@ -864,11 +857,17 @@ def determine_species(tof: np.ndarray, path_length: np.ndarray, type: str) -> ND
864
857
  species_bin : np.array
865
858
  Species bin.
866
859
  """
867
- # Event TOF normalization to Z axis
868
- ctof, _ = get_ctof(tof, path_length, type)
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)
860
+ if type == "PH":
861
+ species_groups = UltraConstants.TOFXPH_SPECIES_GROUPS
862
+ if type == "SSD":
863
+ species_groups = UltraConstants.TOFXE_SPECIES_GROUPS
864
+
865
+ non_proton_bins = species_groups["non_proton"]
866
+ proton_bins = species_groups["proton"]
867
+
868
+ species_bin = np.full(e_bin.shape, fill_value=2, dtype=int)
869
+ species_bin[np.isin(e_bin, non_proton_bins)] = 0
870
+ species_bin[np.isin(e_bin, proton_bins)] = 1
872
871
 
873
872
  return species_bin
874
873
 
@@ -984,9 +983,9 @@ def get_eventtimes(
984
983
  Returns
985
984
  -------
986
985
  event_times : np.ndarray
987
- Event times.
986
+ Event times in et.
988
987
  spin_starts : np.ndarray
989
- Spin start times.
988
+ Spin start times in et.
990
989
  spin_period_sec : np.ndarray
991
990
  Spin period in seconds.
992
991
 
@@ -1007,7 +1006,7 @@ def get_eventtimes(
1007
1006
 
1008
1007
  event_times = spin_starts + spin_period_sec * (phase_angle / 720)
1009
1008
 
1010
- return event_times, spin_starts, spin_period_sec
1009
+ return sct_to_et(event_times), sct_to_et(spin_starts), spin_period_sec
1011
1010
 
1012
1011
 
1013
1012
  def interpolate_fwhm(
@@ -1375,54 +1374,91 @@ def is_coin_ph_valid(
1375
1374
  etof: NDArray,
1376
1375
  xc: NDArray,
1377
1376
  xb: NDArray,
1377
+ stop_north_tdc: NDArray,
1378
+ stop_south_tdc: NDArray,
1379
+ stop_east_tdc: NDArray,
1380
+ stop_west_tdc: NDArray,
1378
1381
  sensor: str,
1379
1382
  ancillary_files: dict,
1383
+ quality_flags: NDArray,
1380
1384
  ) -> NDArray:
1381
1385
  """
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
+ Determine event validity.
1386
1387
 
1387
1388
  Parameters
1388
1389
  ----------
1389
1390
  etof : NDArray
1390
- Electron TOF (tenths of a nanosecond).
1391
+ Time for the electrons to travel back to the coincidence
1392
+ anode (tenths of a nanosecond).
1391
1393
  xc : NDArray
1392
- Coincidence X position (hundredths of a mm).
1394
+ X coincidence position (hundredths of a millimeter).
1393
1395
  xb : NDArray
1394
- Back X position (hundredths of a mm).
1396
+ Back positions in x direction (hundredths of a millimeter).
1397
+ stop_north_tdc : NDArray
1398
+ Stop North Time to Digital Converter.
1399
+ stop_south_tdc : NDArray
1400
+ Stop South Time to Digital Converter.
1401
+ stop_east_tdc : NDArray
1402
+ Stop East Time to Digital Converter.
1403
+ stop_west_tdc : NDArray
1404
+ Stop West Time to Digital Converter.
1395
1405
  sensor : str
1396
1406
  Sensor name: "ultra45" or "ultra90".
1397
1407
  ancillary_files : dict
1398
1408
  Ancillary files for lookup.
1409
+ quality_flags : NDArray
1410
+ Quality flag to set when there is an outlier.
1399
1411
 
1400
1412
  Returns
1401
1413
  -------
1402
- valid_mask : NDArray
1403
- Boolean array indicating Coin-PH validity.
1414
+ combined_mask : NDArray
1415
+ Boolean array indicating whether back TOF is valid.
1404
1416
 
1405
1417
  Notes
1406
1418
  -----
1407
- Logic derived from page 36 of the IMAP-Ultra Flight Software Specification document.
1419
+ From page 36 of the IMAP-Ultra Flight Software Specification document.
1408
1420
  """
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)
1421
+ # Make certain etof is within range for tenths of a nanosecond.
1422
+ etof_valid = (etof >= UltraConstants.ETOFMIN_EVENTFILTER) & (
1423
+ etof <= UltraConstants.ETOFMAX_EVENTFILTER
1424
+ )
1413
1425
 
1426
+ # Hundredths of a mm.
1414
1427
  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
1428
 
1420
- t1 = (etof - etof_offset1) * etof_slope1 / 1024
1421
- t2 = (etof - etof_offset2) * etof_slope2 / 1024
1429
+ t1 = (
1430
+ (etof - UltraConstants.ETOFOFF1_EVENTFILTER)
1431
+ * UltraConstants.ETOFSLOPE1_EVENTFILTER
1432
+ / 1024
1433
+ )
1434
+ t2 = (
1435
+ (etof - UltraConstants.ETOFOFF2_EVENTFILTER)
1436
+ * UltraConstants.ETOFSLOPE2_EVENTFILTER
1437
+ / 1024
1438
+ )
1422
1439
 
1423
1440
  condition_1 = (diff_x >= t1) & (diff_x <= t2)
1424
1441
  condition_2 = (diff_x >= -t2) & (diff_x <= -t1)
1425
1442
 
1426
1443
  spatial_valid = condition_1 | condition_2
1427
1444
 
1428
- return etof_valid & spatial_valid
1445
+ sp_n_norm = get_norm(stop_north_tdc, "SpN", sensor, ancillary_files)
1446
+ sp_s_norm = get_norm(stop_south_tdc, "SpS", sensor, ancillary_files)
1447
+ sp_e_norm = get_norm(stop_east_tdc, "SpE", sensor, ancillary_files)
1448
+ sp_w_norm = get_norm(stop_west_tdc, "SpW", sensor, ancillary_files)
1449
+
1450
+ tofx = sp_n_norm + sp_s_norm
1451
+ tofy = sp_e_norm + sp_w_norm
1452
+
1453
+ # Units in tenths of a nanosecond
1454
+ delta_tof = tofy - tofx
1455
+
1456
+ delta_tof_mask = (delta_tof >= UltraConstants.TOFDIFFTPMIN_EVENTFILTER) & (
1457
+ delta_tof <= UltraConstants.TOFDIFFTPMAX_EVENTFILTER
1458
+ )
1459
+
1460
+ combined_mask = etof_valid & spatial_valid & delta_tof_mask
1461
+
1462
+ quality_flags[~combined_mask] |= ImapDEOutliersUltraFlags.COINPH.value
1463
+
1464
+ return combined_mask