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
@@ -54,6 +54,8 @@ class CoDICEL1aPipeline:
54
54
 
55
55
  Methods
56
56
  -------
57
+ apply_despinning()
58
+ Apply the despinning algorithm to lo- angular and priority products.
57
59
  decompress_data(science_values)
58
60
  Perform decompression on the data.
59
61
  define_coordinates()
@@ -87,6 +89,73 @@ class CoDICEL1aPipeline:
87
89
  self.plan_step = plan_step
88
90
  self.view_id = view_id
89
91
 
92
+ def apply_despinning(self) -> None:
93
+ """
94
+ Apply the despinning algorithm to lo- angular and priority products.
95
+
96
+ This only applies to CoDICE-Lo angular and priority data products. See
97
+ sections 9.3.4 and 9.3.5 of the algorithm document for more details.
98
+ """
99
+ # Determine the appropriate dimensions for the despun data
100
+ num_energies = self.config["dims"]["esa_step"]
101
+ num_spin_sectors = self.config["dims"]["spin_sector"]
102
+ num_spins = num_spin_sectors * 2
103
+ num_counters = self.config["num_counters"]
104
+ num_positions = self.config["dims"].get(
105
+ "inst_az"
106
+ ) # Defaults to None if not present
107
+
108
+ # The dimensions are dependent on the specific data product
109
+ if "angular" in self.config["dataset_name"]:
110
+ despun_dims: tuple[int, ...] = (
111
+ num_energies,
112
+ num_positions,
113
+ num_spins,
114
+ num_counters,
115
+ )
116
+ elif "priority" in self.config["dataset_name"]:
117
+ despun_dims = (num_energies, num_spins, num_counters)
118
+
119
+ # Placeholder for finalized despun data
120
+ self.data: list[np.ndarray] # Needed to appease mypy
121
+ despun_data = [np.zeros(despun_dims) for _ in range(len(self.data))]
122
+
123
+ # Iterate over the energy and spin sector indices, and determine the
124
+ # appropriate pixel orientation. The combination of the pixel
125
+ # orientation and the azimuth determine which spin sector the data
126
+ # gets stored in.
127
+ # TODO: All these nested for-loops are bad. Try to find a better
128
+ # solution.
129
+ for i, epoch_data in enumerate(self.data):
130
+ for energy_index in range(num_energies):
131
+ pixel_orientation = constants.PIXEL_ORIENTATIONS[energy_index]
132
+ for spin_sector_index in range(num_spin_sectors):
133
+ for azimuth_index in range(num_spins):
134
+ if pixel_orientation == "A" and azimuth_index < 12:
135
+ despun_spin_sector = spin_sector_index
136
+ elif pixel_orientation == "A" and azimuth_index >= 12:
137
+ despun_spin_sector = spin_sector_index + 12
138
+ elif pixel_orientation == "B" and azimuth_index < 12:
139
+ despun_spin_sector = spin_sector_index + 12
140
+ elif pixel_orientation == "B" and azimuth_index >= 12:
141
+ despun_spin_sector = spin_sector_index
142
+
143
+ if "angular" in self.config["dataset_name"]:
144
+ spin_data = epoch_data[
145
+ energy_index, :, spin_sector_index, :
146
+ ] # (5, 4)
147
+ despun_data[i][energy_index, :, despun_spin_sector, :] = (
148
+ spin_data
149
+ )
150
+ elif "priority" in self.config["dataset_name"]:
151
+ spin_data = epoch_data[energy_index, spin_sector_index, :]
152
+ despun_data[i][energy_index, despun_spin_sector, :] = (
153
+ spin_data
154
+ )
155
+
156
+ # Replace original data
157
+ self.data = despun_data
158
+
90
159
  def decompress_data(self, science_values: list[NDArray[str]] | list[str]) -> None:
