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
@@ -12,31 +12,77 @@ from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
- TIME_PER_BIN = 0.167 # seconds
15
+ SWAPI_LIVETIME = 0.145 # seconds
16
16
 
17
17
 
18
18
  def solve_full_sweep_energy(
19
- esa_lvl5_data: np.ndarray, esa_table_df: pd.DataFrame, lut_notes_df: pd.DataFrame
19
+ esa_lvl5_data: np.ndarray,
20
+ sweep_table: np.ndarray,
21
+ esa_table_df: pd.DataFrame,
22
+ lut_notes_df: pd.DataFrame,
23
+ data_time: npt.NDArray[np.datetime64],
20
24
  ) -> npt.NDArray:
21
25
  """
22
26
  Calculate the energy of each full sweep data.
23
27
 
28
+ Get the fixed energy values for steps 0-62 using the
29
+ esa_table_df information. It's important to ensure
30
+ that the correct fixed energy values are selected for
31
+ the specified time, as the sweep table can contain
32
+ different values depending on the operational phase
33
+ (e.g., I+T, pre-launch, post-launch). There may be
34
+ more fixed energy added in the future. TODO: add
35
+ document section once SWAPI document is updated.
36
+
37
+ Now, find the last 9 fine energy values using steps
38
+ noted in the section x in the algorithm document.
39
+
24
40
  Parameters
25
41
  ----------
26
42
  esa_lvl5_data : numpy.ndarray
27
43
  The L1 data input.
44
+ sweep_table : numpy.ndarray
45
+ Sweep table information.
28
46
  esa_table_df : pandas.DataFrame
29
47
  The ESA unit conversion table that contains first 63 energies.
30
48
  lut_notes_df : pandas.DataFrame
31
49
  The LUT notes table that contains the last 9 fine energies.
50
+ data_time : numpy.ndarray
51
+ The collection time of the data.
32
52
 
33
53
  Returns
34
54
  -------
35
55
  energy : numpy.ndarray
36
56
  The energy of each full sweep data.
37
57
  """
38
- # Read 0 - 62 energy steps' fixed energy value
39
- fixed_energy_values = esa_table_df["Energy"].values[:63]
58
+ # Convert timestamp from string to datetime
59
+ # and to the same format as data_time
60
+ esa_table_df["timestamp"] = pd.to_datetime(
61
+ esa_table_df["timestamp"], format="%m/%d/%Y %H:%M"
62
+ )
63
+ esa_table_df["timestamp"] = esa_table_df["timestamp"].to_numpy(
64
+ dtype="datetime64[ns]"
65
+ )
66
+
67
+ first_63_energies = []
68
+
69
+ for time, sweep_id in zip(data_time, sweep_table, strict=False):
70
+ # Find the sweep's ESA data for the given time and sweep_id
71
+ subset = esa_table_df[
72
+ (esa_table_df["timestamp"] <= time) & (esa_table_df["Sweep #"] == sweep_id)
73
+ ]
74
+ if subset.empty:
75
+ first_63_energies.append(np.full(63, np.nan, dtype=np.float64))
76
+ continue
77
+
78
+ # Subset data can contain multiple 72 energy values with last 9 fine energies
79
+ # with 'Solve' value. We need to sort by time and ESA step to maintain correct
80
+ # order. Then take the last group of 72 steps values and select first 63
81
+ # values only.
82
+ subset = subset.sort_values(["timestamp", "ESA Step #"])
83
+ grouped = subset["Energy"].values.reshape(-1, 72)
84
+ first_63 = grouped[-1, :63]
85
+ first_63_energies.append(first_63)
40
86
 
41
87
  # Find last 9 fine energy values of all sweeps data
42
88
  # -------------------------------------------------
