imap-processing 0.17.0__py3-none-any.whl → 0.18.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 (89) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ccsds/excel_to_xtce.py +12 -0
  3. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -6
  4. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +11 -0
  5. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +11 -0
  6. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +24 -0
  7. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +163 -100
  8. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +4 -4
  9. imap_processing/cdf/config/imap_ialirt_l1_variable_attrs.yaml +97 -54
  10. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +119 -36
  11. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +16 -90
  12. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +30 -0
  13. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +15 -1
  14. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +60 -0
  15. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +91 -11
  16. imap_processing/cli.py +28 -5
  17. imap_processing/codice/codice_l1a.py +36 -48
  18. imap_processing/codice/codice_l1b.py +1 -1
  19. imap_processing/codice/codice_l2.py +0 -9
  20. imap_processing/codice/constants.py +481 -498
  21. imap_processing/hit/l0/decom_hit.py +2 -2
  22. imap_processing/hit/l1a/hit_l1a.py +64 -24
  23. imap_processing/hit/l1b/constants.py +5 -0
  24. imap_processing/hit/l1b/hit_l1b.py +18 -16
  25. imap_processing/hit/l2/constants.py +1 -1
  26. imap_processing/hit/l2/hit_l2.py +4 -5
  27. imap_processing/ialirt/constants.py +21 -0
  28. imap_processing/ialirt/generate_coverage.py +188 -0
  29. imap_processing/ialirt/l0/parse_mag.py +62 -5
  30. imap_processing/ialirt/l0/process_swapi.py +1 -1
  31. imap_processing/ialirt/l0/process_swe.py +23 -7
  32. imap_processing/ialirt/utils/constants.py +22 -16
  33. imap_processing/ialirt/utils/create_xarray.py +42 -19
  34. imap_processing/idex/idex_constants.py +1 -5
  35. imap_processing/idex/idex_l2b.py +246 -67
  36. imap_processing/idex/idex_l2c.py +30 -196
  37. imap_processing/lo/l0/lo_apid.py +1 -0
  38. imap_processing/lo/l1a/lo_l1a.py +44 -0
  39. imap_processing/lo/packet_definitions/lo_xtce.xml +5359 -106
  40. imap_processing/mag/constants.py +1 -0
  41. imap_processing/mag/l1d/__init__.py +0 -0
  42. imap_processing/mag/l1d/mag_l1d.py +133 -0
  43. imap_processing/mag/l1d/mag_l1d_data.py +588 -0
  44. imap_processing/mag/l2/__init__.py +0 -0
  45. imap_processing/mag/l2/mag_l2.py +25 -20
  46. imap_processing/mag/l2/mag_l2_data.py +191 -130
  47. imap_processing/quality_flags.py +20 -2
  48. imap_processing/spice/geometry.py +25 -3
  49. imap_processing/spice/pointing_frame.py +1 -1
  50. imap_processing/spice/spin.py +4 -0
  51. imap_processing/spice/time.py +51 -0
  52. imap_processing/swapi/l2/swapi_l2.py +52 -8
  53. imap_processing/swapi/swapi_utils.py +1 -1
  54. imap_processing/swe/l1b/swe_l1b.py +2 -4
  55. imap_processing/ultra/constants.py +49 -1
  56. imap_processing/ultra/l0/decom_tools.py +15 -8
  57. imap_processing/ultra/l0/decom_ultra.py +35 -11
  58. imap_processing/ultra/l0/ultra_utils.py +97 -5
  59. imap_processing/ultra/l1a/ultra_l1a.py +25 -4
  60. imap_processing/ultra/l1b/cullingmask.py +3 -3
  61. imap_processing/ultra/l1b/de.py +53 -15
  62. imap_processing/ultra/l1b/extendedspin.py +26 -2
  63. imap_processing/ultra/l1b/lookup_utils.py +171 -50
  64. imap_processing/ultra/l1b/quality_flag_filters.py +14 -0
  65. imap_processing/ultra/l1b/ultra_l1b_culling.py +198 -5
  66. imap_processing/ultra/l1b/ultra_l1b_extended.py +304 -66
  67. imap_processing/ultra/l1c/helio_pset.py +54 -7
  68. imap_processing/ultra/l1c/spacecraft_pset.py +9 -1
  69. imap_processing/ultra/l1c/ultra_l1c.py +2 -0
  70. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +106 -109
  71. imap_processing/ultra/utils/ultra_l1_utils.py +13 -1
  72. {imap_processing-0.17.0.dist-info → imap_processing-0.18.0.dist-info}/METADATA +2 -2
  73. {imap_processing-0.17.0.dist-info → imap_processing-0.18.0.dist-info}/RECORD +76 -83
  74. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +0 -526
  75. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +0 -526
  76. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +0 -526
  77. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +0 -524
  78. imap_processing/ultra/lookup_tables/EgyNorm.mem.csv +0 -32769
  79. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  80. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  81. imap_processing/ultra/lookup_tables/dps_grid45_compressed.cdf +0 -0
  82. imap_processing/ultra/lookup_tables/ultra45_back-pos-luts.csv +0 -4097
  83. imap_processing/ultra/lookup_tables/ultra45_tdc_norm.csv +0 -2050
  84. imap_processing/ultra/lookup_tables/ultra90_back-pos-luts.csv +0 -4097
  85. imap_processing/ultra/lookup_tables/ultra90_tdc_norm.csv +0 -2050
  86. imap_processing/ultra/lookup_tables/yadjust.csv +0 -257
  87. {imap_processing-0.17.0.dist-info → imap_processing-0.18.0.dist-info}/LICENSE +0 -0
  88. {imap_processing-0.17.0.dist-info → imap_processing-0.18.0.dist-info}/WHEEL +0 -0
  89. {imap_processing-0.17.0.dist-info → imap_processing-0.18.0.dist-info}/entry_points.txt +0 -0
