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

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

Potentially problematic release.


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

Files changed (122) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -0
  4. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +221 -1057
  5. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +307 -283
  6. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1044 -203
  7. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  8. imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +11 -0
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +15 -1
  10. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +5 -0
  11. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +10 -4
  12. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  13. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +8 -91
  14. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +106 -16
  15. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +5 -4
  16. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  17. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  18. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +85 -2
  19. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  20. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +20 -8
  21. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +45 -35
  22. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +110 -7
  23. imap_processing/cli.py +138 -93
  24. imap_processing/codice/codice_l0.py +2 -1
  25. imap_processing/codice/codice_l1a.py +167 -69
  26. imap_processing/codice/codice_l1b.py +42 -32
  27. imap_processing/codice/codice_l2.py +215 -9
  28. imap_processing/codice/constants.py +790 -603
  29. imap_processing/codice/data/lo_stepping_values.csv +1 -1
  30. imap_processing/decom.py +1 -4
  31. imap_processing/ena_maps/ena_maps.py +71 -43
  32. imap_processing/ena_maps/utils/corrections.py +291 -0
  33. imap_processing/ena_maps/utils/map_utils.py +20 -4
  34. imap_processing/ena_maps/utils/naming.py +8 -2
  35. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  37. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  38. imap_processing/glows/ancillary/imap_glows_pipeline-settings_20250923_v002.json +54 -0
  39. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  40. imap_processing/glows/l1b/glows_l1b.py +123 -18
  41. imap_processing/glows/l1b/glows_l1b_data.py +358 -47
  42. imap_processing/glows/l2/glows_l2.py +11 -0
  43. imap_processing/hi/hi_l1a.py +124 -3
  44. imap_processing/hi/hi_l1b.py +154 -71
  45. imap_processing/hi/hi_l1c.py +4 -109
  46. imap_processing/hi/hi_l2.py +104 -60
  47. imap_processing/hi/utils.py +262 -8
  48. imap_processing/hit/l0/constants.py +3 -0
  49. imap_processing/hit/l0/decom_hit.py +3 -6
  50. imap_processing/hit/l1a/hit_l1a.py +311 -21
  51. imap_processing/hit/l1b/hit_l1b.py +54 -126
  52. imap_processing/hit/l2/hit_l2.py +6 -6
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +12 -2
  55. imap_processing/ialirt/generate_coverage.py +15 -2
  56. imap_processing/ialirt/l0/ialirt_spice.py +6 -2
  57. imap_processing/ialirt/l0/parse_mag.py +293 -42
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/process_ephemeris.py +70 -14
  61. imap_processing/ialirt/utils/create_xarray.py +1 -1
  62. imap_processing/idex/idex_l0.py +2 -2
  63. imap_processing/idex/idex_l1a.py +2 -3
  64. imap_processing/idex/idex_l1b.py +2 -3
  65. imap_processing/idex/idex_l2a.py +130 -4
  66. imap_processing/idex/idex_l2b.py +158 -143
  67. imap_processing/idex/idex_utils.py +1 -3
  68. imap_processing/lo/ancillary_data/imap_lo_hydrogen-geometric-factor_v001.csv +75 -0
  69. imap_processing/lo/ancillary_data/imap_lo_oxygen-geometric-factor_v001.csv +75 -0
  70. imap_processing/lo/l0/lo_science.py +25 -24
  71. imap_processing/lo/l1b/lo_l1b.py +93 -19
  72. imap_processing/lo/l1c/lo_l1c.py +273 -93
  73. imap_processing/lo/l2/lo_l2.py +949 -135
  74. imap_processing/lo/lo_ancillary.py +55 -0
  75. imap_processing/mag/l1a/mag_l1a.py +1 -0
  76. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  77. imap_processing/mag/l1b/mag_l1b.py +3 -2
  78. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  79. imap_processing/mag/l1c/mag_l1c.py +23 -6
  80. imap_processing/mag/l1d/mag_l1d.py +57 -14
  81. imap_processing/mag/l1d/mag_l1d_data.py +202 -32
  82. imap_processing/mag/l2/mag_l2.py +2 -0
  83. imap_processing/mag/l2/mag_l2_data.py +14 -5
  84. imap_processing/quality_flags.py +23 -1
  85. imap_processing/spice/geometry.py +89 -39
  86. imap_processing/spice/pointing_frame.py +4 -8
  87. imap_processing/spice/repoint.py +78 -2
  88. imap_processing/spice/spin.py +28 -8
  89. imap_processing/spice/time.py +12 -22
  90. imap_processing/swapi/l1/swapi_l1.py +10 -4
  91. imap_processing/swapi/l2/swapi_l2.py +15 -17
  92. imap_processing/swe/l1b/swe_l1b.py +1 -2
  93. imap_processing/ultra/constants.py +30 -24
  94. imap_processing/ultra/l0/ultra_utils.py +9 -11
  95. imap_processing/ultra/l1a/ultra_l1a.py +1 -2
  96. imap_processing/ultra/l1b/badtimes.py +35 -11
  97. imap_processing/ultra/l1b/de.py +95 -31
  98. imap_processing/ultra/l1b/extendedspin.py +31 -16
  99. imap_processing/ultra/l1b/goodtimes.py +112 -0
  100. imap_processing/ultra/l1b/lookup_utils.py +281 -28
  101. imap_processing/ultra/l1b/quality_flag_filters.py +10 -1
  102. imap_processing/ultra/l1b/ultra_l1b.py +7 -7
  103. imap_processing/ultra/l1b/ultra_l1b_culling.py +169 -7
  104. imap_processing/ultra/l1b/ultra_l1b_extended.py +311 -69
  105. imap_processing/ultra/l1c/helio_pset.py +139 -37
  106. imap_processing/ultra/l1c/l1c_lookup_utils.py +289 -0
  107. imap_processing/ultra/l1c/spacecraft_pset.py +140 -29
  108. imap_processing/ultra/l1c/ultra_l1c.py +33 -24
  109. imap_processing/ultra/l1c/ultra_l1c_culling.py +92 -0
  110. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +400 -292
  111. imap_processing/ultra/l2/ultra_l2.py +54 -11
  112. imap_processing/ultra/utils/ultra_l1_utils.py +37 -7
  113. imap_processing/utils.py +3 -4
  114. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/METADATA +2 -2
  115. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/RECORD +118 -109
  116. imap_processing/idex/idex_l2c.py +0 -84
  117. imap_processing/spice/kernels.py +0 -187
  118. imap_processing/ultra/l1b/cullingmask.py +0 -87
  119. imap_processing/ultra/l1c/histogram.py +0 -36
  120. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/LICENSE +0 -0
  121. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/WHEEL +0 -0
  122. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/entry_points.txt +0 -0
