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
@@ -17,8 +17,9 @@ Examples
17
17
  l1a_data, l1a_evt_data, l1b_evt_data = PacketParser(l0_file)
18
18
  l1b_data = idex_l1b(l1a_data)
19
19
  l1a_data = idex_l2a(l1b_data)
20
- l2b_data = idex_l2b(l2a_data, [evt_data])
21
- write_cdf(l2b_data)
20
+ l2b_and_l2c_datasets = idex_l2b(l2a_data, [evt_data])
21
+ write_cdf(l2b_and_l2c_datasets[0])
22
+ write_cdf(l2b_and_l2c_datasets[1])
22
23
  """
23
24
 
24
25
  import collections
@@ -29,8 +30,12 @@ from datetime import datetime, timedelta
29
30
  import numpy as np
30
31
  import xarray as xr
31
32
 
33
+ from imap_processing.ena_maps.ena_maps import SkyTilingType
34
+ from imap_processing.ena_maps.utils.spatial_utils import AzElSkyGrid
32
35
  from imap_processing.idex.idex_constants import (
33
36
  FG_TO_KG,
37
+ IDEX_EVENT_REFERENCE_FRAME,
38
+ IDEX_SPACING_DEG,
34
39
  SECONDS_IN_DAY,
35
40
  IDEXEvtAcquireCodes,
36
41
  )
@@ -71,12 +76,20 @@ CHARGE_BIN_EDGES = np.array(
71
76
  )
72
77
  SPIN_PHASE_BIN_EDGES = np.array([0, 90, 180, 270, 360])
73
78
 
79
+ # Get the rectangular map grid with the specified spacing
80
+ SKY_GRID = AzElSkyGrid(IDEX_SPACING_DEG)
81
+ LON_BINS_EDGES = SKY_GRID.az_bin_edges
82
+ LAT_BINS_EDGES = SKY_GRID.el_bin_edges
83
+
74
84
 
75
85
  def idex_l2b(
76
86
  l2a_datasets: list[xr.Dataset], evt_datasets: list[xr.Dataset]
77
- ) -> xr.Dataset:
87
+ ) -> list[xr.Dataset]:
78
88
  """
79
- Will process IDEX l2a data to create l2b data products.
89
+ Will process IDEX l2a data to create l2b and l2c data products.
90
+
91
+ IDEX L2B processing creates L2b and L2c at the same time because L2c needs no
92
+ additional dependencies and is a natural extension of L2b processing.
80
93
 
81
94
  Parameters
82
95
  ----------