@@ -96,13 +142,9 @@ def solve_full_sweep_energy(
96
142
  # order it should be in:
97
143
  # [64, 65, 66, 67, 68, 69, 70, 71, 72]
98
144
  energy_values = np.flip(energy_values, axis=1)
99
- # Expand to match the number of rows in energy_values
100
- first_63_values = np.tile(
101
- fixed_energy_values, (energy_values.shape[0], 1)
102
- ) # (epoch, 63)
103
145
 
104
146
  # Append the first_63_values in front of energy_values
105
- sweeps_energy_value = np.hstack((first_63_values, energy_values))
147
+ sweeps_energy_value = np.hstack([first_63_energies, energy_values])
106
148
 
107
149
  return sweeps_energy_value
108
150
 
@@ -117,14 +159,12 @@ def swapi_l2(
117
159
 
118
160
  To process science data to L2, we need to:
119
161
  - convert counts to rates. This is done by dividing the counts by the
120
- TIME_PER_BIN time. TIME_PER_BIN is the exposure time per energy bin which is
121
- obtained by dividing the time for one complete sweep
122
- (12 s, coarse + fine sweep) by the total energy steps (72),
123
- i.e., TIME_PER_BIN = 12/72 = 0.167 s. This will be constant.
162
+ SWAPI_LIVETIME time. LIVETIME is data acquisition time. It will
163
+ be constant, SWAPI_LIVETIME = 0.145 s.
124
164
 
125
165
  - update uncertainty. Calculate new uncertainty value using
126
- SWP_PCEM_ERR data from level one and divide by TIME_PER_BIN. Eg.
127
- SWP_PCEM_UNC = SWP_PCEM_ERR / TIME_PER_BIN
166
+ SWP_PCEM_ERR data from level one and divide by SWAPI_LIVETIME. Eg.
167
+ SWP_PCEM_UNC = SWP_PCEM_ERR / SWAPI_LIVETIME
128
168
  Do the same for SCEM and COIN data.
129
169
 
130
170
  Parameters
@@ -169,8 +209,10 @@ def swapi_l2(
169
209
  esa_lvl5_hex = np.vectorize(lambda x: format(x, "X"))(l1_dataset["esa_lvl5"].values)
170
210
  esa_energy = solve_full_sweep_energy(
171
211
  esa_lvl5_hex,
212
+ l1_dataset["sweep_table"].data,
172
213
  esa_table_df=esa_table_df,
173
214
  lut_notes_df=lut_notes_df,
215
+ data_time=np.array(l1_dataset["epoch"].data, dtype="datetime64[ns]"),
174
216
  )
175
217
 
176
218
  l2_dataset["swp_esa_energy"] = xr.DataArray(
@@ -189,9 +231,9 @@ def swapi_l2(
189
231
  ]
190
232
 
191
233
  # convert counts to rate
192
- l2_dataset["swp_pcem_rate"] = l1_dataset["swp_pcem_counts"] / TIME_PER_BIN
193
- l2_dataset["swp_scem_rate"] = l1_dataset["swp_scem_counts"] / TIME_PER_BIN
194
- l2_dataset["swp_coin_rate"] = l1_dataset["swp_coin_counts"] / TIME_PER_BIN
234
+ l2_dataset["swp_pcem_rate"] = l1_dataset["swp_pcem_counts"] / SWAPI_LIVETIME
235
+ l2_dataset["swp_scem_rate"] = l1_dataset["swp_scem_counts"] / SWAPI_LIVETIME
236
+ l2_dataset["swp_coin_rate"] = l1_dataset["swp_coin_counts"] / SWAPI_LIVETIME
195
237
  # update attrs
196
238
  l2_dataset["swp_pcem_rate"].attrs = cdf_manager.get_variable_attributes("pcem_rate")
197
239
  l2_dataset["swp_scem_rate"].attrs = cdf_manager.get_variable_attributes("scem_rate")
@@ -199,22 +241,22 @@ def swapi_l2(
199
241
 
200
242
  # update uncertainty
201
243
  l2_dataset["swp_pcem_rate_stat_uncert_plus"] = (
202
- l1_dataset["swp_pcem_counts_stat_uncert_plus"] / TIME_PER_BIN
244
+ l1_dataset["swp_pcem_counts_stat_uncert_plus"] / SWAPI_LIVETIME
203
245
  )
204
246
  l2_dataset["swp_pcem_rate_stat_uncert_minus"] = (
205
- l1_dataset["swp_pcem_counts_stat_uncert_minus"] / TIME_PER_BIN
247
+ l1_dataset["swp_pcem_counts_stat_uncert_minus"] / SWAPI_LIVETIME
206
248
  )
207
249
  l2_dataset["swp_scem_rate_stat_uncert_plus"] = (
208
- l1_dataset["swp_scem_counts_stat_uncert_plus"] / TIME_PER_BIN
250
+ l1_dataset["swp_scem_counts_stat_uncert_plus"] / SWAPI_LIVETIME
209
251
  )
210
252
  l2_dataset["swp_scem_rate_stat_uncert_minus"] = (
211
- l1_dataset["swp_scem_counts_stat_uncert_minus"] / TIME_PER_BIN
253
+ l1_dataset["swp_scem_counts_stat_uncert_minus"] / SWAPI_LIVETIME
212
254
  )
213
255
  l2_dataset["swp_coin_rate_stat_uncert_plus"] = (
214
- l1_dataset["swp_coin_counts_stat_uncert_plus"] / TIME_PER_BIN
256
+ l1_dataset["swp_coin_counts_stat_uncert_plus"] / SWAPI_LIVETIME
215
257
  )
216
258
  l2_dataset["swp_coin_rate_stat_uncert_minus"] = (
217
- l1_dataset["swp_coin_counts_stat_uncert_minus"] / TIME_PER_BIN
259
+ l1_dataset["swp_coin_counts_stat_uncert_minus"] / SWAPI_LIVETIME
218
260
  )
219
261
  # update attrs
220
262
  l2_dataset[
@@ -51,7 +51,7 @@ def read_swapi_lut_table(file_path: Path) -> pd.DataFrame:
51
51
  .astype(str)
52
52
  .str.replace(",", "", regex=False)
53
53
  .replace("Solve", -1)
54
- .astype(np.int64)
54
+ .astype(np.float64)
55
55
  )
56
56
 
57
57
  return df
@@ -2,7 +2,6 @@
2
2
 
3
3
  import logging
4
4
  from pathlib import Path
5
- from typing import Union
6
5
 
7
6
  import numpy as np
8
7
  import numpy.typing as npt
@@ -51,7 +50,7 @@ def get_esa_dataframe(esa_table_number: int) -> pd.DataFrame:
51
50
 
52
51
 
53
52
  def deadtime_correction(
54
- counts: np.ndarray, acq_duration: Union[int, npt.NDArray]
53
+ counts: np.ndarray, acq_duration: int | npt.NDArray
55
54
  ) -> npt.NDArray:
56
55
  """
57
56
  Calculate deadtime correction.
@@ -767,10 +766,8 @@ def swe_l1b_science(dependencies: ProcessingInputCollection) -> xr.Dataset:
767
766
 
768
767
  # Store ESA energies of full cycle for L2 purposes.
769
768
  esa_energies = get_esa_energy_pattern(esa_lut_files[0])
770
- # Repeat energies to be in the same shape as the science data
771
- esa_energies = np.repeat(esa_energies, total_packets // 4).reshape(
772
- -1, swe_constants.N_ESA_STEPS, swe_constants.N_ANGLE_SECTORS
773
- )
769
+ # Repeat the (24, 30) energy pattern n_cycles times along a new first axis
770
+ esa_energies = np.repeat(esa_energies[np.newaxis, :, :], total_packets // 4, axis=0)
774
771
  # Convert voltage to electron energy in eV by apply conversion factor
775
772
  esa_energies = esa_energies * swe_constants.ENERGY_CONVERSION_FACTOR
776
773
  # ------------------------------------------------------------------
@@ -51,7 +51,6 @@ class UltraConstants:
51
51
  Z_DSTOP: float = 2.6 / 2 # Position of stop foil on Z axis [mm]
52
52
  Z_DS: float = 46.19 - (2.6 / 2) # Position of slit on Z axis [mm]
53
53
  DF: float = 3.39 # Distance from slit to foil [mm]
54
-
55
54
  # Derived constants
56
55
  DMIN_PH_CTOF: float = (
57
56
  Z_DS - (2**0.5) * DF
@@ -79,5 +78,31 @@ class UltraConstants:
79
78
  CULLING_RPM_MIN = 2.0
80
79
  CULLING_RPM_MAX = 6.0
81
80
 
82
- # Thresholds for culling based on counts.
83
- CULLING_ENERGY_BIN_EDGES: ClassVar[list] = [0, 10, 20, 1e5]
81
+ # Thresholds for culling based on counts (keV).
82
+ CULLING_ENERGY_BIN_EDGES: ClassVar[list] = [
83
+ 3.385,
84
+ 4.13722222222222,
85
+ 5.05660493827161,
86
+ 6.18029492455419,
87
+ 7.55369379667734,
88
+ 9.23229241816119,
89
+ 11.2839129555303,
90
+ 13.7914491678704,
91
+ 16.8562156496194,
92
+ 20.6020413495348,
93
+ 25.1802727605426,
94
+ 30.775888929552,
95
+ 37.6149753583414,
96
+ 45.9738587713061,
97
+ 56.1902718315964,
98
+ 68.6769989052845,
99
+ 83.93855421757,
100
+ 102.591566265919,
101
+ 125.38969210279,
102
+ 153.254068125632,
103
+ 187.310527709106,
104
+ 228.93508942224,
105
+ 279.809553738294,
106
+ 341.989454569026,
107
+ 1e5,
108
+ ]
@@ -4,6 +4,7 @@ import numpy as np
4
4
  from numpy.typing import NDArray
5
5
 
6
6
  from imap_processing.ultra.l0.ultra_utils import (
7
+ PacketProperties,
7
8
  parse_event,
8
9
  )
9
10
  from imap_processing.utils import convert_to_binary_string
@@ -155,8 +156,7 @@ def decompress_binary(
155
156
  def decompress_image(
156
157
  pixel0: int,
157
158
  binary_data: str,
158
- width_bit: int,
159
- mantissa_bit_length: int,
159
+ packet_props: PacketProperties,
160
160
  ) -> NDArray:
161
161
  """
162
162
  Will decompress a binary string representing an image into a matrix of pixel values.
@@ -171,10 +171,9 @@ def decompress_image(
171
171
  The first, unmodified pixel p0,0.
172
172
  binary_data : str
173
173
  Binary string.
174
- width_bit : int
175
- The bit width that describes the width of data in the block.
176
- mantissa_bit_length : int
177
- The bit length of the mantissa.
174
+ packet_props : PacketProperties
175
+ Properties of the packet, including width bit, mantissa bit length and pixel
176
+ window dimensions.
178
177
 
179
178
  Returns
180
179
  -------
@@ -187,10 +186,18 @@ def decompress_image(
187
186
  This process is described starting on page 168 in IMAP-Ultra Flight
188
187
  Software Specification document.
189
188
  """
190
- rows = 54
191
- cols = 180
189
+ rows = packet_props.pixel_window_rows
190
+ cols = packet_props.pixel_window_columns
191
+ width_bit = packet_props.width
192
+ mantissa_bit_length = packet_props.mantissa_bit_length
192
193
  pixels_per_block = 15
193
194
 
195
+ if width_bit is None or rows is None or cols is None or mantissa_bit_length is None:
196
+ raise ValueError(
197
+ "Packet properties must specify pixel window dimensions, "
198
+ "width bit, and mantissa bit length for this packet type."
199
+ )
200
+
194
201
  blocks_per_row = cols // pixels_per_block
195
202
 
196
203
  # Compressed pixel matrix
@@ -1,6 +1,7 @@
1
1
  """Decommutates Ultra CCSDS packets."""
2
2
 
3
3
  import logging
4
+ import math
4
5
  from collections import defaultdict
5
6
  from typing import cast
6
7
 
@@ -29,7 +30,7 @@ from imap_processing.ultra.l0.ultra_utils import (
29
30
  ULTRA_PRI_3_EVENTS,
30
31
  ULTRA_PRI_4_EVENTS,
31
32
  ULTRA_RATES,
32
- ULTRA_TOF,
33
+ PacketProperties,
33
34
  )
34
35
  from imap_processing.utils import convert_to_binary_string
35
36
 
@@ -37,7 +38,7 @@ logging.basicConfig(level=logging.INFO)
37
38
  logger = logging.getLogger(__name__)
38
39
 
39
40
 
40
- def process_ultra_tof(ds: xr.Dataset) -> xr.Dataset:
41
+ def process_ultra_tof(ds: xr.Dataset, packet_props: PacketProperties) -> xr.Dataset:
41
42
  """
42
43
  Unpack and decode Ultra TOF packets.
43
44
 
@@ -45,6 +46,9 @@ def process_ultra_tof(ds: xr.Dataset) -> xr.Dataset:
45
46
  ----------
46
47
  ds : xarray.Dataset
47
48
  TOF dataset.
49
+ packet_props : PacketProperties
50
+ Information that defines properties of the packet including the pixel window
51
+ dimensions of images and number of image panes.
48
52
 
49
53
  Returns
50
54
  -------
@@ -53,14 +57,35 @@ def process_ultra_tof(ds: xr.Dataset) -> xr.Dataset:
53
57
  """
54
58
  scalar_keys = [key for key in ds.data_vars if key not in ("packetdata", "sid")]
55
59
 
60
+ image_planes = packet_props.image_planes
61
+ rows = packet_props.pixel_window_rows
62
+ cols = packet_props.pixel_window_columns
63
+ planes_per_packet = packet_props.image_planes_per_packet
64
+
65
+ if (
66
+ image_planes is None
67
+ or rows is None
68
+ or cols is None
69
+ or planes_per_packet is None
70
+ ):
71
+ raise ValueError(
72
+ "Packet properties must specify pixel window dimensions, "
73
+ "width bit, image planes, and image planes per packet for this packet type."
74
+ )
75
+ # Calculate the number of image packets based on the number of image panes and
76
+ # planes per packet.
77
+ # There may be cases where the last packet has fewer planes than the
78
+ # planes_per_packet, to account for this, we use ceiling division.
79
+ num_image_packets = math.ceil(image_planes / planes_per_packet)
80
+
56
81
  decom_data: defaultdict[str, list[np.ndarray]] = defaultdict(list)
57
82
  decom_data["packetdata"] = []
58
83
  valid_epoch = []
59
- width = cast(int, ULTRA_TOF.width)
60
- mantissa_bit_length = cast(int, ULTRA_TOF.mantissa_bit_length)
61
84
 
62
85
  for val, group in ds.groupby("epoch"):
63
- if set(group["sid"].values) >= set(range(8)):
86
+ if set(group["sid"].values) >= set(
87
+ np.arange(0, image_planes, planes_per_packet)
88
+ ):
64
89
  valid_epoch.append(val)
65
90
  group.sortby("sid")
66
91
 
@@ -68,13 +93,12 @@ def process_ultra_tof(ds: xr.Dataset) -> xr.Dataset:
68
93
  decom_data[key].append(group[key].values)
69
94
 
70
95
  image = []
71
- for i in range(8):
96
+ for i in range(num_image_packets):
72
97
  binary = convert_to_binary_string(group["packetdata"].values[i])
73
98
  decompressed = decompress_image(
74
99
  group["p00"].values[i],
75
100
  binary,
76
- width,
77
- mantissa_bit_length,
101
+ packet_props,
78
102
  )
79
103
  image.append(decompressed)
80
104
 
@@ -87,9 +111,9 @@ def process_ultra_tof(ds: xr.Dataset) -> xr.Dataset:
87
111
 
88
112
  coords = {
89
113
  "epoch": np.array(valid_epoch, dtype=np.uint64),
90
- "sid": xr.DataArray(np.arange(8), dims=["sid"], name="sid"),
91
- "row": xr.DataArray(np.arange(54), dims=["row"], name="row"),
92
- "column": xr.DataArray(np.arange(180), dims=["column"], name="column"),
114
+ "sid": xr.DataArray(np.arange(num_image_packets), dims=["sid"], name="sid"),
115
+ "row": xr.DataArray(np.arange(rows), dims=["row"], name="row"),
116
+ "column": xr.DataArray(np.arange(cols), dims=["column"], name="column"),
93
117
  }
94
118
 
95
119
  dataset = xr.Dataset(coords=coords)
@@ -1,6 +1,6 @@
1
1
  """Contains data classes to support Ultra L0 processing."""
2
2
 
3
- from typing import NamedTuple, Union
3
+ from typing import NamedTuple
4
4
 
5
5
 
6
6
  class PacketProperties(NamedTuple):
@@ -9,16 +9,22 @@ class PacketProperties(NamedTuple):
9
9
  apid: list # List of APIDs
10
10
  logical_source: list # List of logical sources
11
11
  addition_to_logical_desc: str # Description of the logical source
12
- width: Union[int, None] # Width of binary data (could be None).
13
- block: Union[int, None] # Number of values in each block (could be None).
14
- # This is important for decompressing the images and
15
- # a description is available on page 171 of IMAP-Ultra Flight
16
- # Software Specification document (7523-9009_Rev_-.pdf).
17
- len_array: Union[
18
- int, None
19
- ] # Length of the array to be decompressed (could be None).
20
- mantissa_bit_length: Union[int, None] # used to determine the level of
12
+ width: int | None # Width of binary data (could be None).
13
+ # Block, image_planes, pixel_window_rows, and pixel_window_columns are important for
14
+ # decompressing the images and a description is available on page 171 of IMAP-Ultra
15
+ # Flight Software Specification document (7523-9009_Rev_-.pdf).
16
+ block: int | None # Number of values in each block (could be None).
17
+ len_array: int | None # Length of the array to be decompressed (could be None).
18
+ mantissa_bit_length: int | None # used to determine the level of
21
19
  # precision that can be recovered from compressed data (could be None).
20
+ image_planes: int | None = None
21
+ # number of images. See table 11 in the FSSD.
22
+ pixel_window_rows: int | None = None
23
+ # number of rows in each image. See table 49 in the FSSD.
24
+ pixel_window_columns: int | None = None
25
+ # number of columns in each image. See table 49 in the FSSD.
26
+ image_planes_per_packet: int | None = None
27
+ # number of image planes in each packet. See table 52 in the FSSD.
22
28
 
23
29
 
24
30
  # Define PacketProperties instances directly in the module namespace
@@ -64,15 +70,99 @@ ULTRA_ENERGY_SPECTRA = PacketProperties(
64
70
  len_array=1,
65
71
  mantissa_bit_length=5,
66
72
  )
67
- ULTRA_TOF = PacketProperties(
73
+ ULTRA_PHXTOF_HIGH_ANGULAR = PacketProperties(
68
74
  apid=[883, 947],
69
75
  logical_source=[
70
76
  "imap_ultra_l1a_45sensor-histogram-ena-phxtof-hi-ang",
71
77
  "imap_ultra_l1a_90sensor-histogram-ena-phxtof-hi-ang",
72
78
  ],
73
- addition_to_logical_desc="Time of Flight Images",
79
+ addition_to_logical_desc="Pulse Height Time of Flight High Angular Images",
74
80
  width=4,
75
81
  block=15,
82
+ image_planes=8,
83
+ pixel_window_rows=54,
84
+ pixel_window_columns=180,
85
+ image_planes_per_packet=1,
86
+ len_array=None,
87
+ mantissa_bit_length=4,
88
+ )
89
+ ULTRA_PHXTOF_HIGH_ENERGY = PacketProperties(
90
+ apid=[884, 948],
91
+ logical_source=[
92
+ "imap_ultra_l1a_45sensor-histogram-ena-phxtof-hi-nrg",
93
+ "imap_ultra_l1a_90sensor-histogram-ena-phxtof-hi-nrg",
94
+ ],
95
+ addition_to_logical_desc="Pulse Height By Time of Flight High Energy Images",
96
+ width=4,
97
+ block=15,
98
+ image_planes=28,
99
+ pixel_window_rows=27,
100
+ pixel_window_columns=90,
101
+ image_planes_per_packet=1,
102
+ len_array=None,
103
+ mantissa_bit_length=4,
104
+ )
105
+ ULTRA_PHXTOF_HIGH_TIME = PacketProperties(
106
+ apid=[885, 949],
107
+ logical_source=[
108
+ "imap_ultra_l1a_45sensor-histogram-ena-phxtof-hi-time",
109
+ "imap_ultra_l1a_90sensor-histogram-ena-phxtof-hi-time",
110
+ ],
111
+ addition_to_logical_desc="Time of Flight High Time Images",
112
+ width=4,
113
+ block=15,
114
+ image_planes=8,
115
+ pixel_window_rows=18,
116
+ pixel_window_columns=60,
117
+ image_planes_per_packet=2,
118
+ len_array=None,
119
+ mantissa_bit_length=4,
120
+ )
121
+ ULTRA_EXTOF_HIGH_ANGULAR = PacketProperties(
122
+ apid=[886, 950],
123
+ logical_source=[
124
+ "imap_ultra_l1a_45sensor-histogram-ena-extof-hi-ang",
125
+ "imap_ultra_l1a_90sensor-histogram-ena-extof-hi-ang",
126
+ ],
127
+ addition_to_logical_desc="Energy By Time of Flight High Angular Images",
128
+ width=4,
129
+ block=15,
130
+ image_planes=12,
131
+ pixel_window_rows=18,
132
+ pixel_window_columns=60,
133
+ image_planes_per_packet=2,
134
+ len_array=None,
135
+ mantissa_bit_length=4,
136
+ )
137
+ ULTRA_EXTOF_HIGH_TIME = PacketProperties(
138
+ apid=[888, 952],
139
+ logical_source=[
140
+ "imap_ultra_l1a_45sensor-histogram-ena-extof-hi-time",
141
+ "imap_ultra_l1a_90sensor-histogram-ena-extof-hi-time",
142
+ ],
143
+ addition_to_logical_desc="Energy By Time of Flight High Time Images",
144
+ width=4,
145
+ block=15,
146
+ image_planes=4,
147
+ pixel_window_rows=9,
148
+ pixel_window_columns=30,
149
+ image_planes_per_packet=8,
150
+ len_array=None,
151
+ mantissa_bit_length=4,
152
+ )
153
+ ULTRA_EXTOF_HIGH_ENERGY = PacketProperties(
154
+ apid=[887, 951],
155
+ logical_source=[
156
+ "imap_ultra_l1a_45sensor-histogram-ena-extof-hi-nrg",
157
+ "imap_ultra_l1a_90sensor-histogram-ena-extof-hi-nrg",
158
+ ],
159
+ addition_to_logical_desc="Energy By Time of Flight High Energy Images",
160
+ width=4,
161
+ block=15,
162
+ image_planes=44,
163
+ pixel_window_rows=9,
164
+ pixel_window_columns=30,
165
+ image_planes_per_packet=8,
76
166
  len_array=None,
77
167
  mantissa_bit_length=4,
78
168
  )
@@ -1,7 +1,6 @@
1
1
  """Generate ULTRA L1a CDFs."""
2
2
 
3
3
  import logging
4
- from typing import Optional
5
4
 
6
5
  import xarray as xr
7
6
 
@@ -24,14 +23,19 @@ from imap_processing.ultra.l0.ultra_utils import (
24
23
  ULTRA_ENERGY_RATES,
25
24
  ULTRA_ENERGY_SPECTRA,
26
25
  ULTRA_EVENTS,
26
+ ULTRA_EXTOF_HIGH_ANGULAR,
27
+ ULTRA_EXTOF_HIGH_ENERGY,
28
+ ULTRA_EXTOF_HIGH_TIME,
27
29
  ULTRA_HK,
28
30
  ULTRA_MACROS_CHECKSUM,
31
+ ULTRA_PHXTOF_HIGH_ANGULAR,
32
+ ULTRA_PHXTOF_HIGH_ENERGY,
33
+ ULTRA_PHXTOF_HIGH_TIME,
29
34
  ULTRA_PRI_1_EVENTS,
30
35
  ULTRA_PRI_2_EVENTS,
31
36
  ULTRA_PRI_3_EVENTS,
32
37
  ULTRA_PRI_4_EVENTS,
33
38
  ULTRA_RATES,
34
- ULTRA_TOF,
35
39
  )
36
40
  from imap_processing.utils import packet_file_to_datasets
37
41
 
@@ -39,7 +43,7 @@ logger = logging.getLogger(__name__)
39
43
 
40
44
 
41
45
  def ultra_l1a( # noqa: PLR0912
42
- packet_file: str, apid_input: Optional[int] = None
46
+ packet_file: str, apid_input: int | None = None
43
47
  ) -> list[xr.Dataset]:
44
48
  """
45
49
  Will process ULTRA L0 data into L1A CDF files at output_filepath.
@@ -87,6 +91,19 @@ def ultra_l1a( # noqa: PLR0912
87
91
  for i, apid in enumerate(group.apid)
88
92
  }
89
93
 
94
+ all_l1a_image_apids = {
95
+ apid: group
96
+ for group in [
97
+ ULTRA_PHXTOF_HIGH_ANGULAR,
98
+ ULTRA_PHXTOF_HIGH_ENERGY,
99
+ ULTRA_PHXTOF_HIGH_TIME,
100
+ ULTRA_EXTOF_HIGH_ANGULAR,
101
+ ULTRA_EXTOF_HIGH_TIME,
102
+ ULTRA_EXTOF_HIGH_ENERGY,
103
+ ]
104
+ for apid in group.apid
105
+ }
106
+
90
107
  # Update dataset global attributes
91
108
  attr_mgr = ImapCdfAttributes()
92
109
  attr_mgr.add_instrument_global_attrs("ultra")
@@ -96,9 +113,12 @@ def ultra_l1a( # noqa: PLR0912
96
113
  if apid in ULTRA_AUX.apid:
97
114
  decom_ultra_dataset = datasets_by_apid[apid]
98
115
  gattr_key = ULTRA_AUX.logical_source[ULTRA_AUX.apid.index(apid)]
99
- elif apid in ULTRA_TOF.apid:
100
- decom_ultra_dataset = process_ultra_tof(datasets_by_apid[apid])
101
- gattr_key = ULTRA_TOF.logical_source[ULTRA_TOF.apid.index(apid)]
116
+ elif apid in all_l1a_image_apids:
117
+ packet_props = all_l1a_image_apids[apid]
118
+ decom_ultra_dataset = process_ultra_tof(
119
+ datasets_by_apid[apid], packet_props
120
+ )
121
+ gattr_key = packet_props.logical_source[packet_props.apid.index(apid)]
102
122
  elif apid in ULTRA_RATES.apid:
103
123
  decom_ultra_dataset = process_ultra_rates(datasets_by_apid[apid])
104
124
  decom_ultra_dataset = decom_ultra_dataset.drop_vars("fastdata_00")
@@ -3,7 +3,7 @@
3
3
  import numpy as np
4
4
  import xarray as xr
5
5
 
6
- from imap_processing.quality_flags import ImapAttitudeUltraFlags, ImapRatesUltraFlags
6
+ from imap_processing.ultra.l1b.quality_flag_filters import SPIN_QUALITY_FLAG_FILTERS
7
7
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset, extract_data_dict
8
8
 
9
9
  FILLVAL_UINT16 = 65535
@@ -32,14 +32,17 @@ def calculate_cullingmask(extendedspin_dataset: xr.Dataset, name: str) -> xr.Dat
32
32
  good_mask = (
33
33
  (
34
34
  extendedspin_dataset["quality_attitude"]
35
- & ImapAttitudeUltraFlags.SPINRATE.value
35
+ & sum(flag.value for flag in SPIN_QUALITY_FLAG_FILTERS["quality_attitude"])
36
36
  )
37
37
  == 0
38
38
  ) & (
39
39
  (
40
40
  (
41
41
  extendedspin_dataset["quality_ena_rates"]
42
- & ImapRatesUltraFlags.HIGHRATES.value
42
+ & sum(
43
+ flag.value
44
+ for flag in SPIN_QUALITY_FLAG_FILTERS["quality_ena_rates"]
45
+ )
43
46
  )
44
47
  == 0
45
48
  ).all(dim="energy_bin_geometric_mean")