91
160
  """
92
161
  Perform decompression on the data.
@@ -122,7 +191,7 @@ class CoDICEL1aPipeline:
122
191
 
123
192
  else:
124
193
  for packet_data, byte_count in zip(
125
- science_values, self.dataset.byte_count.data
194
+ science_values, self.dataset.byte_count.data, strict=False
126
195
  ):
127
196
  # Convert from numpy array to byte object
128
197
  values = packet_data[()]
@@ -134,17 +203,20 @@ class CoDICEL1aPipeline:
134
203
  decompressed_values = decompress(values, compression_algorithm)
135
204
  self.raw_data.append(decompressed_values)
136
205
 
137
- def define_coordinates(self) -> None:
206
+ def define_coordinates(self) -> None: # noqa: PLR0912 (too many branches)
138
207
  """
139
208
  Create ``xr.DataArrays`` for the coords needed in the final dataset.
140
209
 
141
210
  The coordinates for the dataset depend on the data product being made.
211
+
212
+ # TODO: Split this function up or simplify it to avoid too many branches
213
+ # error.
142
214
  """
143
215
  self.coords = {}
144
216
 
145
217
  coord_names = [
146
- *self.config["output_dims"].keys(),
147
- *[key + "_label" for key in self.config["output_dims"].keys()],
218
+ *self.config["dims"].keys(),
219
+ *[key + "_label" for key in self.config["dims"].keys()],
148
220
  ]
149
221
 
150
222
  # Define epoch coordinates
@@ -169,12 +241,17 @@ class CoDICEL1aPipeline:
169
241
  if name in [
170
242
  "esa_step",
171
243
  "inst_az",
172
- "spin_sector",
173
244
  "spin_sector_pairs",
174
245
  "spin_sector_index",
175
246
  "ssd_index",
176
247
  ]:
177
- values = np.arange(self.config["output_dims"][name])
248
+ values = np.arange(self.config["dims"][name])
249
+ dims = [name]
250
+ elif name == "spin_sector":
251
+ if self.config["dataset_name"] in constants.REQUIRES_DESPINNING:
252
+ values = np.arange(24)
253
+ else:
254
+ values = np.arange(self.config["dims"][name])
178
255
  dims = [name]
179
256
  elif name == "spin_sector_pairs_label":
180
257
  values = np.array(
@@ -188,16 +265,29 @@ class CoDICEL1aPipeline:
188
265
  ]
189
266
  )
190
267
  dims = [name]
268
+ elif name == "inst_az_label":
269
+ if self.config["dataset_name"] == "imap_codice_l1a_lo-nsw-angular":
270
+ values = [str(x) for x in range(4, 23)]
271
+ elif self.config["dataset_name"] == "imap_codice_l1a_lo-sw-angular":
272
+ values = ["1", "2", "3", "23", "24"]
273
+ else:
274
+ values = np.arange(self.config["dims"]["inst_az"]).astype(str)
275
+ dims = ["inst_az"]
191
276
  elif name in [
192
- "spin_sector_label",
193
277
  "esa_step_label",
194
- "inst_az_label",
195
278
  "spin_sector_index_label",
196
279
  "ssd_index_label",
197
280
  ]:
198
281
  key = name.removesuffix("_label")
199
- values = np.arange(self.config["output_dims"][key]).astype(str)
282
+ values = np.arange(self.config["dims"][key]).astype(str)
200
283
  dims = [key]
284
+ elif name == "spin_sector_label":
285
+ key = name.removesuffix("_label")
286
+ dims = [key]
287
+ if self.config["dataset_name"] in constants.REQUIRES_DESPINNING:
288
+ values = np.arange(24).astype(str)
289
+ else:
290
+ values = np.arange(self.config["dims"][key]).astype(str)
201
291
 
202
292
  coord = xr.DataArray(
203
293
  values,
@@ -230,16 +320,16 @@ class CoDICEL1aPipeline:
230
320
  # Stack the data so that it is easier to reshape and iterate over
231
321
  all_data = np.stack(self.data)
232
322
 
233
- # The dimension of all_data is something like (epoch, num_counters,
234
- # num_energy_steps, num_positions, num_spin_sectors) (or may be slightly
323
+ # The dimension of all_data is something like (epoch, num_energy_steps,
324
+ # num_positions, num_spin_sectors, num_counters) (or may be slightly
235
325
  # different depending on the data product). In any case, iterate over
236
326
  # the num_counters dimension to isolate the data for each counter so
237
327
  # each counter's data can be placed in a separate CDF data variable.
238
328
  for counter, variable_name in zip(
239
- range(all_data.shape[1]), self.config["variable_names"]
329
+ range(all_data.shape[-1]), self.config["variable_names"], strict=False
240
330
  ):
241
331
  # Extract the counter data
242
- counter_data = all_data[:, counter, ...]
332
+ counter_data = all_data[..., counter]
243
333
 
244
334
  # Get the CDF attributes
245
335
  descriptor = self.config["dataset_name"].split("imap_codice_l1a_")[-1]
@@ -249,7 +339,7 @@ class CoDICEL1aPipeline:
249
339
  # For most products, the final CDF dimensions always has "epoch" as
250
340
  # the first dimension followed by the dimensions for the specific
251
341
  # data product
252
- dims = ["epoch", *list(self.config["output_dims"].keys())]
342
+ dims = ["epoch", *list(self.config["dims"].keys())]
253
343
 
254
344
  # However, CoDICE-Hi products use specific energy bins for the
255
345
  # energy dimension
@@ -306,7 +396,7 @@ class CoDICEL1aPipeline:
306
396
  ``xarray`` dataset for the data product, with added energy variables.
307
397
  """