@@ -48,6 +48,13 @@ def create_xarray_from_records(records: list[dict]) -> xr.Dataset: # noqa: PLR0
48
48
  attrs=cdf_manager.get_variable_attributes("component", check_schema=False),
49
49
  )
50
50
 
51
+ rtn_component = xr.DataArray(
52
+ ["radial", "tangential", "normal"],
53
+ name="RTN_component",
54
+ dims=["RTN_component"],
55
+ attrs=cdf_manager.get_variable_attributes("RTN_componentt", check_schema=False),
56
+ )
57
+
51
58
  esa_step = xr.DataArray(
52
59
  data=np.arange(8, dtype=np.uint8),
53
60
  name="esa_step",
@@ -57,32 +64,39 @@ def create_xarray_from_records(records: list[dict]) -> xr.Dataset: # noqa: PLR0
57
64
 
58
65
  energy_ranges = xr.DataArray(
59
66
  data=np.arange(15, dtype=np.uint8),
60
- name="energy_ranges",
61
- dims=["energy_ranges"],
62
- attrs=cdf_manager.get_variable_attributes("energy_ranges", check_schema=False),
67
+ name="codice_hi_h_energy_ranges",
68
+ dims=["codice_hi_h_energy_ranges"],
69
+ attrs=cdf_manager.get_variable_attributes(
70
+ "codice_hi_h_energy_ranges", check_schema=False
71
+ ),
63
72
  )
64
73
 
65
- azimuth = xr.DataArray(
74
+ elevation = xr.DataArray(
66
75
  data=np.arange(4, dtype=np.uint8),
67
- name="azimuth",
68
- dims=["azimuth"],
69
- attrs=cdf_manager.get_variable_attributes("azimuth", check_schema=False),
76
+ name="codice_hi_h_elevation",
77
+ dims=["codice_hi_h_elevation"],
78
+ attrs=cdf_manager.get_variable_attributes(
79
+ "codice_hi_h_elevation", check_schema=False
80
+ ),
70
81
  )
71
82
 
72
- spin_angle_bin = xr.DataArray(
83
+ spin_angle = xr.DataArray(
73
84
  data=np.arange(4, dtype=np.uint8),
74
- name="spin_angle_bin",
75
- dims=["spin_angle_bin"],
76
- attrs=cdf_manager.get_variable_attributes("spin_angle_bin", check_schema=False),
85
+ name="codice_hi_h_spin_angle",
86
+ dims=["codice_hi_h_spin_angle"],
87
+ attrs=cdf_manager.get_variable_attributes(
88
+ "codice_hi_h_spin_anglen", check_schema=False
89
+ ),
77
90
  )
78
91
 
79
92
  coords = {
80
93
  "epoch": epoch,
81
94
  "component": component,
95
+ "RTN_component": rtn_component,
82
96
  "esa_step": esa_step,
83
- "energy_ranges": energy_ranges,
84
- "azimuth": azimuth,
85
- "spin_angle_bin": spin_angle_bin,
97
+ "codice_hi_h_energy_ranges": energy_ranges,
98
+ "codice_hi_h_elevation": elevation,
99
+ "codice_hi_h_spin_angle": spin_angle,
86
100
  }
87
101
  dataset = xr.Dataset(
88
102
  coords=coords,
@@ -93,13 +107,22 @@ def create_xarray_from_records(records: list[dict]) -> xr.Dataset: # noqa: PLR0
93
107
  for key in instrument_keys:
94
108
  attrs = cdf_manager.get_variable_attributes(key, check_schema=False)
95
109
  fillval = attrs.get("FILLVAL")
96
- if key.startswith("mag"):
110
+ if key in ["mag_B_GSE", "mag_B_GSM"]:
97
111
  data = np.full((n, 3), fillval, dtype=np.float32)
98
112
  dims = ["epoch", "component"]
99
113
  dataset[key] = xr.DataArray(data, dims=dims, attrs=attrs)
100
- elif key.startswith("codicehi"):
114
+ elif key == "mag_B_RTN":
115
+ data = np.full((n, 3), fillval, dtype=np.float32)
116
+ dims = ["epoch", "RTN_component"]
117
+ dataset[key] = xr.DataArray(data, dims=dims, attrs=attrs)
118
+ elif key.startswith("codice_hi"):
101
119
  data = np.full((n, 15, 4, 4), fillval, dtype=np.float32)
102
- dims = ["epoch", "energy", "azimuth", "spin_angle_bin"]
120
+ dims = [
121
+ "epoch",
122
+ "codice_hi_h_energy_ranges",
123
+ "codice_hi_h_elevation",
124
+ "codice_hi_h_spin_angle",
125
+ ]
103
126
  dataset[key] = xr.DataArray(data, dims=dims, attrs=attrs)
104
127
  elif key == "swe_counterstreaming_electrons":
105
128
  data = np.full(n, fillval, dtype=np.uint8)
@@ -123,11 +146,11 @@ def create_xarray_from_records(records: list[dict]) -> xr.Dataset: # noqa: PLR0
123
146
  for key, val in record.items():
124
147
  if key in ["apid", "met", "met_in_utc", "ttj2000ns"]:
125
148
  continue
126
- elif key.startswith("mag"):
149
+ elif key in ["mag_B_GSE", "mag_B_GSM", "mag_B_RTN"]:
127
150
  dataset[key].data[i, :] = val
128
151
  elif key.startswith("swe_normalized_counts"):
129
152
  dataset[key].data[i, :] = val
130
- elif key.startswith("codicehi"):
153
+ elif key.startswith("codice_hi"):
131
154
  dataset[key].data[i, :, :, :] = val
132
155
  else:
133
156
  dataset[key].data[i] = val
@@ -82,13 +82,9 @@ SPICE_ARRAYS = [
82
82
  "spin_phase",
83
83
  ]
84
84
 
85
- # Default IDEX Healpix parameters
86
- # Used in IDEX l2c processing
87
- IDEX_HEALPIX_NSIDE = 8
88
- IDEX_HEALPIX_NESTED = False
89
85
  # Default IDEX Rectangular parameters
90
86
  # Used in IDEX l2c processing
91
- IDEX_SPACING_DEG = 4 # TODO
87
+ IDEX_SPACING_DEG = 6
92
88
 
93
89
  # Define the pointing reference frame for IDEX
94
90
  IDEX_EVENT_REFERENCE_FRAME = SpiceFrame.ECLIPJ2000
@@ -29,8 +29,10 @@ from datetime import datetime, timedelta
29
29
  import numpy as np
30
30
  import xarray as xr
31
31
 
32
+ from imap_processing.ena_maps.utils.spatial_utils import AzElSkyGrid
32
33
  from imap_processing.idex.idex_constants import (
33
34
  FG_TO_KG,
35
+ IDEX_SPACING_DEG,
34
36
  SECONDS_IN_DAY,
35
37
  IDEXEvtAcquireCodes,
36
38
  )
@@ -71,6 +73,11 @@ CHARGE_BIN_EDGES = np.array(
71
73
  )
72
74
  SPIN_PHASE_BIN_EDGES = np.array([0, 90, 180, 270, 360])
73
75
 
76
+ # Get the rectangular map grid with the specified spacing
77
+ SKY_GRID = AzElSkyGrid(IDEX_SPACING_DEG)
78
+ LON_BINS_EDGES = SKY_GRID.az_bin_edges
79
+ LAT_BINS_EDGES = SKY_GRID.el_bin_edges
80
+
74
81
 
75
82
  def idex_l2b(
76
83
  l2a_datasets: list[xr.Dataset], evt_datasets: list[xr.Dataset]
@@ -102,17 +109,32 @@ def idex_l2b(
102
109
  # Concat all the l2a datasets together
103
110
  l2a_dataset = xr.concat(l2a_datasets, dim="epoch")
104
111
  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
- )
112
+ (
113
+ counts_by_charge,
114
+ counts_by_mass,
115
+ counts_by_charge_map,
116
+ counts_by_mass_map,
117
+ daily_epoch,
118
+ ) = compute_counts_by_charge_and_mass(l2a_dataset, epoch_doy_unique)
108
119
  # Get science acquisition percentage for each day
109
120
  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
121
+ (
122
+ rate_by_charge,
123
+ rate_by_mass,
124
+ rate_by_charge_map,
125
+ rate_by_mass_map,
126
+ rate_quality_flags,
127
+ ) = compute_rates_by_charge_and_mass(
128
+ counts_by_charge,
129
+ counts_by_mass,
130
+ counts_by_charge_map,
131
+ counts_by_mass_map,
132
+ epoch_doy_unique,
133
+ daily_on_percentage,
112
134
  )
113
135
  # Create l2b Dataset
114
- charge_bins = np.arange(len(CHARGE_BIN_EDGES))
115
- mass_bins = np.arange(len(CHARGE_BIN_EDGES))
136
+ charge_bins = np.arange(len(CHARGE_BIN_EDGES) - 1)
137
+ mass_bins = np.arange(len(CHARGE_BIN_EDGES) - 1)
116
138
  spin_phase_bins = np.arange(len(SPIN_PHASE_BIN_EDGES) - 1)
117
139
  epoch = xr.DataArray(
118
140
  name="epoch",
@@ -120,7 +142,7 @@ def idex_l2b(
120
142
  dims="epoch",
121
143
  attrs=idex_attrs.get_variable_attributes("epoch", check_schema=False),
122
144
  )
123
- vars = {
145
+ data_vars = {
124
146
  "impact_day_of_year": xr.DataArray(
125
147
  name="impact_day_of_year",
126
148
  data=epoch_doy_unique,
@@ -136,7 +158,7 @@ def idex_l2b(
136
158
  "charge_labels": xr.DataArray(
137
159
  name="impact_charge_labels",
138
160
  data=charge_bins.astype(str),
139
- dims="impact_charge_bins",
161
+ dims="impact_charge",
140
162
  attrs=idex_attrs.get_variable_attributes(
141
163
  "charge_labels", check_schema=False
142
164
  ),
@@ -144,7 +166,7 @@ def idex_l2b(
144
166
  "spin_phase_labels": xr.DataArray(
145
167
  name="spin_phase_labels",
146
168
  data=spin_phase_bins.astype(str),
147
- dims="spin_phase_bins",
169
+ dims="spin_phase",
148
170
  attrs=idex_attrs.get_variable_attributes(
149
171
  "spin_phase_labels", check_schema=False
150
172
  ),
@@ -152,64 +174,135 @@ def idex_l2b(
152
174
  "mass_labels": xr.DataArray(
153
175
  name="mass_labels",
154
176
  data=mass_bins.astype(str),
155
- dims="mass_bins",
177
+ dims="mass",
156
178
  attrs=idex_attrs.get_variable_attributes("mass_labels", check_schema=False),
157
179
  ),
158
- "impact_charge_bins": xr.DataArray(
159
- name="impact_charge_bins",
180
+ "rectangular_lon_pixel_label": xr.DataArray(
181
+ name="rectangular_lon_pixel_label",
182
+ data=SKY_GRID.az_bin_midpoints.astype(str),
183
+ dims="rectangular_lon_pixel",
184
+ attrs=idex_attrs.get_variable_attributes(
185
+ "rectangular_lon_pixel_label", check_schema=False
186
+ ),
187
+ ),
188
+ "rectangular_lat_pixel_label": xr.DataArray(
189
+ name="rectangular_lat_pixel_label",
190
+ data=SKY_GRID.el_bin_midpoints.astype(str),
191
+ dims="rectangular_lat_pixel",
192
+ attrs=idex_attrs.get_variable_attributes(
193
+ "rectangular_lat_pixel_label", check_schema=False
194
+ ),
195
+ ),
196
+ "impact_charge": xr.DataArray(
197
+ name="impact_charge",
160
198
  data=charge_bins,
161
- dims="impact_charge_bins",
199
+ dims="impact_charge",
162
200
  attrs=idex_attrs.get_variable_attributes(
163
- "impact_charge_bins", check_schema=False
201
+ "impact_charge", check_schema=False
164
202
  ),
165
203
  ),
166
- "mass_bins": xr.DataArray(
167
- name="mass_bins",
204
+ "mass": xr.DataArray(
205
+ name="mass",
168
206
  data=mass_bins,
169
- dims="mass_bins",
170
- attrs=idex_attrs.get_variable_attributes("mass_bins", check_schema=False),
207
+ dims="mass",
208
+ attrs=idex_attrs.get_variable_attributes("mass", check_schema=False),
171
209
  ),
172
- "spin_phase_bins": xr.DataArray(
173
- name="spin_phase_bins",
210
+ "spin_phase": xr.DataArray(
211
+ name="spin_phase",
174
212
  data=spin_phase_bins,
175
- dims="spin_phase_bins",
213
+ dims="spin_phase",
214
+ attrs=idex_attrs.get_variable_attributes("spin_phase", check_schema=False),
215
+ ),
216
+ "rectangular_lon_pixel": xr.DataArray(
217
+ name="rectangular_lon_pixel",
218
+ data=SKY_GRID.az_bin_midpoints,
219
+ dims="rectangular_lon_pixel",
220
+ attrs=idex_attrs.get_variable_attributes(
221
+ "rectangular_lon_pixel", check_schema=False
222
+ ),
223
+ ),
224
+ "rectangular_lat_pixel": xr.DataArray(
225
+ name="rectangular_lat_pixel",
226
+ data=SKY_GRID.el_bin_midpoints,
227
+ dims="rectangular_lat_pixel",
176
228
  attrs=idex_attrs.get_variable_attributes(
177
- "spin_phase_bins", check_schema=False
229
+ "rectangular_lat_pixel", check_schema=False
178
230
  ),
179
231
  ),
180
232
  "counts_by_charge": xr.DataArray(
181
233
  name="counts_by_charge",
182
234
  data=counts_by_charge.astype(np.int64),
183
- dims=("epoch", "charge_bins", "spin_phase_bins"),
235
+ dims=("epoch", "impact_charge", "spin_phase"),
184
236
  attrs=idex_attrs.get_variable_attributes("counts_by_charge"),
185
237
  ),
186
238
  "counts_by_mass": xr.DataArray(
187
239
  name="counts_by_mass",
188
240
  data=counts_by_mass.astype(np.int64),
189
- dims=("epoch", "mass_bins", "spin_phase_bins"),
241
+ dims=("epoch", "mass", "spin_phase"),
190
242
  attrs=idex_attrs.get_variable_attributes("counts_by_mass"),
191
243
  ),
192
244
  "rate_by_charge": xr.DataArray(
193
245
  name="rate_by_charge",
194
246
  data=rate_by_charge,
195
- dims=("epoch", "charge_bins", "spin_phase_bins"),
247
+ dims=("epoch", "impact_charge", "spin_phase"),
196
248
  attrs=idex_attrs.get_variable_attributes("rate_by_charge"),
197
249
  ),
198
250
  "rate_by_mass": xr.DataArray(
199
251
  name="rate_by_mass",
200
252
  data=rate_by_mass,
201
- dims=("epoch", "mass_bins", "spin_phase_bins"),
253
+ dims=("epoch", "mass", "spin_phase"),
202
254
  attrs=idex_attrs.get_variable_attributes("rate_by_mass"),
203
255
  ),
256
+ "counts_by_charge_map": xr.DataArray(
257
+ name="counts_by_charge_map",
258
+ data=counts_by_charge_map.astype(np.int64),
259
+ dims=(
260
+ "epoch",
261
+ "impact_charge",
262
+ "rectangular_lon_pixel",
263
+ "rectangular_lat_pixel",
264
+ ),
265
+ attrs=idex_attrs.get_variable_attributes("counts_by_charge_map"),
266
+ ),
267
+ "counts_by_mass_map": xr.DataArray(
268
+ name="counts_by_mass_map",
269
+ data=counts_by_mass_map.astype(np.int64),
270
+ dims=(
271
+ "epoch",
272
+ "mass",
273
+ "rectangular_lon_pixel",
274
+ "rectangular_lat_pixel",
275
+ ),
276
+ attrs=idex_attrs.get_variable_attributes("counts_by_mass_map"),
277
+ ),
278
+ "rate_by_charge_map": xr.DataArray(
279
+ name="rate_by_charge_map",
280
+ data=rate_by_charge_map,
281
+ dims=(
282
+ "epoch",
283
+ "impact_charge",
284
+ "rectangular_lon_pixel",
285
+ "rectangular_lat_pixel",
286
+ ),
287
+ attrs=idex_attrs.get_variable_attributes("rate_by_charge_map"),
288
+ ),
289
+ "rate_by_mass_map": xr.DataArray(
290
+ name="rate_by_mass_map",
291
+ data=rate_by_mass_map,
292
+ dims=(
293
+ "epoch",
294
+ "mass",
295
+ "rectangular_lon_pixel",
296
+ "rectangular_lat_pixel",
297
+ ),
298
+ attrs=idex_attrs.get_variable_attributes("rate_by_mass_map"),
299
+ ),
204
300
  }
205
301
  l2b_dataset = xr.Dataset(
206
302
  coords={"epoch": epoch},
207
- data_vars=vars,
303
+ data_vars=data_vars,
208
304
  attrs=idex_attrs.get_global_attributes("imap_idex_l2b_sci"),
209
305
  )
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()
213
306
 
214
307
  logger.info("IDEX L2B science data processing completed.")
215
308
 
@@ -218,9 +311,9 @@ def idex_l2b(
218
311
 
219
312
  def compute_counts_by_charge_and_mass(
220
313
  l2a_dataset: xr.Dataset, epoch_doy_unique: np.ndarray
221
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
314
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
222
315
  """
223
- Compute the dust event counts by charge and mass by spin phase per day.
316
+ Compute the dust counts by charge and mass by spin phase or lon and lat per day.
224
317
 
225
318
  Parameters
226
319
  ----------
@@ -231,20 +324,43 @@ def compute_counts_by_charge_and_mass(
231
324
 
232
325
  Returns
233
326
  -------
234
- tuple[np.ndarray, np.ndarray, np.ndarray]
327
+ tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
235
328
  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.
329
+ dataset, Two 4D arrays containing counts by charge or mass, and by lon and lat
330
+ for each dataset, and a 1D array of daily epoch values.
237
331
  """
238
332
  # Initialize arrays to hold counts.
239
- # There should be 4 spin phase bins, 11 charge bins, and 11 mass bins.
333
+ # There should be 4 spin phase bins, 10 charge bins, and 10 mass bins.
240
334
  # The first bin for charge and mass is for values below the first bin edge.
241
335
  counts_by_charge = np.zeros(
242
- (len(epoch_doy_unique), len(CHARGE_BIN_EDGES), len(SPIN_PHASE_BIN_EDGES) - 1),
336
+ (
337
+ len(epoch_doy_unique),
338
+ len(CHARGE_BIN_EDGES) - 1,
339
+ len(SPIN_PHASE_BIN_EDGES) - 1,
340
+ ),
243
341
  )
244
342
  counts_by_mass = np.zeros(
245
- (len(epoch_doy_unique), len(MASS_BIN_EDGES), len(SPIN_PHASE_BIN_EDGES) - 1),
343
+ (len(epoch_doy_unique), len(MASS_BIN_EDGES) - 1, len(SPIN_PHASE_BIN_EDGES) - 1),
344
+ )
345
+ # Initialize arrays to hold count maps. Each map is a 3 or 4D array with shape
346
+ # (epoch, 10 [charge or mass], 60 [longitude bins], 30 [latitude bins]).
347
+ counts_by_charge_map = np.zeros(
348
+ (
349
+ len(epoch_doy_unique),
350
+ len(CHARGE_BIN_EDGES) - 1,
351
+ len(LON_BINS_EDGES) - 1,
352
+ len(LAT_BINS_EDGES) - 1,
353
+ ),
354
+ )
355
+ counts_by_mass_map = np.zeros(
356
+ (
357
+ len(epoch_doy_unique),
358
+ len(MASS_BIN_EDGES) - 1,
359
+ len(LON_BINS_EDGES) - 1,
360
+ len(LAT_BINS_EDGES) - 1,
361
+ ),
246
362
  )
247
- daily_epoch = np.zeros(len(epoch_doy_unique))
363
+ daily_epoch = np.zeros(len(epoch_doy_unique), dtype=np.float64)
248
364
  for i in range(len(epoch_doy_unique)):
249
365
  doy = epoch_doy_unique[i]
250
366
  # Get the indices for the current day
@@ -258,39 +374,88 @@ def compute_counts_by_charge_and_mass(
258
374
  ]
259
375
  charge_vals = l2a_dataset["target_low_impact_charge"].data[current_day_indices]
260
376
  spin_phase_angles = l2a_dataset["spin_phase"].data[current_day_indices]
377
+ # Make sure longitude values are in the range [0, 360)
378
+ longitude = np.mod(l2a_dataset["longitude"].data[current_day_indices], 360)
379
+ latitude = l2a_dataset["latitude"].data[current_day_indices]
261
380
  # Convert units
262
- mass_vals = FG_TO_KG * np.array(mass_vals)
381
+ mass_vals = FG_TO_KG * np.atleast_1d(mass_vals)
263
382
  # Bin masses
264
- binned_mass = np.array(np.digitize(mass_vals, bins=MASS_BIN_EDGES))
383
+ binned_mass = np.asarray(np.digitize(mass_vals, bins=MASS_BIN_EDGES))
265
384
  # Bin charges
266
- binned_charge = np.array(np.digitize(charge_vals, bins=CHARGE_BIN_EDGES))
385
+ binned_charge = np.asarray(np.digitize(charge_vals, bins=CHARGE_BIN_EDGES))
267
386
  # Bin spin phases
268
387
  binned_spin_phase = bin_spin_phases(spin_phase_angles)
388
+ # Bin longitude and latitude into the rectangular grid.
389
+ binned_longitude = np.asarray(np.digitize(longitude, bins=LON_BINS_EDGES))
390
+ # Latitude should be binned with the right edge included. 90 is a valid latitude
391
+ binned_latitude = np.asarray(np.digitize(latitude, bins=LAT_BINS_EDGES))
392
+ # Clip latitude value above the right edge to be in the last bin
393
+ binned_latitude = np.clip(binned_latitude, 1, len(LAT_BINS_EDGES) - 1)
269
394
  # If the values in the array are beyond the bounds of bins, 0 or len(bins) it is
270
395
  # 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
274
- )
275
- binned_mass[binned_mass == len(MASS_BIN_EDGES)] = len(MASS_BIN_EDGES) - 1
276
-
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
396
+ # beyond the first or last bin into the first or last bin, respectively.
397
+ binned_charge = np.clip(binned_charge, 1, len(CHARGE_BIN_EDGES) - 1)
398
+ binned_mass = np.clip(binned_mass, 1, len(MASS_BIN_EDGES) - 1)
399
+
400
+ # Count dust events for each spin phase, mass bin, charge bin, and bin into
401
+ # a rectangular grid
402
+ for mass_bin, charge_bin, spin_phase_bin, lon_bin, lat_bin in zip(
403
+ binned_mass,
404
+ binned_charge,
405
+ binned_spin_phase,
406
+ binned_longitude,
407
+ binned_latitude,
281
408
  ):
282
- counts_by_mass[i, mass_bin, spin_phase_bin] += 1
283
- counts_by_charge[i, charge_bin, spin_phase_bin] += 1
409
+ counts_by_mass[i, mass_bin - 1, spin_phase_bin] += 1
410
+ counts_by_charge[i, charge_bin - 1, spin_phase_bin] += 1
411
+ counts_by_mass_map[i, mass_bin - 1, lon_bin - 1, lat_bin - 1] += 1
412
+ counts_by_charge_map[i, charge_bin - 1, lon_bin - 1, lat_bin - 1] += 1
413
+
414
+ return (
415
+ counts_by_charge,
416
+ counts_by_mass,
417
+ counts_by_charge_map,
418
+ counts_by_mass_map,
419
+ daily_epoch,
420
+ )
421
+
422
+
423
+ def compute_rates(
424
+ counts: np.ndarray, epoch_doy_percent_on: np.ndarray, non_zero_inds: np.ndarray
425
+ ) -> np.ndarray:
426
+ """
427
+ Compute the count rates given the percent uptime of IDEX.
428
+
429
+ Parameters
430
+ ----------
431
+ counts : np.ndarray
432
+ Count values for the dust events.
433
+ epoch_doy_percent_on : np.ndarray
434
+ Percentage of time science acquisition was on for each day of the year.
435
+ non_zero_inds : np.ndarray
436
+ Indices of the days with non-zero science acquisition percentage.
437
+
438
+ Returns
439
+ -------
440
+ np.ndarray
441
+ Count rates.
442
+ """
443
+ while len(epoch_doy_percent_on.shape) < len(counts.shape):
444
+ epoch_doy_percent_on = np.expand_dims(epoch_doy_percent_on, axis=-1)
284
445
 
285
- return counts_by_charge, counts_by_mass, daily_epoch
446
+ return counts[non_zero_inds] / (
447
+ 0.01 * epoch_doy_percent_on[non_zero_inds] * SECONDS_IN_DAY
448
+ )
286
449
 
287
450
 
288
451
  def compute_rates_by_charge_and_mass(
289
452
  counts_by_charge: np.ndarray,
290
453
  counts_by_mass: np.ndarray,
454
+ counts_by_charge_map: np.ndarray,
455
+ counts_by_mass_map: np.ndarray,
291
456
  epoch_doy: np.ndarray,
292
457
  daily_on_percentage: dict,
293
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
458
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
294
459
  """
295
460
  Compute the dust event counts rates by charge and mass by spin phase for each day.
296
461
 
@@ -299,7 +464,11 @@ def compute_rates_by_charge_and_mass(
299
464
  counts_by_charge : np.ndarray
300
465
  3D array containing counts by charge and spin phase for each dataset.
301
466
  counts_by_mass : np.ndarray
302
- 3D array containing counts by mass and spin phase for each dataset.
467
+ 3D array containing counts by mass and lon and lat for each dataset.
468
+ counts_by_charge_map : np.ndarray
469
+ 4D array containing counts by charge and lon and lat for each dataset.
470
+ counts_by_mass_map : np.ndarray
471
+ 4D array containing counts by mass and spin phase for each dataset.
303
472
  epoch_doy : np.ndarray
304
473
  Unique days of year corresponding to the epochs in the dataset.
305
474
  daily_on_percentage : dict
@@ -314,6 +483,8 @@ def compute_rates_by_charge_and_mass(
314
483
  # Initialize arrays to hold rates.
315
484
  rate_by_charge = np.full(counts_by_charge.shape, -1.0)
316
485
  rate_by_mass = np.full(counts_by_mass.shape, -1.0)
486
+ rate_by_charge_map = np.full(counts_by_charge_map.shape, -1.0)
487
+ rate_by_mass_map = np.full(counts_by_mass_map.shape, -1.0)
317
488
  # Initialize an array to hold quality flags for each epoch. A quality flag of 0
318
489
  # indicates that there was no science acquisition data for that epoch, and the rate
319
490
  # is not valid. A quality flag of 1 indicates that the rate is valid.
@@ -336,18 +507,26 @@ def compute_rates_by_charge_and_mass(
336
507
  # acquisition time.
337
508
  non_zero_inds = np.where(epoch_doy_percent_on > 0)[0]
338
509
  # 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
510
+ rate_by_charge[non_zero_inds] = compute_rates(
511
+ counts_by_charge, epoch_doy_percent_on, non_zero_inds
512
+ )
513
+ rate_by_mass[non_zero_inds] = compute_rates(
514
+ counts_by_mass, epoch_doy_percent_on, non_zero_inds
343
515
  )
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
516
+ rate_by_charge_map[non_zero_inds] = compute_rates(
517
+ counts_by_charge_map, epoch_doy_percent_on, non_zero_inds
518
+ )
519
+ rate_by_mass_map[non_zero_inds] = compute_rates(
520
+ counts_by_mass_map, epoch_doy_percent_on, non_zero_inds
348
521
  )
349
522
 
350
- return rate_by_charge, rate_by_mass, rate_quality_flags
523
+ return (
524
+ rate_by_charge,
525
+ rate_by_mass,
526
+ rate_by_charge_map,
527
+ rate_by_mass_map,
528
+ rate_quality_flags,
529
+ )
351
530
 
352
531
 
353
532
  def bin_spin_phases(spin_phases: xr.DataArray) -> np.ndarray:
@@ -370,7 +549,7 @@ def bin_spin_phases(spin_phases: xr.DataArray) -> np.ndarray:
370
549
  f"phase angle range, [0, 360)."
371
550
  )
372
551
  # Shift spin phases by +45° so that the first bin starts at 0°.
373
- # Use mod to wrap values > 360 to 0.
552
+ # Use mod to wrap values >= 360 to 0.
374
553
  shifted_spin_phases = (spin_phases + 45) % 360
375
554
  # Use np.digitize to find the bin index for each spin phase.
376
555
  bin_indices = np.digitize(shifted_spin_phases, SPIN_PHASE_BIN_EDGES, right=False)