@@ -87,140 +100,254 @@ def idex_l2b(
87
100
 
88
101
  Returns
89
102
  -------
90
- l2b_dataset : xarray.Dataset
91
- The``xarray`` dataset containing the science data and supporting metadata.
103
+ list[xarray.Dataset]
104
+ The``xarray`` datasets containing the l2b and l2c science data and supporting
105
+ metadata.
92
106
  """
93
107
  logger.info(
94
- f"Running IDEX L2B processing on datasets: "
95
- f"{[ds.attrs['Logical_source'] for ds in l2a_datasets]}"
108
+ "Running IDEX L2B and L2C processing on L2a datasets. NOTE: L2C datasets are "
109
+ "processed at the same time as L2B datasets because L2C needs no additional "
110
+ "dependencies."
96
111
  )
97
-
98
112
  # create the attribute manager for this data level
99
- idex_attrs = get_idex_attrs("l2b")
113
+ idex_l2b_attrs = get_idex_attrs("l2b")
114
+ idex_l2c_attrs = get_idex_attrs("l2c")
100
115
  evt_dataset = xr.concat(evt_datasets, dim="epoch")
101
116
 
102
117
  # Concat all the l2a datasets together
103
118
  l2a_dataset = xr.concat(l2a_datasets, dim="epoch")
104
119
  epoch_doy_unique = np.unique(epoch_to_doy(l2a_dataset["epoch"].data))
105
- counts_by_charge, counts_by_mass, daily_epoch = compute_counts_by_charge_and_mass(
106
- l2a_dataset, epoch_doy_unique
107
- )
120
+ (
121
+ counts_by_charge,
122
+ counts_by_mass,
123
+ counts_by_charge_map,
124
+ counts_by_mass_map,
125
+ daily_epoch,
126
+ ) = compute_counts_by_charge_and_mass(l2a_dataset, epoch_doy_unique)
108
127
  # Get science acquisition percentage for each day
109
128
  daily_on_percentage = get_science_acquisition_on_percentage(evt_dataset)
110
- rate_by_charge, rate_by_mass, rate_quality_flags = compute_rates_by_charge_and_mass(
111
- counts_by_charge, counts_by_mass, epoch_doy_unique, daily_on_percentage
129
+ (
130
+ rate_by_charge,
131
+ rate_by_mass,
132
+ rate_by_charge_map,
133
+ rate_by_mass_map,
134
+ rate_quality_flags,
135
+ ) = compute_rates_by_charge_and_mass(
136
+ counts_by_charge,
137
+ counts_by_mass,
138
+ counts_by_charge_map,
139
+ counts_by_mass_map,
140
+ epoch_doy_unique,
141
+ daily_on_percentage,
112
142
  )
113
143
  # Create l2b Dataset
114
- charge_bins = np.arange(len(CHARGE_BIN_EDGES))
115
- mass_bins = np.arange(len(CHARGE_BIN_EDGES))
116
- spin_phase_bins = np.arange(len(SPIN_PHASE_BIN_EDGES) - 1)
144
+ charge_bin_means = np.sqrt(CHARGE_BIN_EDGES[:-1] * CHARGE_BIN_EDGES[1:])
145
+ mass_bin_means = np.sqrt(MASS_BIN_EDGES[:-1] * MASS_BIN_EDGES[1:])
146
+ spin_phase_means = (SPIN_PHASE_BIN_EDGES[:-1] + SPIN_PHASE_BIN_EDGES[1:]) / 2
147
+
148
+ # Define xarrays that are shared between l2b and l2c
117
149
  epoch = xr.DataArray(
118
150
  name="epoch",
119
151
  data=daily_epoch,
120
152
  dims="epoch",
121
- attrs=idex_attrs.get_variable_attributes("epoch", check_schema=False),
153
+ attrs=idex_l2b_attrs.get_variable_attributes("epoch", check_schema=False),
122
154
  )
123
- vars = {
155
+
156
+ common_vars = {
124
157
  "impact_day_of_year": xr.DataArray(
125
158
  name="impact_day_of_year",
126
159
  data=epoch_doy_unique,
127
160
  dims="epoch",
128
- attrs=idex_attrs.get_variable_attributes("impact_day_of_year"),
129
- ),
130
- "rate_calculation_quality_flags": xr.DataArray(
131
- name="rate_calculation_quality_flags",
132
- data=rate_quality_flags,
133
- dims="epoch",
134
- attrs=idex_attrs.get_variable_attributes("rate_calculation_quality_flags"),
161
+ attrs=idex_l2b_attrs.get_variable_attributes("impact_day_of_year"),
135
162
  ),
136
163
  "charge_labels": xr.DataArray(
137
164
  name="impact_charge_labels",
138
- data=charge_bins.astype(str),
139
- dims="impact_charge_bins",
140
- attrs=idex_attrs.get_variable_attributes(
165
+ data=charge_bin_means.astype(str),
166
+ dims="impact_charge",
167
+ attrs=idex_l2b_attrs.get_variable_attributes(
141
168
  "charge_labels", check_schema=False
142
169
  ),
143
170
  ),
144
- "spin_phase_labels": xr.DataArray(
145
- name="spin_phase_labels",
146
- data=spin_phase_bins.astype(str),
147
- dims="spin_phase_bins",
148
- attrs=idex_attrs.get_variable_attributes(
149
- "spin_phase_labels", check_schema=False
150
- ),
151
- ),
152
171
  "mass_labels": xr.DataArray(
153
172
  name="mass_labels",
154
- data=mass_bins.astype(str),
155
- dims="mass_bins",
156
- attrs=idex_attrs.get_variable_attributes("mass_labels", check_schema=False),
173
+ data=mass_bin_means.astype(str),
174
+ dims="mass",
175
+ attrs=idex_l2b_attrs.get_variable_attributes(
176
+ "mass_labels", check_schema=False
177
+ ),
157
178
  ),
158
- "impact_charge_bins": xr.DataArray(
159
- name="impact_charge_bins",
160
- data=charge_bins,
161
- dims="impact_charge_bins",
162
- attrs=idex_attrs.get_variable_attributes(
163
- "impact_charge_bins", check_schema=False
179
+ "impact_charge": xr.DataArray(
180
+ name="impact_charge",
181
+ data=charge_bin_means,
182
+ dims="impact_charge",
183
+ attrs=idex_l2b_attrs.get_variable_attributes(
184
+ "impact_charge", check_schema=False
164
185
  ),
165
186
  ),
166
- "mass_bins": xr.DataArray(
167
- name="mass_bins",
168
- data=mass_bins,
169
- dims="mass_bins",
170
- attrs=idex_attrs.get_variable_attributes("mass_bins", check_schema=False),
187
+ "mass": xr.DataArray(
188
+ name="mass",
189
+ data=mass_bin_means,
190
+ dims="mass",
191
+ attrs=idex_l2b_attrs.get_variable_attributes("mass", check_schema=False),
171
192
  ),
172
- "spin_phase_bins": xr.DataArray(
173
- name="spin_phase_bins",
174
- data=spin_phase_bins,
175
- dims="spin_phase_bins",
176
- attrs=idex_attrs.get_variable_attributes(
177
- "spin_phase_bins", check_schema=False
193
+ }
194
+ l2b_vars = common_vars | {
195
+ "spin_phase": xr.DataArray(
196
+ name="spin_phase",
197
+ data=spin_phase_means,
198
+ dims="spin_phase",
199
+ attrs=idex_l2b_attrs.get_variable_attributes(
200
+ "spin_phase", check_schema=False
201
+ ),
202
+ ),
203
+ "spin_phase_labels": xr.DataArray(
204
+ name="spin_phase_labels",
205
+ data=spin_phase_means.astype(str),
206
+ dims="spin_phase",
207
+ attrs=idex_l2b_attrs.get_variable_attributes(
208
+ "spin_phase_labels", check_schema=False
209
+ ),
210
+ ),
211
+ "rate_calculation_quality_flags": xr.DataArray(
212
+ name="rate_calculation_quality_flags",
213
+ data=rate_quality_flags,
214
+ dims="epoch",
215
+ attrs=idex_l2b_attrs.get_variable_attributes(
216
+ "rate_calculation_quality_flags"
178
217
  ),
179
218
  ),
180
219
  "counts_by_charge": xr.DataArray(
181
220
  name="counts_by_charge",
182
221
  data=counts_by_charge.astype(np.int64),
183
- dims=("epoch", "charge_bins", "spin_phase_bins"),
184
- attrs=idex_attrs.get_variable_attributes("counts_by_charge"),
222
+ dims=("epoch", "impact_charge", "spin_phase"),
223
+ attrs=idex_l2b_attrs.get_variable_attributes("counts_by_charge"),
185
224
  ),
186
225
  "counts_by_mass": xr.DataArray(
187
226
  name="counts_by_mass",
188
227
  data=counts_by_mass.astype(np.int64),
189
- dims=("epoch", "mass_bins", "spin_phase_bins"),
190
- attrs=idex_attrs.get_variable_attributes("counts_by_mass"),
228
+ dims=("epoch", "mass", "spin_phase"),
229
+ attrs=idex_l2b_attrs.get_variable_attributes("counts_by_mass"),
191
230
  ),
192
231
  "rate_by_charge": xr.DataArray(
193
232
  name="rate_by_charge",
194
233
  data=rate_by_charge,
195
- dims=("epoch", "charge_bins", "spin_phase_bins"),
196
- attrs=idex_attrs.get_variable_attributes("rate_by_charge"),
234
+ dims=("epoch", "impact_charge", "spin_phase"),
235
+ attrs=idex_l2b_attrs.get_variable_attributes("rate_by_charge"),
197
236
  ),
198
237
  "rate_by_mass": xr.DataArray(
199
238
  name="rate_by_mass",
200
239
  data=rate_by_mass,
201
- dims=("epoch", "mass_bins", "spin_phase_bins"),
202
- attrs=idex_attrs.get_variable_attributes("rate_by_mass"),
240
+ dims=("epoch", "mass", "spin_phase"),
241
+ attrs=idex_l2b_attrs.get_variable_attributes("rate_by_mass"),
203
242
  ),
204
243
  }
244
+ l2c_vars = common_vars | {
245
+ "rectangular_lon_pixel_label": xr.DataArray(
246
+ name="rectangular_lon_pixel_label",
247
+ data=SKY_GRID.az_bin_midpoints.astype(str),
248
+ dims="rectangular_lon_pixel",
249
+ attrs=idex_l2c_attrs.get_variable_attributes(
250
+ "rectangular_lon_pixel_label", check_schema=False
251
+ ),
252
+ ),
253
+ "rectangular_lat_pixel_label": xr.DataArray(
254
+ name="rectangular_lat_pixel_label",
255
+ data=SKY_GRID.el_bin_midpoints.astype(str),
256
+ dims="rectangular_lat_pixel",
257
+ attrs=idex_l2c_attrs.get_variable_attributes(
258
+ "rectangular_lat_pixel_label", check_schema=False
259
+ ),
260
+ ),
261
+ "rectangular_lon_pixel": xr.DataArray(
262
+ name="rectangular_lon_pixel",
263
+ data=SKY_GRID.az_bin_midpoints,
264
+ dims="rectangular_lon_pixel",
265
+ attrs=idex_l2c_attrs.get_variable_attributes(
266
+ "rectangular_lon_pixel", check_schema=False
267
+ ),
268
+ ),
269
+ "rectangular_lat_pixel": xr.DataArray(
270
+ name="rectangular_lat_pixel",
271
+ data=SKY_GRID.el_bin_midpoints,
272
+ dims="rectangular_lat_pixel",
273
+ attrs=idex_l2c_attrs.get_variable_attributes(
274
+ "rectangular_lat_pixel", check_schema=False
275
+ ),
276
+ ),
277
+ "counts_by_charge_map": xr.DataArray(
278
+ name="counts_by_charge_map",
279
+ data=counts_by_charge_map.astype(np.int64),
280
+ dims=(
281
+ "epoch",
282
+ "impact_charge",
283
+ "rectangular_lon_pixel",
284
+ "rectangular_lat_pixel",
285
+ ),
286
+ attrs=idex_l2c_attrs.get_variable_attributes("counts_by_charge_map"),
287
+ ),
288
+ "counts_by_mass_map": xr.DataArray(
289
+ name="counts_by_mass_map",
290
+ data=counts_by_mass_map.astype(np.int64),
291
+ dims=(
292
+ "epoch",
293
+ "mass",
294
+ "rectangular_lon_pixel",
295
+ "rectangular_lat_pixel",
296
+ ),
297
+ attrs=idex_l2c_attrs.get_variable_attributes("counts_by_mass_map"),
298
+ ),
299
+ "rate_by_charge_map": xr.DataArray(
300
+ name="rate_by_charge_map",
301
+ data=rate_by_charge_map,
302
+ dims=(
303
+ "epoch",
304
+ "impact_charge",
305
+ "rectangular_lon_pixel",
306
+ "rectangular_lat_pixel",
307
+ ),
308
+ attrs=idex_l2c_attrs.get_variable_attributes("rate_by_charge_map"),
309
+ ),
310
+ "rate_by_mass_map": xr.DataArray(
311
+ name="rate_by_mass_map",
312
+ data=rate_by_mass_map,
313
+ dims=(
314
+ "epoch",
315
+ "mass",
316
+ "rectangular_lon_pixel",
317
+ "rectangular_lat_pixel",
318
+ ),
319
+ attrs=idex_l2c_attrs.get_variable_attributes("rate_by_mass_map"),
320
+ ),
321
+ }
322
+
205
323
  l2b_dataset = xr.Dataset(
206
324
  coords={"epoch": epoch},
207
- data_vars=vars,
208
- attrs=idex_attrs.get_global_attributes("imap_idex_l2b_sci"),
325
+ data_vars=l2b_vars,
326
+ attrs=idex_l2b_attrs.get_global_attributes("imap_idex_l2b_sci"),
327
+ )
328
+ l2c_dataset = xr.Dataset(
329
+ coords={"epoch": epoch},
330
+ data_vars=l2c_vars,
209
331
  )
210
- # Copy longitude and latitude from the l2a dataset
211
- l2b_dataset["longitude"] = l2a_dataset["longitude"].copy()
212
- l2b_dataset["latitude"] = l2a_dataset["latitude"].copy()
332
+ # Add map attributes
333
+ map_attrs = {
334
+ "sky_tiling_type": SkyTilingType.RECTANGULAR.value,
335
+ "Spacing_degrees": str(IDEX_SPACING_DEG),
336
+ "Spice_reference_frame": IDEX_EVENT_REFERENCE_FRAME.name,
337
+ } | idex_l2c_attrs.get_global_attributes("imap_idex_l2c_sci-rectangular")
213
338
 
214
- logger.info("IDEX L2B science data processing completed.")
339
+ l2c_dataset.attrs.update(map_attrs)
215
340
 
216
- return l2b_dataset
341
+ logger.info("IDEX L2B and L2C science data processing completed.")
342
+
343
+ return [l2b_dataset, l2c_dataset]
217
344
 
218
345
 
219
346
  def compute_counts_by_charge_and_mass(
220
347
  l2a_dataset: xr.Dataset, epoch_doy_unique: np.ndarray
221
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
348
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
222
349
  """
223
- Compute the dust event counts by charge and mass by spin phase per day.
350
+ Compute the dust counts by charge and mass by spin phase or lon and lat per day.
224
351
 
225
352
  Parameters
226
353
  ----------
@@ -231,20 +358,17 @@ def compute_counts_by_charge_and_mass(
231
358
 
232
359
  Returns
233
360
  -------
234
- tuple[np.ndarray, np.ndarray, np.ndarray]
361
+ tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
235
362
  Two 3D arrays containing counts by charge or mass, and by spin phase for each
236
- dataset, and a 1D array of daily epoch values.
363
+ dataset, Two 4D arrays containing counts by charge or mass, and by lon and lat
364
+ for each dataset, and a 1D array of daily epoch values.
237
365
  """
238
- # Initialize arrays to hold counts.
239
- # There should be 4 spin phase bins, 11 charge bins, and 11 mass bins.
240
- # The first bin for charge and mass is for values below the first bin edge.
241
- counts_by_charge = np.zeros(
242
- (len(epoch_doy_unique), len(CHARGE_BIN_EDGES), len(SPIN_PHASE_BIN_EDGES) - 1),
243
- )
244
- counts_by_mass = np.zeros(
245
- (len(epoch_doy_unique), len(MASS_BIN_EDGES), len(SPIN_PHASE_BIN_EDGES) - 1),
246
- )
247
- daily_epoch = np.zeros(len(epoch_doy_unique))
366
+ # Initialize lists to hold counts.
367
+ counts_by_charge = []
368
+ counts_by_mass = []
369
+ counts_by_charge_map = []
370
+ counts_by_mass_map = []
371
+ daily_epoch = np.zeros(len(epoch_doy_unique), dtype=np.float64)
248
372
  for i in range(len(epoch_doy_unique)):
249
373
  doy = epoch_doy_unique[i]
250
374
  # Get the indices for the current day
@@ -258,39 +382,89 @@ def compute_counts_by_charge_and_mass(
258
382
  ]
259
383
  charge_vals = l2a_dataset["target_low_impact_charge"].data[current_day_indices]
260
384
  spin_phase_angles = l2a_dataset["spin_phase"].data[current_day_indices]
385
+ # Make sure longitude values are in the range [0, 360)
386
+ longitude = np.mod(l2a_dataset["longitude"].data[current_day_indices], 360)
387
+ latitude = l2a_dataset["latitude"].data[current_day_indices]
261
388
  # Convert units
262
- mass_vals = FG_TO_KG * np.array(mass_vals)
263
- # Bin masses
264
- binned_mass = np.array(np.digitize(mass_vals, bins=MASS_BIN_EDGES))
265
- # Bin charges
266
- binned_charge = np.array(np.digitize(charge_vals, bins=CHARGE_BIN_EDGES))
389
+ mass_vals = FG_TO_KG * np.atleast_1d(mass_vals)
267
390
  # Bin spin phases
268
391
  binned_spin_phase = bin_spin_phases(spin_phase_angles)
269
- # If the values in the array are beyond the bounds of bins, 0 or len(bins) it is
270
- # returned as such. In this case, the desired result is to place the values
271
- # beyond the last bin into the last bin and keep the values below the first bin.
272
- binned_charge[binned_charge == len(CHARGE_BIN_EDGES)] = (
273
- len(CHARGE_BIN_EDGES) - 1
392
+ # Clip arrays to ensure that the values are within the valid range of bins.
393
+ # Latitude should be binned with the right edge included. 90 is a valid latitude
394
+ latitude = np.clip(latitude, -90, 90)
395
+ mass_vals = np.clip(mass_vals, MASS_BIN_EDGES[0], MASS_BIN_EDGES[-1])
396
+ charge_vals = np.clip(charge_vals, CHARGE_BIN_EDGES[0], CHARGE_BIN_EDGES[-1])
397
+
398
+ counts_by_mass.append(
399
+ np.histogramdd(
400
+ np.column_stack([mass_vals, binned_spin_phase]),
401
+ bins=[MASS_BIN_EDGES, np.arange(5)],
402
+ )[0]
274
403
  )
275
- binned_mass[binned_mass == len(MASS_BIN_EDGES)] = len(MASS_BIN_EDGES) - 1
404
+ counts_by_charge.append(
405
+ np.histogramdd(
406
+ np.column_stack([charge_vals, binned_spin_phase]),
407
+ bins=[CHARGE_BIN_EDGES, np.arange(5)],
408
+ )[0]
409
+ )
410
+ counts_by_mass_map.append(
411
+ np.histogramdd(
412
+ np.column_stack([mass_vals, longitude, latitude]),
413
+ bins=[MASS_BIN_EDGES, LON_BINS_EDGES, LAT_BINS_EDGES],
414
+ )[0]
415
+ )
416
+ counts_by_charge_map.append(
417
+ np.histogramdd(
418
+ np.column_stack([charge_vals, longitude, latitude]),
419
+ bins=[CHARGE_BIN_EDGES, LON_BINS_EDGES, LAT_BINS_EDGES],
420
+ )[0]
421
+ )
422
+
423
+ return (
424
+ np.stack(counts_by_charge),
425
+ np.stack(counts_by_mass),
426
+ np.stack(counts_by_charge_map),
427
+ np.stack(counts_by_mass_map),
428
+ daily_epoch,
429
+ )
276
430
 
277
- # TODO use np.histogramdd to compute the counts by charge and mass.
278
- # Count dust events for each spin phase and mass bin or charge bin.
279
- for mass_bin, charge_bin, spin_phase_bin in zip(
280
- binned_mass, binned_charge, binned_spin_phase
281
- ):
282
- counts_by_mass[i, mass_bin, spin_phase_bin] += 1
283
- counts_by_charge[i, charge_bin, spin_phase_bin] += 1
284
431
 
285
- return counts_by_charge, counts_by_mass, daily_epoch
432
+ def compute_rates(
433
+ counts: np.ndarray, epoch_doy_percent_on: np.ndarray, non_zero_inds: np.ndarray
434
+ ) -> np.ndarray:
435
+ """
436
+ Compute the count rates given the percent uptime of IDEX.
437
+
438
+ Parameters
439
+ ----------
440
+ counts : np.ndarray
441
+ Count values for the dust events.
442
+ epoch_doy_percent_on : np.ndarray
443
+ Percentage of time science acquisition was on for each day of the year.
444
+ non_zero_inds : np.ndarray
445
+ Indices of the days with non-zero science acquisition percentage.
446
+
447
+ Returns
448
+ -------
449
+ np.ndarray
450
+ Count rates.
451
+ """
452
+ while len(epoch_doy_percent_on.shape) < len(counts.shape):
453
+ epoch_doy_percent_on = np.expand_dims(epoch_doy_percent_on, axis=-1)
454
+
455
+ return counts[non_zero_inds] / (
456
+ 0.01 * epoch_doy_percent_on[non_zero_inds] * SECONDS_IN_DAY
457
+ )
286
458
 
287
459
 
288
460
  def compute_rates_by_charge_and_mass(
289
461
  counts_by_charge: np.ndarray,
290
462
  counts_by_mass: np.ndarray,
463
+ counts_by_charge_map: np.ndarray,
464
+ counts_by_mass_map: np.ndarray,
291
465
  epoch_doy: np.ndarray,
292
466
  daily_on_percentage: dict,
293
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
467
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
294
468
  """
295
469
  Compute the dust event counts rates by charge and mass by spin phase for each day.
296
470
 
@@ -299,7 +473,11 @@ def compute_rates_by_charge_and_mass(
299
473
  counts_by_charge : np.ndarray
300
474
  3D array containing counts by charge and spin phase for each dataset.
301
475
  counts_by_mass : np.ndarray
302
- 3D array containing counts by mass and spin phase for each dataset.
476
+ 3D array containing counts by mass and lon and lat for each dataset.
477
+ counts_by_charge_map : np.ndarray
478
+ 4D array containing counts by charge and lon and lat for each dataset.
479
+ counts_by_mass_map : np.ndarray
480
+ 4D array containing counts by mass and spin phase for each dataset.
303
481
  epoch_doy : np.ndarray
304
482
  Unique days of year corresponding to the epochs in the dataset.
305
483
  daily_on_percentage : dict
@@ -314,6 +492,8 @@ def compute_rates_by_charge_and_mass(
314
492
  # Initialize arrays to hold rates.
315
493
  rate_by_charge = np.full(counts_by_charge.shape, -1.0)
316
494
  rate_by_mass = np.full(counts_by_mass.shape, -1.0)
495
+ rate_by_charge_map = np.full(counts_by_charge_map.shape, -1.0)
496
+ rate_by_mass_map = np.full(counts_by_mass_map.shape, -1.0)
317
497
  # Initialize an array to hold quality flags for each epoch. A quality flag of 0
318
498
  # indicates that there was no science acquisition data for that epoch, and the rate
319
499
  # is not valid. A quality flag of 1 indicates that the rate is valid.
@@ -336,18 +516,26 @@ def compute_rates_by_charge_and_mass(
336
516
  # acquisition time.
337
517
  non_zero_inds = np.where(epoch_doy_percent_on > 0)[0]
338
518
  # Compute rates only for days with non-zero science acquisition percentage
339
- rate_by_charge[non_zero_inds] = counts_by_charge[non_zero_inds] / (
340
- 0.01
341
- * epoch_doy_percent_on[non_zero_inds, np.newaxis, np.newaxis]
342
- * SECONDS_IN_DAY
519
+ rate_by_charge[non_zero_inds] = compute_rates(
520
+ counts_by_charge, epoch_doy_percent_on, non_zero_inds
521
+ )
522
+ rate_by_mass[non_zero_inds] = compute_rates(
523
+ counts_by_mass, epoch_doy_percent_on, non_zero_inds
524
+ )
525
+ rate_by_charge_map[non_zero_inds] = compute_rates(
526
+ counts_by_charge_map, epoch_doy_percent_on, non_zero_inds
343
527
  )
344
- rate_by_mass[non_zero_inds] = counts_by_mass[non_zero_inds] / (
345
- 0.01
346
- * epoch_doy_percent_on[non_zero_inds, np.newaxis, np.newaxis]
347
- * SECONDS_IN_DAY
528
+ rate_by_mass_map[non_zero_inds] = compute_rates(
529
+ counts_by_mass_map, epoch_doy_percent_on, non_zero_inds
348
530
  )
349
531
 
350
- return rate_by_charge, rate_by_mass, rate_quality_flags
532
+ return (
533
+ rate_by_charge,
534
+ rate_by_mass,
535
+ rate_by_charge_map,
536
+ rate_by_mass_map,
537
+ rate_quality_flags,
538
+ )
351
539
 
352
540
 
353
541
  def bin_spin_phases(spin_phases: xr.DataArray) -> np.ndarray:
@@ -370,7 +558,7 @@ def bin_spin_phases(spin_phases: xr.DataArray) -> np.ndarray:
370
558
  f"phase angle range, [0, 360)."
371
559
  )
372
560
  # Shift spin phases by +45° so that the first bin starts at 0°.
373
- # Use mod to wrap values > 360 to 0.
561
+ # Use mod to wrap values >= 360 to 0.
374
562
  shifted_spin_phases = (spin_phases + 45) % 360
375
563
  # Use np.digitize to find the bin index for each spin phase.
376
564
  bin_indices = np.digitize(shifted_spin_phases, SPIN_PHASE_BIN_EDGES, right=False)
@@ -419,7 +607,7 @@ def get_science_acquisition_timestamps(
419
607
  epochs = evt_dataset["epoch"][sc_indices].data
420
608
  # Now the state change values and check if it is either a science
421
609
  # acquisition start or science acquisition stop event.
422
- for v1, v2, epoch in zip(val1, val2, epochs):
610
+ for v1, v2, epoch in zip(val1, val2, epochs, strict=False):
423
611
  # An "acquire" start will have val1=ACQSETUP and val2=ACQ
424
612
  # An "acquire" stop will have val1=ACQ and val2=CHILL
425
613
  if (v1, v2) == (IDEXEvtAcquireCodes.ACQSETUP, IDEXEvtAcquireCodes.ACQ):
@@ -458,6 +646,12 @@ def get_science_acquisition_on_percentage(evt_dataset: xr.Dataset) -> dict:
458
646
  """
459
647
  # Get science acquisition start and stop times
460
648
  evt_logs, evt_time, evt_values = get_science_acquisition_timestamps(evt_dataset)
649
+ if len(evt_time) == 0:
650
+ logger.warning(
651
+ "No science acquisition events found in event dataset. Returning empty "
652
+ "uptime percentages. All rate variables will be set to -1."
653
+ )
654
+ return {}
461
655
  # Track total and 'on' durations per day
462
656
  daily_totals: collections.defaultdict = defaultdict(timedelta)
463
657
  daily_on: collections.defaultdict = defaultdict(timedelta)
@@ -1,7 +1,5 @@
1
1
  """Contains helper functions to support IDEX processing."""
2
2
 
3
- from typing import Optional
4
-
5
3
  import xarray as xr
6
4
 
7
5
  from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
@@ -31,7 +29,7 @@ def setup_dataset(
31
29
  dataset: xr.Dataset,
32
30
  match_strings: list,
33
31
  idex_attrs: ImapCdfAttributes,
34
- data_vars: Optional[dict] = None,
32
+ data_vars: dict | None = None,
35
33
  ) -> xr.Dataset:
36
34
  """
37
35
  Initialize a dataset and copy over any dataArrays.
@@ -13,3 +13,4 @@ class LoAPID(IntEnum):
13
13
  ILO_SCI_DE = 706 # Science direct event data
14
14
  ILO_STAR = 707 # Science star sensor data, every spin
15
15
  ILO_SPIN = 708 # Spin information for each science cycle (28 spins)
16
+ ILO_DIAG_PCC = 725 # Diagnostic pivot platform information