308
398
  energy_bin_name = f"energy_{species}"
309
- centers, deltas = self.get_hi_energy_table_data(
399
+ centers, deltas_minus, deltas_plus = self.get_hi_energy_table_data(
310
400
  energy_bin_name.split("energy_")[-1]
311
401
  )
312
402
 
@@ -319,11 +409,19 @@ class CoDICEL1aPipeline:
319
409
  check_schema=False,
320
410
  ),
321
411
  )
322
- dataset[f"{energy_bin_name}_delta"] = xr.DataArray(
323
- deltas,
324
- dims=[f"{energy_bin_name}_delta"],
412
+ dataset[f"{energy_bin_name}_minus"] = xr.DataArray(
413
+ deltas_minus,
414
+ dims=[f"{energy_bin_name}_minus"],
415
+ attrs=self.cdf_attrs.get_variable_attributes(
416
+ f"{self.config['dataset_name'].split('_')[-1]}-{energy_bin_name}_minus",
417
+ check_schema=False,
418
+ ),
419
+ )
420
+ dataset[f"{energy_bin_name}_plus"] = xr.DataArray(
421
+ deltas_plus,
422
+ dims=[f"{energy_bin_name}_plus"],
325
423
  attrs=self.cdf_attrs.get_variable_attributes(
326
- f"{self.config['dataset_name'].split('_')[-1]}-{energy_bin_name}_delta",
424
+ f"{self.config['dataset_name'].split('_')[-1]}-{energy_bin_name}_plus",
327
425
  check_schema=False,
328
426
  ),
329
427
  )
@@ -398,6 +496,12 @@ class CoDICEL1aPipeline:
398
496
  dims = ["epoch"]
399
497
  attrs = self.cdf_attrs.get_variable_attributes("spin_period")
400
498
 
499
+ # The k-factor is a constant that maps voltages to energies
500
+ elif variable_name == "k_factor":
501
+ variable_data = np.array([constants.K_FACTOR], dtype=np.float32)
502
+ dims = [""]
503
+ attrs = self.cdf_attrs.get_variable_attributes("k_factor")
504
+
401
505
  # Add variable to the dataset
402
506
  dataset[variable_name] = xr.DataArray(
403
507
  variable_data,
@@ -475,7 +579,7 @@ class CoDICEL1aPipeline:
475
579
 
476
580
  def get_hi_energy_table_data(
477
581
  self, species: str
478
- ) -> tuple[NDArray[float], NDArray[float]]:
582
+ ) -> tuple[NDArray[float], NDArray[float], NDArray[float]]:
479
583
  """
480
584
  Retrieve energy table data for CoDICE-Hi products.
481
585
 
@@ -493,22 +597,25 @@ class CoDICEL1aPipeline:
493
597
  -------
494
598
  centers : NDArray[float]
495
599
  An array whose values represent the centers of the energy bins.
496
- deltas : NDArray[float]
497
- An array whose values represent the deltas of the energy bins.
600
+ deltas_minus : NDArray[float]
601
+ An array whose values represent the minus deltas of the energy bins.
602
+ deltas_plus : NDArray[float]
603
+ An array whose values represent the plus deltas of the energy bins.
498
604
  """
499
605
  data_product = self.config["dataset_name"].split("-")[-1].upper()
500
- energy_table = getattr(constants, f"{data_product}_ENERGY_TABLE")[species]
501
-
502
- # Find the centers and deltas of the energy bins
503
- centers = np.array(
504
- [
505
- (energy_table[i] + energy_table[i + 1]) / 2
506
- for i in range(len(energy_table) - 1)
507
- ]
606
+ energy_table = np.array(
607
+ getattr(constants, f"{data_product}_ENERGY_TABLE")[species]
508
608
  )
509
- deltas = energy_table[1:] - centers
510
609
 
511
- return centers, deltas
610
+ # Find the geometric centers and deltas of the energy bins
611
+ # The delta minus is the difference between the center of the bin
612
+ # and the 'left edge' of the bin. The delta plus is the difference
613
+ # between the 'right edge' of the bin and the center of the bin
614
+ centers = np.sqrt(energy_table[:-1] * energy_table[1:])
615
+ deltas_minus = centers - energy_table[:-1]
616
+ deltas_plus = energy_table[1:] - centers
617
+
618
+ return centers, deltas_minus, deltas_plus
512
619
 
513
620
  def reshape_binned_data(self, dataset: xr.Dataset) -> dict[str, list]:
514
621
  """
@@ -589,52 +696,31 @@ class CoDICEL1aPipeline:
589
696
 
590
697
  These data need to be divided up by species or priorities (or
591
698
  what I am calling "counters" as a general term), and re-arranged into
592
- 4D arrays representing dimensions such as time, spin sectors, positions,
593
- and energies (depending on the data product).
699
+ multidimensional arrays representing dimensions such as time,
700
+ spin sectors, positions, and energies (depending on the data product).
594
701
 
595
702
  However, the existence and order of these dimensions can vary depending
596
- on the specific data product, so we define this in the "input_dims"
597
- and "output_dims" values configuration dictionary; the "input_dims"
598
- defines how the dimensions are written into the packet data, while
599
- "output_dims" defines how the dimensions should be written to the final
600
- CDF product.
703
+ on the specific data product, so we define this in the "dims" key of the
704
+ configuration dictionary.
601
705
  """
602
706
  # This will contain the reshaped data for all counters
603
707
  self.data = []
604
708
 
605
- # First reshape the data based on how it is written to the data array of
606
- # the packet data. The number of counters is the first dimension / axis,
607
- # with the exception of lo-counters-aggregated which is treated slightly
608
- # differently
609
- if self.config["dataset_name"] != "imap_codice_l1a_lo-counters-aggregated":
610
- reshape_dims = (
611
- self.config["num_counters"],
612
- *self.config["input_dims"].values(),
613
- )
614
- else:
615
- reshape_dims = (
616
- *self.config["input_dims"].values(),
617
- self.config["num_counters"],
618
- )
619
-
620
- # Then, transpose the data based on how the dimensions should be written
621
- # to the CDF file. Since this is specific to each data product, we need
622
- # to determine this dynamically based on the "output_dims" config.
623
- # Again, lo-counters-aggregated is treated slightly differently
624
- input_keys = ["num_counters", *self.config["input_dims"].keys()]
625
- output_keys = ["num_counters", *self.config["output_dims"].keys()]
626
- if self.config["dataset_name"] != "imap_codice_l1a_lo-counters-aggregated":
627
- transpose_axes = [input_keys.index(dim) for dim in output_keys]
628
- else:
629
- transpose_axes = [1, 2, 0] # [esa_step, spin_sector_pairs, num_counters]
630
-
709
+ # Reshape the data based on how it is written to the data array of
710
+ # the packet data. The number of counters is the last dimension / axis.
711
+ reshape_dims = (
712
+ *self.config["dims"].values(),
713
+ self.config["num_counters"],
714
+ )
631
715
  for packet_data in self.raw_data:
632
716
  reshaped_packet_data = np.array(packet_data, dtype=np.uint32).reshape(
633
717
  reshape_dims
634
718
  )
635
- reshaped_cdf_data = np.transpose(reshaped_packet_data, axes=transpose_axes)
719
+ self.data.append(reshaped_packet_data)
636
720
 
637
- self.data.append(reshaped_cdf_data)
721
+ # Apply despinning if necessary
722
+ if self.config["dataset_name"] in constants.REQUIRES_DESPINNING:
723
+ self.apply_despinning()
638
724
 
639
725
  # No longer need to keep the raw data around
640
726
  del self.raw_data
@@ -950,9 +1036,9 @@ def create_direct_event_dataset(apid: int, packets: xr.Dataset) -> xr.Dataset:
950
1036
 
951
1037
  # Create the dataset to hold the data variables
952
1038
  if apid == CODICEAPID.COD_LO_PHA:
953
- attrs = cdf_attrs.get_global_attributes("imap_codice_l1a_lo-pha")
1039
+ attrs = cdf_attrs.get_global_attributes("imap_codice_l1a_lo-direct-events")
954
1040
  elif apid == CODICEAPID.COD_HI_PHA:
955
- attrs = cdf_attrs.get_global_attributes("imap_codice_l1a_hi-pha")
1041
+ attrs = cdf_attrs.get_global_attributes("imap_codice_l1a_hi-direct-events")
956
1042
  dataset = xr.Dataset(
957
1043
  coords={
958
1044
  "epoch": epoch,
@@ -967,9 +1053,9 @@ def create_direct_event_dataset(apid: int, packets: xr.Dataset) -> xr.Dataset:
967
1053
  # Create the CDF data variables for each Priority and Field
968
1054
  for i in range(constants.DE_DATA_PRODUCT_CONFIGURATIONS[apid]["num_priorities"]):
969
1055
  for field in constants.DE_DATA_PRODUCT_CONFIGURATIONS[apid]["cdf_fields"]:
970
- variable_name = f"P{i}_{field}"
1056
+ variable_name = f"p{i}_{field}"
971
1057
  attrs = cdf_attrs.get_variable_attributes(variable_name)
972
- if field in ["NumEvents", "DataQuality"]:
1058
+ if field in ["num_events", "data_quality"]:
973
1059
  dims = ["epoch"]
974
1060
  else:
975
1061
  dims = ["epoch", "event_num"]
@@ -1427,13 +1513,15 @@ def reshape_de_data(
1427
1513
  for priority_num in range(num_priorities):
1428
1514
  for field in bit_structure:
1429
1515
  if field not in ["Priority", "Spare"]:
1430
- data[f"P{priority_num}_{field}"] = np.full(
1516
+ data[f"p{priority_num}_{field}"] = np.full(
1431
1517
  (num_epochs, 10000),
1432
1518
  bit_structure[field]["fillval"],
1433
1519
  dtype=bit_structure[field]["dtype"],
1434
1520
  )
1435
- data[f"P{priority_num}_NumEvents"] = np.full(num_epochs, 65535, dtype=np.uint16)
1436
- data[f"P{priority_num}_DataQuality"] = np.full(num_epochs, 255, dtype=np.uint8)
1521
+ data[f"p{priority_num}_num_events"] = np.full(
1522
+ num_epochs, 65535, dtype=np.uint16
1523
+ )
1524
+ data[f"p{priority_num}_data_quality"] = np.full(num_epochs, 255, dtype=np.uint8)
1437
1525
 
1438
1526
  # decompressed_data is one large list of values of length
1439
1527
  # (<number of epochs> * <number of priorities>)
@@ -1457,8 +1545,8 @@ def reshape_de_data(
1457
1545
 
1458
1546
  # Number of events and data quality can be determined at this stage
1459
1547
  num_events = num_events_arr[epoch_start:epoch_end][i]
1460
- data[f"P{priority_num}_NumEvents"][epoch_index] = num_events
1461
- data[f"P{priority_num}_DataQuality"][epoch_index] = data_quality[i]
1548
+ data[f"p{priority_num}_num_events"][epoch_index] = num_events
1549
+ data[f"p{priority_num}_data_quality"][epoch_index] = data_quality[i]
1462
1550
 
1463
1551
  # Iterate over each event
1464
1552
  for event_index in range(num_events):
@@ -1489,7 +1577,7 @@ def reshape_de_data(
1489
1577
  )
1490
1578
 
1491
1579
  # Set the value into the data array
1492
- data[f"P{priority_num}_{field_name}"][epoch_index, event_index] = (
1580
+ data[f"p{priority_num}_{field_name}"][epoch_index, event_index] = (
1493
1581
  value
1494
1582
  )
1495
1583
  bit_position += field_components["bit_length"]
@@ -116,7 +116,7 @@ def process_codice_l1b(file_path: Path) -> xr.Dataset:
116
116
  descriptor = dataset_name.removeprefix("imap_codice_l1b_")
117
117
 
118
118
  # Direct event data products do not have a level L1B
119
- if descriptor in ["lo-pha", "hi-pha"]:
119
+ if descriptor in ["lo-direct-events", "hi-direct-events"]:
120
120
  logger.warning("Encountered direct event data product. Skipping L1b processing")
121
121
  return None
122
122
 
@@ -45,22 +45,112 @@ def process_codice_l2(file_path: Path) -> xr.Dataset:
45
45
  # TODO: Could clean this up by using imap-data-access methods?
46
46
  dataset_name = l1_dataset.attrs["Logical_source"]
47
47
  data_level = dataset_name.removeprefix("imap_codice_").split("_")[0]
48
- descriptor = dataset_name.removeprefix(f"imap_codice_{data_level}_")
49
48
  dataset_name = dataset_name.replace(data_level, "l2")
50
49
 
51
- # TODO: Temporary work-around to replace "PHA" naming convention with
52
- # "direct events" This will eventually be changed at the L1 level and
53
- # thus this will eventually be removed.
54
- if descriptor == "lo-pha":
55
- dataset_name = dataset_name.replace("lo-pha", "lo-direct-events")
56
- elif descriptor == "hi-pha":
57
- dataset_name = dataset_name.replace("hi-pha", "hi-direct-events")
58
-
59
50
  # Use the L1 data product as a starting point for L2
60
51
  l2_dataset = l1_dataset.copy()
61
52
 
62
53
  # Get the L2 CDF attributes
63
54
  cdf_attrs = ImapCdfAttributes()
55
+ l2_dataset = add_dataset_attributes(l2_dataset, dataset_name, cdf_attrs)
56
+
57
+ if dataset_name in [
58
+ "imap_codice_l2_hi-counters-singles",
59
+ "imap_codice_l2_hi-counters-aggregated",
60
+ "imap_codice_l2_lo-counters-singles",
61
+ "imap_codice_l2_lo-counters-aggregated",
62
+ "imap_codice_l2_lo-sw-priority",
63
+ "imap_codice_l2_lo-nsw-priority",
64
+ ]:
65
+ # No changes needed. Just save to an L2 CDF file.
66
+ pass
67
+
68
+ elif dataset_name == "imap_codice_l2_hi-direct-events":
69
+ # Convert the following data variables to physical units using
70
+ # calibration data:
71
+ # - ssd_energy
72
+ # - tof
73
+ # - elevation_angle
74
+ # - spin_angle
75
+ # These converted variables are *in addition* to the existing L1 variables
76
+ # The other data variables require no changes
77
+ # See section 11.1.2 of algorithm document
78
+ pass
79
+
80
+ elif dataset_name == "imap_codice_l2_hi-sectored":
81
+ # Convert the sectored count rates using equation described in section
82
+ # 11.1.3 of algorithm document.
83
+ pass
84
+
85
+ elif dataset_name == "imap_codice_l2_hi-omni":
86
+ # Calculate the omni-directional intensity for each species using
87
+ # equation described in section 11.1.4 of algorithm document
88
+ # hopefully this can also apply to hi-ialirt
89
+ pass
90
+
91
+ elif dataset_name == "imap_codice_l2_lo-direct-events":
92
+ # Convert the following data variables to physical units using
93
+ # calibration data:
94
+ # - apd_energy
95
+ # - elevation_angle
96
+ # - tof
97
+ # - spin_sector
98
+ # - esa_step
99
+ # These converted variables are *in addition* to the existing L1 variables
100
+ # The other data variables require no changes
101
+ # See section 11.1.2 of algorithm document
102
+ pass
103
+
104
+ elif dataset_name == "imap_codice_l2_lo-sw-angular":
105
+ # Calculate the sunward angular intensities using equation described in
106
+ # section 11.2.3 of algorithm document.
107
+ pass
108
+
109
+ elif dataset_name == "imap_codice_l2_lo-nsw-angular":
110
+ # Calculate the non-sunward angular intensities using equation described
111
+ # in section 11.2.3 of algorithm document.
112
+ pass
113
+
114
+ elif dataset_name == "imap_codice_l2_lo-sw-species":
115
+ # Calculate the sunward solar wind species intensities using equation
116
+ # described in section 11.2.4 of algorithm document.
117
+ # Calculate the pickup ion sunward solar wind intensities using equation
118
+ # described in section 11.2.4 of algorithm document.
119
+ # Hopefully this can also apply to lo-ialirt
120
+ pass
121
+
122
+ elif dataset_name == "imap_codice_l2_lo-nsw-species":
123
+ # Calculate the non-sunward solar wind species intensities using
124
+ # equation described in section 11.2.4 of algorithm document.
125
+ # Calculate the pickup ion non-sunward solar wind intensities using
126
+ # equation described in section 11.2.4 of algorithm document.
127
+ pass
128
+
129
+ logger.info(f"\nFinal data product:\n{l2_dataset}\n")
130
+
131
+ return l2_dataset
132
+
133
+
134
+ def add_dataset_attributes(
135
+ l2_dataset: xr.Dataset, dataset_name: str, cdf_attrs: ImapCdfAttributes
136
+ ) -> xr.Dataset:
137
+ """
138
+ Add the global and variable attributes to the dataset.
139
+
140
+ Parameters
141
+ ----------
142
+ l2_dataset : xarray.Dataset
143
+ The dataset to update.
144
+ dataset_name : str
145
+ The name of the dataset.
146
+ cdf_attrs : ImapCdfAttributes
147
+ The attribute manager for CDF attributes.
148
+
149
+ Returns
150
+ -------
151
+ xarray.Dataset
152
+ The updated dataset.
153
+ """
64
154
  cdf_attrs.add_instrument_global_attrs("codice")
65
155
  cdf_attrs.add_instrument_variable_attrs("codice", "l2")
66
156
 
@@ -68,14 +158,23 @@ def process_codice_l2(file_path: Path) -> xr.Dataset:
68
158
  l2_dataset.attrs = cdf_attrs.get_global_attributes(dataset_name)
69
159
 
70
160
  # Set the variable attributes
71
- for variable_name in l2_dataset:
72
- l2_dataset[variable_name].attrs = cdf_attrs.get_variable_attributes(
73
- variable_name, check_schema=False
74
- )
75
-
76
- # TODO: Add L2-specific algorithms/functionality here. For SIT-4, we can
77
- # just keep the data as-is.
78
-
79
- logger.info(f"\nFinal data product:\n{l2_dataset}\n")
80
-
161
+ for variable_name in l2_dataset.data_vars.keys():
162
+ try:
163
+ l2_dataset[variable_name].attrs = cdf_attrs.get_variable_attributes(
164
+ variable_name, check_schema=False
165
+ )
166
+ except KeyError:
167
+ # Some variables may have a product descriptor prefix in the
168
+ # cdf attributes key if they are common to multiple products.
169
+ descriptor = dataset_name.split("imap_codice_l2_")[-1]
170
+ cdf_attrs_key = f"{descriptor}-{variable_name}"
171
+ try:
172
+ l2_dataset[variable_name].attrs = cdf_attrs.get_variable_attributes(
173
+ f"{cdf_attrs_key}", check_schema=False
174
+ )
175
+ except KeyError:
176
+ logger.error(
177
+ f"Field '{variable_name}' and '{cdf_attrs_key}' not found in "
178
+ f"attribute manager."
179
+ )
81
180
  return l2_dataset