@@ -3,9 +3,9 @@
3
3
  import logging
4
4
  from enum import IntEnum
5
5
  from pathlib import Path
6
- from typing import Union
7
6
 
8
7
  import numpy as np
8
+ import pandas as pd
9
9
  import xarray as xr
10
10
 
11
11
  from imap_processing import imap_module_directory
@@ -15,6 +15,7 @@ from imap_processing.hi.hi_l1a import HALF_CLOCK_TICK_S
15
15
  from imap_processing.hi.utils import (
16
16
  HIAPID,
17
17
  CoincidenceBitmap,
18
+ EsaEnergyStepLookupTable,
18
19
  HiConstants,
19
20
  create_dataset_variables,
20
21
  parse_sensor_number,
@@ -27,7 +28,7 @@ from imap_processing.spice.spin import (
27
28
  get_instrument_spin_phase,
28
29
  get_spacecraft_spin_phase,
29
30
  )
30
- from imap_processing.spice.time import met_to_sclkticks, sct_to_et
31
+ from imap_processing.spice.time import met_to_sclkticks, met_to_utc, sct_to_et
31
32
  from imap_processing.utils import packet_file_to_datasets
32
33
 
33
34
 
@@ -45,52 +46,7 @@ ATTR_MGR.add_instrument_global_attrs("hi")
45
46
  ATTR_MGR.add_instrument_variable_attrs(instrument="hi", level=None)
46
47
 
47
48
 
48
- def hi_l1b(dependency: Union[str, Path, xr.Dataset]) -> list[xr.Dataset]:
49
- """
50
- High level IMAP-HI L1B processing function.
51
-
52
- Parameters
53
- ----------
54
- dependency : str or xarray.Dataset
55
- Path to L0 file or L1A dataset to process.
56
-
57
- Returns
58
- -------
59
- l1b_dataset : list[xarray.Dataset]
60
- Processed xarray datasets.
61
- """
62
- # Housekeeping processing
63
- if isinstance(dependency, (Path, str)):
64
- logger.info(f"Running Hi L1B processing on file: {dependency}")
65
- l1b_datasets = housekeeping(dependency)
66
- elif isinstance(dependency, xr.Dataset):
67
- l1a_dataset = dependency
68
- logger.info(
69
- f"Running Hi L1B processing on dataset: "
70
- f"{l1a_dataset.attrs['Logical_source']}"
71
- )
72
- logical_source_parts = parse_filename_like(l1a_dataset.attrs["Logical_source"])
73
- # TODO: apid is not currently stored in all L1A data but should be.
74
- # Use apid to determine what L1B processing function to call
75
-
76
- # DE processing
77
- if logical_source_parts["descriptor"].endswith("de"):
78
- l1b_datasets = [annotate_direct_events(l1a_dataset)]
79
- l1b_datasets[0].attrs["Logical_source"] = (
80
- l1b_datasets[0]
81
- .attrs["Logical_source"]
82
- .format(sensor=logical_source_parts["sensor"])
83
- )
84
- else:
85
- raise NotImplementedError(
86
- f"No Hi L1B processing defined for file type: "
87
- f"{l1a_dataset.attrs['Logical_source']}"
88
- )
89
-
90
- return l1b_datasets
91
-
92
-
93
- def housekeeping(packet_file_path: Union[str, Path]) -> list[xr.Dataset]:
49
+ def housekeeping(packet_file_path: str | Path) -> list[xr.Dataset]:
94
50
  """
95
51
  Will process IMAP raw data to l1b housekeeping dataset.
96
52
 
@@ -108,6 +64,7 @@ def housekeeping(packet_file_path: Union[str, Path]) -> list[xr.Dataset]:
108
64
  processed_data : list[xarray.Dataset]
109
65
  Housekeeping datasets with engineering units.
110
66
  """
67
+ logger.info(f"Running Hi L1B processing on file: {packet_file_path}")
111
68
  packet_def_file = (
112
69
  imap_module_directory / "hi/packet_definitions/TLM_HI_COMBINED_SCI.xml"
113
70
  )
@@ -137,33 +94,46 @@ def housekeeping(packet_file_path: Union[str, Path]) -> list[xr.Dataset]:
137
94
  return datasets
138
95
 
139
96
 
140
- def annotate_direct_events(l1a_dataset: xr.Dataset) -> xr.Dataset:
97
+ def annotate_direct_events(
98
+ l1a_de_dataset: xr.Dataset, l1b_hk_dataset: xr.Dataset, esa_energies_anc: Path
99
+ ) -> list[xr.Dataset]:
141
100
  """
142
101
  Perform Hi L1B processing on direct event data.
143
102
 
144
103
  Parameters
145
104
  ----------
146
- l1a_dataset : xarray.Dataset
105
+ l1a_de_dataset : xarray.Dataset
147
106
  L1A direct event data.
107
+ l1b_hk_dataset : xarray.Dataset
108
+ L1B housekeeping data coincident with the L1A DE data.
109
+ esa_energies_anc : pathlib.Path
110
+ Location of the esa-energies ancillary csv file.
148
111
 
149
112
  Returns
150
113
  -------
151
- l1b_dataset : xarray.Dataset
152
- L1B direct event data.
114
+ l1b_datasets : list[xarray.Dataset]
115
+ List containing exactly one L1B direct event dataset.
153
116
  """
154
- l1b_dataset = l1a_dataset.copy()
155
- l1b_dataset.update(de_esa_energy_step(l1b_dataset))
156
- l1b_dataset.update(compute_coincidence_type_and_tofs(l1b_dataset))
157
- l1b_dataset.update(de_nominal_bin_and_spin_phase(l1b_dataset))
158
- l1b_dataset.update(compute_hae_coordinates(l1b_dataset))
159
- l1b_dataset.update(
117
+ logger.info(
118
+ f"Running Hi L1B processing on dataset: "
119
+ f"{l1a_de_dataset.attrs['Logical_source']}"
120
+ )
121
+
122
+ l1b_de_dataset = l1a_de_dataset.copy()
123
+ l1b_de_dataset.update(
124
+ de_esa_energy_step(l1b_de_dataset, l1b_hk_dataset, esa_energies_anc)
125
+ )
126
+ l1b_de_dataset.update(compute_coincidence_type_and_tofs(l1b_de_dataset))
127
+ l1b_de_dataset.update(de_nominal_bin_and_spin_phase(l1b_de_dataset))
128
+ l1b_de_dataset.update(compute_hae_coordinates(l1b_de_dataset))
129
+ l1b_de_dataset.update(
160
130
  create_dataset_variables(
161
131
  ["quality_flag"],
162
- l1b_dataset["event_met"].size,
132
+ l1b_de_dataset["event_met"].size,
163
133
  att_manager_lookup_str="hi_de_{0}",
164
134
  )
165
135
  )
166
- l1b_dataset = l1b_dataset.drop_vars(
136
+ l1b_de_dataset = l1b_de_dataset.drop_vars(
167
137
  [
168
138
  "src_seq_ctr",
169
139
  "pkt_len",
@@ -179,8 +149,13 @@ def annotate_direct_events(l1a_dataset: xr.Dataset) -> xr.Dataset:
179
149
  )
180
150
 
181
151
  de_global_attrs = ATTR_MGR.get_global_attributes("imap_hi_l1b_de_attrs")
182
- l1b_dataset.attrs.update(**de_global_attrs)
183
- return l1b_dataset
152
+ l1b_de_dataset.attrs.update(**de_global_attrs)
153
+
154
+ logical_source_parts = parse_filename_like(l1a_de_dataset.attrs["Logical_source"])
155
+ l1b_de_dataset.attrs["Logical_source"] = l1b_de_dataset.attrs[
156
+ "Logical_source"
157
+ ].format(sensor=logical_source_parts["sensor"])
158
+ return [l1b_de_dataset]
184
159
 
185
160
 
186
161
  def compute_coincidence_type_and_tofs(
@@ -387,18 +362,20 @@ def compute_hae_coordinates(dataset: xr.Dataset) -> dict[str, xr.DataArray]:
387
362
  return new_vars
388
363
 
389
364
 
390
- def de_esa_energy_step(dataset: xr.Dataset) -> dict[str, xr.DataArray]:
365
+ def de_esa_energy_step(
366
+ l1b_de_ds: xr.Dataset, l1b_hk_ds: xr.Dataset, esa_energies_anc: Path
367
+ ) -> dict[str, xr.DataArray]:
391
368
  """
392
369
  Compute esa_energy_step for each direct event.
393
370
 
394
- TODO: For now this function just returns the esa_step from the input dataset.
395
- Eventually, it will take L1B housekeeping data and determine the esa
396
- energy steps from that data.
397
-
398
371
  Parameters
399
372
  ----------
400
- dataset : xarray.Dataset
373
+ l1b_de_ds : xarray.Dataset
401
374
  The partial L1B dataset.
375
+ l1b_hk_ds : xarray.Dataset
376
+ L1B housekeeping data coincident with the L1A DE data.
377
+ esa_energies_anc : pathlib.Path
378
+ Location of the esa-energies ancillary csv file.
402
379
 
403
380
  Returns
404
381
  -------
@@ -407,10 +384,116 @@ def de_esa_energy_step(dataset: xr.Dataset) -> dict[str, xr.DataArray]:
407
384
  """
408
385
  new_vars = create_dataset_variables(
409
386
  ["esa_energy_step"],
410
- len(dataset.epoch),
387
+ len(l1b_de_ds.epoch),
411
388
  att_manager_lookup_str="hi_de_{0}",
412
389
  )
413
- # TODO: Implement this algorithm
414
- new_vars["esa_energy_step"].values = dataset.esa_step.values
390
+
391
+ # Get the LUT object using the HK data and esa-energies ancillary csv
392
+ esa_energies_lut = pd.read_csv(esa_energies_anc, comment="#")
393
+ esa_to_esa_energy_step_lut = get_esa_to_esa_energy_step_lut(
394
+ l1b_hk_ds, esa_energies_lut
395
+ )
396
+ new_vars["esa_energy_step"].values = esa_to_esa_energy_step_lut.query(
397
+ l1b_de_ds["ccsds_met"].data, l1b_de_ds["esa_step"].data
398
+ )
415
399
 
416
400
  return new_vars
401
+
402
+
403
+ def get_esa_to_esa_energy_step_lut(
404
+ l1b_hk_ds: xr.Dataset, esa_energies_lut: pd.DataFrame
405
+ ) -> EsaEnergyStepLookupTable:
406
+ """
407
+ Generate a lookup table that associates an esa_step to an esa_energy_step.
408
+
409
+ Parameters
410
+ ----------
411
+ l1b_hk_ds : xarray.Dataset
412
+ L1B housekeeping dataset.
413
+ esa_energies_lut : pandas.DataFrame
414
+ Esa energies lookup table derived from ancillary file.
415
+
416
+ Returns
417
+ -------
418
+ esa_energy_step_lut : EsaEnergyStepLookupTable
419
+ A lookup table object that can be used to query by MET time and esa_step
420
+ for the associated esa_energy_step values.
421
+
422
+ Notes
423
+ -----
424
+ Algorithm definition in section 2.1.2 of IMAP Hi Algorithm Document.
425
+ """
426
+ # Instantiate a lookup table object
427
+ esa_energy_step_lut = EsaEnergyStepLookupTable()
428
+ # Get the set of esa_steps visited
429
+ esa_steps = list(sorted(set(l1b_hk_ds["sci_esa_step"].data)))
430
+ # Break into contiguous segments where op_mode == "HVSCI"
431
+ # Pad the boolean array `op_mode == HVSCI` with False values on each end.
432
+ # This treats starting or ending in HVSCI mode as a transition in the next
433
+ # step where np.diff is used to find op_mode transitions into and out of
434
+ # HVSCI
435
+ padded_mask = np.pad(
436
+ l1b_hk_ds["op_mode"].data == "HVSCI", (1, 1), constant_values=False
437
+ )
438
+ mode_changes = np.diff(padded_mask.astype(int))
439
+ hsvsci_starts = np.nonzero(mode_changes == 1)[0]
440
+ hsvsci_ends = np.nonzero(mode_changes == -1)[0]
441
+ for i_start, i_end in zip(hsvsci_starts, hsvsci_ends, strict=False):
442
+ contiguous_hvsci_ds = l1b_hk_ds.isel(dict(epoch=slice(i_start, i_end)))
443
+ # Find median inner and outer ESA voltages for each ESA step
444
+ for esa_step in esa_steps:
445
+ single_esa_ds = contiguous_hvsci_ds.where(
446
+ contiguous_hvsci_ds["sci_esa_step"] == esa_step, drop=True
447
+ )
448
+ if len(single_esa_ds["epoch"].data) == 0:
449
+ logger.debug(
450
+ f"No instances of sci_esa_step == {esa_step} "
451
+ f"present in contiguous HVSCI block with interval: "
452
+ f"({met_to_utc(contiguous_hvsci_ds['shcoarse'].data[[0, -1]])})"
453
+ )
454
+ continue
455
+ inner_esa_voltage = np.where(
456
+ single_esa_ds["inner_esa_state"].data == "LO",
457
+ single_esa_ds["inner_esa_lo"].data,
458
+ single_esa_ds["inner_esa_hi"].data,
459
+ )
460
+ median_inner_esa = np.median(inner_esa_voltage)
461
+ median_outer_esa = np.median(single_esa_ds["outer_esa"].data)
462
+ # Match median voltages to ESA Energies LUT
463
+ inner_voltage_match = (
464
+ np.abs(median_inner_esa - esa_energies_lut["inner_esa_voltage"])
465
+ <= esa_energies_lut["inner_esa_delta_v"]
466
+ )
467
+ outer_voltage_match = (
468
+ np.abs(median_outer_esa - esa_energies_lut["outer_esa_voltage"])
469
+ <= esa_energies_lut["outer_esa_delta_v"]
470
+ )
471
+ matching_esa_energy = esa_energies_lut[
472
+ np.logical_and(inner_voltage_match, outer_voltage_match)
473
+ ]
474
+ if len(matching_esa_energy) != 1:
475
+ if len(matching_esa_energy) == 0:
476
+ logger.critical(
477
+ f"No esa_energy_step matches found for esa_step "
478
+ f"{esa_step} during interval: "
479
+ f"({met_to_utc(single_esa_ds['shcoarse'].data[[0, -1]])}) "
480
+ f"with median esa voltages: "
481
+ f"{median_inner_esa}, {median_outer_esa}."
482
+ )
483
+ if len(matching_esa_energy) > 1:
484
+ logger.critical(
485
+ f"Multiple esa_energy_step matches found for esa_step "
486
+ f"{esa_step} during interval: "
487
+ f"({met_to_utc(single_esa_ds['shcoarse'].data[[0, -1]])}) "
488
+ f"with median esa voltages: "
489
+ f"{median_inner_esa}, {median_outer_esa}."
490
+ )
491
+ continue
492
+ # Set LUT to matching esa_energy_step for time range
493
+ esa_energy_step_lut.add_entry(
494
+ contiguous_hvsci_ds["shcoarse"].data[0],
495
+ contiguous_hvsci_ds["shcoarse"].data[-1],
496
+ esa_step,
497
+ matching_esa_energy["esa_energy_step"].values[0],
498
+ )
499
+ return esa_energy_step_lut
@@ -19,7 +19,7 @@ from imap_processing.hi.hi_l1a import (
19
19
  HALF_CLOCK_TICK_S,
20
20
  )
21
21
  from imap_processing.hi.utils import (
22
- CoincidenceBitmap,
22
+ CalibrationProductConfig,
23
23
  create_dataset_variables,
24
24
  full_dataarray,
25
25
  parse_sensor_number,
@@ -378,8 +378,9 @@ def pset_counts(
378
378
  filtered_de_df["spin_phase"].to_numpy() * N_SPIN_BINS
379
379
  ).astype(int)
380
380
  # When iterating over rows of a dataframe, the names of the multi-index
381
- # are not preserved. Below, `config_row.Index[0]` gets the cal_prod_num
382
- # value from the namedtuple representing the dataframe row.
381
+ # are not preserved. Below, `config_row.Index[0]` gets the
382
+ # calibration_prod value from the namedtuple representing the
383
+ # dataframe row.
383
384
  np.add.at(
384
385
  counts_var["counts"].data[0, i_esa, config_row.Index[0]],
385
386
  spin_bin_indices,
@@ -684,109 +685,3 @@ def good_time_and_phase_mask(
684
685
  """
685
686
  # TODO: Implement this once we have Goodtimes data product defined.
686
687
  return np.full_like(tick_mets, True, dtype=bool)
687
-
688
-
689
- @pd.api.extensions.register_dataframe_accessor("cal_prod_config")
690
- class CalibrationProductConfig:
691
- """
692
- Register custom accessor for calibration product configuration DataFrames.
693
-
694
- Parameters
695
- ----------
696
- pandas_obj : pandas.DataFrame
697
- Object to run validation and use accessor functions on.
698
- """
699
-
700
- index_columns = (
701
- "cal_prod_num",
702
- "esa_energy_step",
703
- )
704
- tof_detector_pairs = ("ab", "ac1", "bc1", "c1c2")
705
- required_columns = (
706
- "coincidence_type_list",
707
- *[
708
- f"tof_{det_pair}_{limit}"
709
- for det_pair in tof_detector_pairs
710
- for limit in ["low", "high"]
711
- ],
712
- )
713
-
714
- def __init__(self, pandas_obj: pd.DataFrame) -> None:
715
- self._validate(pandas_obj)
716
- self._obj = pandas_obj
717
- self._add_coincidence_values_column()
718
-
719
- def _validate(self, df: pd.DataFrame) -> None:
720
- """
721
- Validate the current configuration.
722
-
723
- Parameters
724
- ----------
725
- df : pandas.DataFrame
726
- Object to validate.
727
-
728
- Raises
729
- ------
730
- AttributeError : If the dataframe does not pass validation.
731
- """
732
- for index_name in self.index_columns:
733
- if index_name in df.index:
734
- raise AttributeError(
735
- f"Required index {index_name} not present in dataframe."
736
- )
737
- # Verify that the Dataframe has all the required columns
738
- for col in self.required_columns:
739
- if col not in df.columns:
740
- raise AttributeError(f"Required column {col} not present in dataframe.")
741
- # TODO: Verify that the same ESA energy steps exist in all unique calibration
742
- # product numbers
743
-
744
- def _add_coincidence_values_column(self) -> None:
745
- """Generate and add the coincidence_type_values column to the dataframe."""
746
- # Add a column that consists of the coincidence type strings converted
747
- # to integer values
748
- self._obj["coincidence_type_values"] = self._obj.apply(
749
- lambda row: tuple(
750
- CoincidenceBitmap.detector_hit_str_to_int(entry)
751
- for entry in row["coincidence_type_list"]
752
- ),
753
- axis=1,
754
- )
755
-
756
- @classmethod
757
- def from_csv(cls, path: Path) -> pd.DataFrame:
758
- """
759
- Read configuration CSV file into a pandas.DataFrame.
760
-
761
- Parameters
762
- ----------
763
- path : pathlib.Path
764
- Location of the Calibration Product configuration CSV file.
765
-
766
- Returns
767
- -------
768
- dataframe : pandas.DataFrame
769
- Validated calibration product configuration data frame.
770
- """
771
- df = pd.read_csv(
772
- path,
773
- index_col=cls.index_columns,
774
- converters={"coincidence_type_list": lambda s: tuple(s.split("|"))},
775
- comment="#",
776
- )
777
- # Force the _init_ method to run by using the namespace
778
- _ = df.cal_prod_config.number_of_products
779
- return df
780
-
781
- @property
782
- def number_of_products(self) -> int:
783
- """
784
- Get the number of calibration products in the current configuration.
785
-
786
- Returns
787
- -------
788
- number_of_products : int
789
- The maximum number of calibration products defined in the list of
790
- calibration product definitions.
791
- """
792
- return len(self._obj.index.unique(level="cal_prod_num"))