imap-processing 0.18.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 (104) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +301 -274
  4. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +28 -28
  5. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1044 -203
  6. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  7. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +12 -0
  8. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +5 -0
  9. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +10 -4
  10. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  11. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +8 -91
  12. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +106 -16
  13. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  14. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  15. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +85 -2
  16. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  17. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +12 -4
  18. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +50 -7
  19. imap_processing/cli.py +95 -41
  20. imap_processing/codice/codice_l1a.py +131 -31
  21. imap_processing/codice/codice_l2.py +118 -10
  22. imap_processing/codice/constants.py +740 -595
  23. imap_processing/decom.py +1 -4
  24. imap_processing/ena_maps/ena_maps.py +32 -25
  25. imap_processing/ena_maps/utils/naming.py +8 -2
  26. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  27. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  28. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  29. imap_processing/glows/ancillary/imap_glows_pipeline_settings_20250923_v002.json +54 -0
  30. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  31. imap_processing/glows/l1b/glows_l1b.py +99 -9
  32. imap_processing/glows/l1b/glows_l1b_data.py +350 -38
  33. imap_processing/glows/l2/glows_l2.py +11 -0
  34. imap_processing/hi/hi_l1a.py +124 -3
  35. imap_processing/hi/hi_l1b.py +154 -71
  36. imap_processing/hi/hi_l2.py +84 -51
  37. imap_processing/hi/utils.py +153 -8
  38. imap_processing/hit/l0/constants.py +3 -0
  39. imap_processing/hit/l0/decom_hit.py +3 -6
  40. imap_processing/hit/l1a/hit_l1a.py +311 -21
  41. imap_processing/hit/l1b/hit_l1b.py +54 -126
  42. imap_processing/hit/l2/hit_l2.py +6 -6
  43. imap_processing/ialirt/calculate_ingest.py +219 -0
  44. imap_processing/ialirt/constants.py +12 -2
  45. imap_processing/ialirt/generate_coverage.py +15 -2
  46. imap_processing/ialirt/l0/ialirt_spice.py +5 -2
  47. imap_processing/ialirt/l0/parse_mag.py +293 -42
  48. imap_processing/ialirt/l0/process_hit.py +5 -3
  49. imap_processing/ialirt/l0/process_swapi.py +41 -25
  50. imap_processing/ialirt/process_ephemeris.py +70 -14
  51. imap_processing/idex/idex_l0.py +2 -2
  52. imap_processing/idex/idex_l1a.py +2 -3
  53. imap_processing/idex/idex_l1b.py +2 -3
  54. imap_processing/idex/idex_l2a.py +130 -4
  55. imap_processing/idex/idex_l2b.py +158 -143
  56. imap_processing/idex/idex_utils.py +1 -3
  57. imap_processing/lo/l0/lo_science.py +25 -24
  58. imap_processing/lo/l1b/lo_l1b.py +3 -3
  59. imap_processing/lo/l1c/lo_l1c.py +116 -50
  60. imap_processing/lo/l2/lo_l2.py +29 -29
  61. imap_processing/lo/lo_ancillary.py +55 -0
  62. imap_processing/mag/l1a/mag_l1a.py +1 -0
  63. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  64. imap_processing/mag/l1b/mag_l1b.py +3 -2
  65. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  66. imap_processing/mag/l1c/mag_l1c.py +23 -6
  67. imap_processing/mag/l1d/mag_l1d.py +57 -14
  68. imap_processing/mag/l1d/mag_l1d_data.py +167 -30
  69. imap_processing/mag/l2/mag_l2_data.py +10 -2
  70. imap_processing/quality_flags.py +9 -1
  71. imap_processing/spice/geometry.py +76 -33
  72. imap_processing/spice/pointing_frame.py +0 -6
  73. imap_processing/spice/repoint.py +29 -2
  74. imap_processing/spice/spin.py +28 -8
  75. imap_processing/spice/time.py +12 -22
  76. imap_processing/swapi/l1/swapi_l1.py +10 -4
  77. imap_processing/swapi/l2/swapi_l2.py +15 -17
  78. imap_processing/swe/l1b/swe_l1b.py +1 -2
  79. imap_processing/ultra/constants.py +1 -24
  80. imap_processing/ultra/l0/ultra_utils.py +9 -11
  81. imap_processing/ultra/l1a/ultra_l1a.py +1 -2
  82. imap_processing/ultra/l1b/cullingmask.py +6 -3
  83. imap_processing/ultra/l1b/de.py +81 -23
  84. imap_processing/ultra/l1b/extendedspin.py +13 -10
  85. imap_processing/ultra/l1b/lookup_utils.py +281 -28
  86. imap_processing/ultra/l1b/quality_flag_filters.py +10 -1
  87. imap_processing/ultra/l1b/ultra_l1b_culling.py +161 -3
  88. imap_processing/ultra/l1b/ultra_l1b_extended.py +253 -47
  89. imap_processing/ultra/l1c/helio_pset.py +97 -24
  90. imap_processing/ultra/l1c/l1c_lookup_utils.py +256 -0
  91. imap_processing/ultra/l1c/spacecraft_pset.py +83 -16
  92. imap_processing/ultra/l1c/ultra_l1c.py +6 -2
  93. imap_processing/ultra/l1c/ultra_l1c_culling.py +85 -0
  94. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +385 -277
  95. imap_processing/ultra/l2/ultra_l2.py +0 -1
  96. imap_processing/ultra/utils/ultra_l1_utils.py +28 -3
  97. imap_processing/utils.py +3 -4
  98. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/METADATA +2 -2
  99. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/RECORD +102 -95
  100. imap_processing/idex/idex_l2c.py +0 -84
  101. imap_processing/spice/kernels.py +0 -187
  102. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/LICENSE +0 -0
  103. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/WHEEL +0 -0
  104. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -3,17 +3,30 @@
3
3
  import astropy_healpix.healpy as hp
4
4
  import numpy as np
5
5
  import pandas
6
- import pandas as pd
7
6
  import xarray as xr
8
7
  from numpy.typing import NDArray
9
- from scipy.interpolate import interp1d
8
+ from scipy import interpolate
10
9
 
11
10
  from imap_processing.spice.geometry import (
12
11
  SpiceFrame,
13
12
  cartesian_to_spherical,
14
13
  imap_state,
15
14
  )
15
+ from imap_processing.spice.spin import get_spacecraft_spin_phase, get_spin_angle
16
16
  from imap_processing.ultra.constants import UltraConstants
17
+ from imap_processing.ultra.l1b.lookup_utils import (
18
+ get_geometric_factor,
19
+ get_image_params,
20
+ load_geometric_factor_tables,
21
+ )
22
+ from imap_processing.ultra.l1b.ultra_l1b_culling import (
23
+ get_pulses_per_spin,
24
+ get_spin_and_duration,
25
+ )
26
+ from imap_processing.ultra.l1b.ultra_l1b_extended import (
27
+ get_efficiency,
28
+ get_efficiency_interpolator,
29
+ )
17
30
 
18
31
  # TODO: add species binning.
19
32
  FILLVAL_FLOAT32 = -1.0e31
@@ -32,15 +45,8 @@ def build_energy_bins() -> tuple[list[tuple[float, float]], np.ndarray, np.ndarr
32
45
  energy_bin_geometric_means : np.ndarray
33
46
  Array of geometric means of energy bins.
34
47
  """
35
- # Calculate energy step
36
- energy_step = (1 + UltraConstants.ALPHA / 2) / (1 - UltraConstants.ALPHA / 2)
37
-
38
48
  # Create energy bins.
39
- energy_bin_edges = UltraConstants.ENERGY_START * energy_step ** np.arange(
40
- UltraConstants.N_BINS + 1
41
- )
42
- # Add a zero to the left side for outliers and round to nearest 3 decimal places.
43
- energy_bin_edges = np.around(np.insert(energy_bin_edges, 0, 0), 3)
49
+ energy_bin_edges = np.array(UltraConstants.CULLING_ENERGY_BIN_EDGES)
44
50
  energy_midpoints = (energy_bin_edges[:-1] + energy_bin_edges[1:]) / 2
45
51
 
46
52
  intervals = [
@@ -73,7 +79,7 @@ def get_energy_delta_minus_plus() -> tuple[NDArray, NDArray]:
73
79
  """
74
80
  bins, _, bin_geom_means = build_energy_bins()
75
81
  bins_energy_delta_plus, bins_energy_delta_minus = [], []
76
- for bin_edges, bin_geom_mean in zip(bins, bin_geom_means):
82
+ for bin_edges, bin_geom_mean in zip(bins, bin_geom_means, strict=False):
77
83
  bins_energy_delta_plus.append(bin_edges[1] - bin_geom_mean)
78
84
  bins_energy_delta_minus.append(bin_geom_mean - bin_edges[0])
79
85
  return abs(np.array(bins_energy_delta_minus)), abs(np.array(bins_energy_delta_plus))
@@ -87,7 +93,7 @@ def get_spacecraft_histogram(
87
93
  nested: bool = False,
88
94
  ) -> tuple[NDArray, NDArray, NDArray, NDArray]:
89
95
  """
90
- Compute a 3D histogram of the particle data using HEALPix binning.
96
+ Compute a 2D histogram of the particle data using HEALPix binning.
91
97
 
92
98
  Parameters
93
99
  ----------
@@ -106,7 +112,7 @@ def get_spacecraft_histogram(
106
112
  Returns
107
113
  -------
108
114
  hist : np.ndarray
109
- A 3D histogram array with shape (n_pix, n_energy_bins).
115
+ A 2D histogram array with shape (n_pix, n_energy_bins).
110
116
  latitude : np.ndarray
111
117
  Array of latitude values.
112
118
  longitude : np.ndarray
@@ -152,66 +158,44 @@ def get_spacecraft_histogram(
152
158
  return hist, latitude, longitude, n_pix
153
159
 
154
160
 
155
- def get_spacecraft_background_rates(
156
- nside: int = 128,
157
- ) -> NDArray:
161
+ def get_spacecraft_count_rate_uncertainty(hist: NDArray, exposure: NDArray) -> NDArray:
158
162
  """
159
- Calculate background rates.
163
+ Calculate the count rate uncertainty for HEALPix-binned data.
160
164
 
161
165
  Parameters
162
166
  ----------
163
- nside : int, optional
164
- The nside parameter of the Healpix tessellation (default is 128).
167
+ hist : NDArray
168
+ A 2D histogram array with shape (n_pix, n_energy_bins).
169
+ exposure : NDArray
170
+ A 2D array of exposure times with shape (n_pix, n_energy_bins).
165
171
 
166
172
  Returns
167
173
  -------
168
- background_rates : np.ndarray
169
- Array of background rates.
174
+ count_rate_uncertainty : NDArray
175
+ Rate uncertainty with shape (n_pix, n_energy_bins) (counts/sec).
170
176
 
171
177
  Notes
172
178
  -----
173
- This is a placeholder.
174
- TODO: background rates to be provided by IT.
179
+ These calculations were based on Eqn 15 from the IMAP-Ultra Algorithm Document.
175
180
  """
176
- npix = hp.nside2npix(nside)
177
- _, energy_midpoints, _ = build_energy_bins()
178
- background = np.zeros((len(energy_midpoints), npix))
179
- return background
181
+ count_uncertainty = np.sqrt(hist)
180
182
 
183
+ rate_uncertainty = np.zeros_like(hist)
184
+ valid = exposure > 0
185
+ rate_uncertainty[valid] = count_uncertainty[valid] / exposure[valid]
181
186
 
182
- def get_helio_background_rates(
183
- nside: int = 128,
184
- ) -> NDArray:
185
- """
186
- Calculate background rates.
187
+ return rate_uncertainty
187
188
 
188
- Parameters
189
- ----------
190
- nside : int, optional
191
- The nside parameter of the Healpix tessellation (default is 128).
192
189
 
193
- Returns
194
- -------
195
- background_rates : np.ndarray
196
- Array of background rates.
197
-
198
- Notes
199
- -----
200
- This is a placeholder.
201
- TODO: background rates to be provided by IT.
190
+ def get_deadtime_ratios(sectored_rates_ds: xr.Dataset) -> xr.DataArray:
202
191
  """
203
- npix = hp.nside2npix(nside)
204
- _, energy_midpoints, _ = build_energy_bins()
205
- background = np.zeros((len(energy_midpoints), npix))
206
- return background
207
-
208
-
209
- def get_deadtime_correction_factors(sectored_rates_ds: xr.Dataset) -> xr.DataArray:
210
- """
211
- Compute the dead time correction factor at each sector.
192
+ Compute the dead time ratio at each sector.
212
193
 
213
- Further description is available in section 3.4.3 of the IMAP-Ultra Algorithm
214
- Document.
194
+ A reduction in exposure time (duty cycle) is caused by the flight hardware listening
195
+ for coincidence events that never occur, due to singles starts predominantly from UV
196
+ radiation. The static exposure time for a given Pointing should be reduced by this
197
+ spatially dependent exposure time reduction factor (the dead time). Further
198
+ description is available in section 3.4.3 of the IMAP-Ultra Algorithm Document.
215
199
 
216
200
  Parameters
217
201
  ----------
@@ -286,18 +270,142 @@ def get_sectored_rates(rates_ds: xr.Dataset, params_ds: xr.Dataset) -> xr.Datase
286
270
  # Build a list of conditions for each sector mode time range
287
271
  conditions = [
288
272
  (rates_ds["epoch"] >= start) & (rates_ds["epoch"] < end)
289
- for start, end in zip(mode_3_start, mode_3_end)
273
+ for start, end in zip(mode_3_start, mode_3_end, strict=False)
290
274
  ]
291
275
 
292
276
  sector_mode_mask = np.logical_or.reduce(conditions)
293
277
  return rates_ds.isel(epoch=sector_mode_mask)
294
278
 
295
279
 
280
+ def get_deadtime_ratios_by_spin_phase(
281
+ sectored_rates: xr.Dataset,
282
+ ) -> np.ndarray:
283
+ """
284
+ Calculate nominal deadtime ratios at every spin phase step (1ms res).
285
+
286
+ Parameters
287
+ ----------
288
+ sectored_rates : xarray.Dataset
289
+ Dataset containing sector mode image rates data.
290
+
291
+ Returns
292
+ -------
293
+ numpy.ndarray
294
+ Nominal deadtime ratios at every spin phase step (1ms res).
295
+ """
296
+ deadtime_ratios = get_deadtime_ratios(sectored_rates)
297
+ # Get the spin phase at the start of each sector rate measurement
298
+ spin_phases = np.asarray(
299
+ get_spin_angle(
300
+ get_spacecraft_spin_phase(np.array(sectored_rates.epoch.data)), degrees=True
301
+ )
302
+ )
303
+ # Assume the sectored rate data is evenly spaced in time, and find the middle spin
304
+ # phase value for each sector.
305
+ # The center spin phase is the closest / most accurate spin phase.
306
+ # There are 24 spin phases per sector so the nominal middle sector spin phases
307
+ # would be: array([ 12., 36., ..., 300., 324.]) for 15 sectors.
308
+ spin_phases_centered = (spin_phases[:-1] + spin_phases[1:]) / 2
309
+ # Assume the last sector is nominal because we dont have enough data to determine
310
+ # the spin phase at the end of the last sector.
311
+ # TODO: is this assumption valid?
312
+ # Add the last spin phase value + half of a nominal sector.
313
+ spin_phases_centered = np.append(spin_phases_centered, spin_phases[-1] + 12)
314
+ # Wrap any spin phases > 360 back to [0, 360]
315
+ spin_phases_centered = spin_phases_centered % 360
316
+ # Create a dataset with spin phases and dead time ratios
317
+ deadtime_by_spin_phase = xr.Dataset(
318
+ {"deadtime_ratio": deadtime_ratios},
319
+ coords={
320
+ "spin_phase": xr.DataArray(np.array(spin_phases_centered), dims="epoch")
321
+ },
322
+ )
323
+
324
+ # Sort the dataset by spin phase (ascending order)
325
+ deadtime_by_spin_phase = deadtime_by_spin_phase.sortby("spin_phase")
326
+ # Group by spin phase and calculate the median dead time ratio for each phase
327
+ deadtime_medians = deadtime_by_spin_phase.groupby("spin_phase").median(skipna=True)
328
+
329
+ if np.any(np.isnan(deadtime_medians["deadtime_ratio"].values)):
330
+ raise ValueError(
331
+ "Dead time ratios contain NaN values, cannot create interpolator."
332
+ )
333
+ interpolator = interpolate.PchipInterpolator(
334
+ deadtime_medians["spin_phase"].values, deadtime_medians["deadtime_ratio"].values
335
+ )
336
+ # Calculate the nominal spin phases at 1 ms resolution and query the pchip
337
+ # interpolator to get the deadtime ratios.
338
+ steps = 15 * 1000 # 15 seconds at 1 ms resolution
339
+ nominal_spin_phases_1ms_res = np.arange(0, 360, 360 / steps)
340
+ return interpolator(nominal_spin_phases_1ms_res)
341
+
342
+
343
+ def apply_deadtime_correction(
344
+ exposure_pointing: pandas.DataFrame,
345
+ deadtime_ratios: np.ndarray,
346
+ pixels_below_scattering: list,
347
+ boundary_scale_factors: NDArray,
348
+ ) -> np.ndarray:
349
+ """
350
+ Adjust the exposure time at each pixel to account for dead time.
351
+
352
+ Parameters
353
+ ----------
354
+ exposure_pointing : pandas.DataFrame
355
+ Exposure data.
356
+ deadtime_ratios : PchipInterpolator
357
+ Interpolating function for dead time ratios.
358
+ pixels_below_scattering : list
359
+ A Nested list of arrays indicating pixels within the scattering threshold.
360
+ The outer list indicates spin phase steps, the middle list indicates energy
361
+ bins, and the inner arrays contain indices indicating pixels that are below
362
+ the FWHM scattering threshold.
363
+ boundary_scale_factors : np.ndarray
364
+ Boundary scale factors for each pixel at each spin phase.
365
+
366
+ Returns
367
+ -------
368
+ exposure_pointing_adjusted : np.ndarray
369
+ Adjusted exposure times accounting for dead time.
370
+ """
371
+ # Get energy bin geometric means
372
+ energy_bin_geometric_means = build_energy_bins()[2]
373
+ # Exposure time should now be of shape (npix, energy)
374
+ exposure_pointing = np.repeat(
375
+ exposure_pointing.to_numpy()[np.newaxis, :],
376
+ len(energy_bin_geometric_means),
377
+ axis=0,
378
+ )
379
+ # nominal spin phase step.
380
+ nominal_ms_step = 15 / len(pixels_below_scattering) # time step
381
+ # Query the dead-time ratio and apply the nominal exposure time to pixels in the FOR
382
+ # and below the scattering threshold
383
+ # Loop through the spin phase steps. This is spinning the spacecraft by nominal
384
+ # 1 ms steps in the despun frame.
385
+ for i, pixels_at_spin in enumerate(pixels_below_scattering):
386
+ # Loop through energy bins
387
+ for energy_bin_idx in range(len(energy_bin_geometric_means)):
388
+ pixels_at_energy_and_spin = pixels_at_spin[energy_bin_idx]
389
+ if pixels_at_energy_and_spin.size == 0:
390
+ continue
391
+ # Apply the nominal exposure time (1 ms) scaled by the deadtime ratio to
392
+ # every pixel in the FOR, that is below the FWHM scattering threshold,
393
+ exposure_pointing[energy_bin_idx, pixels_at_energy_and_spin] += (
394
+ nominal_ms_step
395
+ * deadtime_ratios[i]
396
+ * boundary_scale_factors[pixels_at_energy_and_spin, i]
397
+ )
398
+
399
+ return exposure_pointing
400
+
401
+
296
402
  def get_spacecraft_exposure_times(
297
403
  constant_exposure: pandas.DataFrame,
298
404
  rates_dataset: xr.Dataset,
299
405
  params_dataset: xr.Dataset,
300
- ) -> NDArray:
406
+ pixels_below_scattering: list[list],
407
+ boundary_scale_factors: NDArray,
408
+ ) -> tuple[NDArray, NDArray]:
301
409
  """
302
410
  Compute exposure times for HEALPix pixels.
303
411
 
@@ -309,6 +417,13 @@ def get_spacecraft_exposure_times(
309
417
  Dataset containing image rates data.
310
418
  params_dataset : xarray.Dataset
311
419
  Dataset containing image parameters data.
420
+ pixels_below_scattering : list
421
+ List of lists indicating pixels within the scattering threshold.
422
+ The outer list indicates spin phase steps, the middle list indicates energy
423
+ bins, and the inner list contains pixel indices indicating pixels that are
424
+ below the FWHM scattering threshold.
425
+ boundary_scale_factors : np.ndarray
426
+ Boundary scale factors for each pixel at each spin phase.
312
427
 
313
428
  Returns
314
429
  -------
@@ -316,36 +431,145 @@ def get_spacecraft_exposure_times(
316
431
  Total exposure times of pixels in a
317
432
  Healpix tessellation of the sky
318
433
  in the pointing (dps) frame.
434
+ nominal_deadtime_ratios : np.ndarray
435
+ Deadtime ratios at each spin phase step (1ms res).
319
436
  """
320
- # TODO: uncomment these lines when the deadtime correction is implemented
321
- # sectored_rates = get_sectored_rates(rates_dataset, params_dataset)
322
- # get_deadtime_correction_factors(sectored_rates)
323
- # TODO: calculate the deadtime correction function
324
- # TODO: Apply the deadtime correction to the exposure times
325
437
  # TODO: use the universal spin table and
326
438
  # universal pointing table here to determine actual number of spins
439
+ sectored_rates = get_sectored_rates(rates_dataset, params_dataset)
440
+ nominal_deadtime_ratios = get_deadtime_ratios_by_spin_phase(sectored_rates)
327
441
  exposure_pointing = (
328
442
  constant_exposure["Exposure Time"] * 5760
329
443
  ) # 5760 spins per pointing (for now)
444
+ exposure_pointing_adjusted = apply_deadtime_correction(
445
+ exposure_pointing,
446
+ nominal_deadtime_ratios,
447
+ pixels_below_scattering,
448
+ boundary_scale_factors,
449
+ )
450
+ return exposure_pointing_adjusted, nominal_deadtime_ratios
330
451
 
331
- return exposure_pointing
332
452
 
453
+ def get_efficiencies_and_geometric_function(
454
+ pixels_below_scattering: list[list],
455
+ boundary_scale_factors: np.ndarray,
456
+ theta_vals: np.ndarray,
457
+ phi_vals: np.ndarray,
458
+ npix: int,
459
+ ancillary_files: dict,
460
+ ) -> tuple[np.ndarray, np.ndarray]:
461
+ """
462
+ Compute the geometric factor and efficiency for each pixel and energy bin.
463
+
464
+ The results are averaged over all spin phases.
333
465
 
334
- def get_helio_exposure_times(
335
- time: np.ndarray,
336
- df_exposure: pd.DataFrame,
466
+ Parameters
467
+ ----------
468
+ pixels_below_scattering : list
469
+ List of lists indicating pixels within the scattering threshold.
470
+ The outer list indicates spin phase steps, the middle list indicates energy
471
+ bins, and the inner list contains pixel indices indicating pixels that are
472
+ below the FWHM scattering threshold.
473
+ boundary_scale_factors : np.ndarray
474
+ Boundary scale factors for each pixel at each spin phase.
475
+ theta_vals : np.ndarray
476
+ A 2D array of theta values for each HEALPix pixel at each spin phase step.
477
+ phi_vals : np.ndarray
478
+ A 2D array of phi values for each HEALPix pixel at each spin phase step.
479
+ npix : int
480
+ Number of HEALPix pixels.
481
+ ancillary_files : dict
482
+ Dictionary containing ancillary files.
483
+
484
+ Returns
485
+ -------
486
+ gf_summation : np.ndarray
487
+ Summation of geometric factors for each pixel and energy bin.
488
+ eff_summation : np.ndarray
489
+ Summation of efficiencies for each pixel and energy bin.
490
+ """
491
+ # Load callable efficiency interpolator function
492
+ eff_interpolator = get_efficiency_interpolator(ancillary_files)
493
+ # load geometric factor lookup table
494
+ geometric_lookup_table = load_geometric_factor_tables(
495
+ ancillary_files, "l1b-sensor-gf-blades"
496
+ )
497
+ # Get energy bin geometric means
498
+ energy_bin_geometric_means = build_energy_bins()[2]
499
+ energy_bins = len(energy_bin_geometric_means)
500
+ # Initialize summation arrays for geometric factors and efficiencies
501
+ gf_summation = np.zeros((energy_bins, npix))
502
+ eff_summation = np.zeros((energy_bins, npix))
503
+ sample_count = np.zeros((energy_bins, npix))
504
+ # Loop through spin phases
505
+ for i, pixels_at_spin in enumerate(pixels_below_scattering):
506
+ # Loop through energy bins
507
+ # Compute gf and eff for these theta/phi pairs
508
+ theta_at_spin = theta_vals[:, i]
509
+ phi_at_spin = phi_vals[:, i]
510
+ gf_values = get_geometric_factor(
511
+ phi=phi_at_spin,
512
+ theta=theta_at_spin,
513
+ quality_flag=np.zeros(len(phi_at_spin)).astype(np.uint16),
514
+ geometric_factor_tables=geometric_lookup_table,
515
+ )
516
+ for energy_bin_idx in range(energy_bins):
517
+ pixel_inds = pixels_at_spin[energy_bin_idx]
518
+ if pixel_inds.size == 0:
519
+ continue
520
+ energy = energy_bin_geometric_means[energy_bin_idx]
521
+ eff_values = get_efficiency(
522
+ np.full(phi_at_spin[pixel_inds].shape, energy),
523
+ phi_at_spin[pixel_inds],
524
+ theta_at_spin[pixel_inds],
525
+ ancillary_files,
526
+ interpolator=eff_interpolator,
527
+ )
528
+ # Accumulate gf and eff values
529
+ gf_summation[energy_bin_idx, pixel_inds] += (
530
+ gf_values[pixel_inds] * boundary_scale_factors[pixel_inds, i]
531
+ )
532
+ eff_summation[energy_bin_idx, pixel_inds] += (
533
+ eff_values * boundary_scale_factors[pixel_inds, i]
534
+ )
535
+ sample_count[energy_bin_idx, pixel_inds] += 1
536
+
537
+ # return averaged geometric factors and efficiencies across all spin phases
538
+ # These are now energy dependent.
539
+ gf_averaged = np.divide(gf_summation, sample_count, where=sample_count != 0)
540
+ eff_averaged = np.divide(eff_summation, sample_count, where=sample_count != 0)
541
+ return gf_averaged, eff_averaged
542
+
543
+
544
+ def get_helio_adjusted_data(
545
+ time: float,
546
+ exposure_time: np.ndarray,
547
+ geometric_factor: np.ndarray,
548
+ efficiency: np.ndarray,
549
+ ra: np.ndarray,
550
+ dec: np.ndarray,
337
551
  nside: int = 128,
338
552
  nested: bool = False,
339
- ) -> NDArray:
553
+ ) -> tuple[NDArray, NDArray, NDArray]:
340
554
  """
341
- Compute a 2D (Healpix index, energy) array of exposure in the helio frame.
555
+ Compute 2D (Healpix index, energy) arrays for in the helio frame.
556
+
557
+ Build CG corrected exposure, efficiency, and geometric factor arrays.
342
558
 
343
559
  Parameters
344
560
  ----------
345
- time : np.ndarray
561
+ time : float
346
562
  Median time of pointing in et.
347
- df_exposure : pd.DataFrame
348
- Spacecraft exposure in healpix coordinates.
563
+ exposure_time : np.ndarray
564
+ Spacecraft exposure. Shape = (energy, npix).
565
+ geometric_factor : np.ndarray
566
+ Geometric factor values. Shape = (energy, npix).
567
+ efficiency : np.ndarray
568
+ Efficiency values. Shape = (energy, npix).
569
+ ra : np.ndarray
570
+ Right ascension in the spacecraft frame (degrees).
571
+ dec : np.ndarray
572
+ Declination in the spacecraft frame (degrees).
349
573
  nside : int, optional
350
574
  The nside parameter of the Healpix tessellation (default is 128).
351
575
  nested : bool, optional
@@ -354,18 +578,18 @@ def get_helio_exposure_times(
354
578
  Returns
355
579
  -------
356
580
  helio_exposure : np.ndarray
357
- A 2D array of shape (npix, n_energy_bins).
581
+ A 2D array of shape (n_energy_bins, npix).
582
+ helio_efficiency : np.ndarray
583
+ A 2D array of shape (n_energy_bins, npix).
584
+ helio_geometric_factors : np.ndarray
585
+ A 2D array of shape (n_energy_bins, npix).
358
586
 
359
587
  Notes
360
588
  -----
361
589
  These calculations are performed once per pointing.
362
590
  """
363
591
  # Get energy midpoints.
364
- _, energy_midpoints, _ = build_energy_bins()
365
- # Extract (RA/Dec) and exposure from the spacecraft frame.
366
- ra = df_exposure["Right Ascension (deg)"].values
367
- dec = df_exposure["Declination (deg)"].values
368
- exposure_flat = df_exposure["Exposure Time"].values
592
+ _, _, energy_bin_geometric_means = build_energy_bins()
369
593
 
370
594
  # The Cartesian state vector representing the position and velocity of the
371
595
  # IMAP spacecraft.
@@ -376,19 +600,28 @@ def get_helio_exposure_times(
376
600
  # Convert (RA, Dec) angles into 3D unit vectors.
377
601
  # Each unit vector represents a direction in the sky where the spacecraft observed
378
602
  # and accumulated exposure time.
603
+ npix = hp.nside2npix(nside)
379
604
  unit_dirs = hp.ang2vec(ra, dec, lonlat=True).T # Shape (N, 3)
380
-
605
+ shape = (len(energy_bin_geometric_means), int(npix))
606
+ if np.any(
607
+ [arr.shape != shape for arr in [exposure_time, geometric_factor, efficiency]]
608
+ ):
609
+ raise ValueError(
610
+ f"Input arrays must have the same shape {shape}, but got "
611
+ f"{exposure_time.shape}, {geometric_factor.shape}, {efficiency.shape}."
612
+ )
381
613
  # Initialize output array.
382
614
  # Each row corresponds to a HEALPix pixel, and each column to an energy bin.
383
- npix = hp.nside2npix(nside)
384
- helio_exposure = np.zeros((len(energy_midpoints), npix))
615
+ helio_exposure = np.zeros(shape)
616
+ helio_efficiency = np.zeros(shape)
617
+ helio_geometric_factors = np.zeros(shape)
385
618
 
386
619
  # Loop through energy bins and compute transformed exposure.
387
- for i, energy_midpoint in enumerate(energy_midpoints):
620
+ for i, energy_mean in enumerate(energy_bin_geometric_means):
388
621
  # Convert the midpoint energy to a velocity (km/s).
389
622
  # Based on kinetic energy equation: E = 1/2 * m * v^2.
390
623
  energy_velocity = (
391
- np.sqrt(2 * energy_midpoint * UltraConstants.KEV_J / UltraConstants.MASS_H)
624
+ np.sqrt(2 * energy_mean * UltraConstants.KEV_J / UltraConstants.MASS_H)
392
625
  / 1e3
393
626
  )
394
627
 
@@ -411,222 +644,97 @@ def get_helio_exposure_times(
411
644
  # Convert azimuth/elevation directions to HEALPix pixel indices.
412
645
  hpix_idx = hp.ang2pix(nside, az, el, nest=nested, lonlat=True)
413
646
 
414
- # Accumulate exposure values into HEALPix pixels for this energy bin.
647
+ # Accumulate exposure, eff, and gf values into HEALPix pixels for this energy
648
+ # bin.
415
649
  helio_exposure[i, :] = np.bincount(
416
- hpix_idx, weights=exposure_flat, minlength=npix
650
+ hpix_idx, weights=exposure_time[i, :], minlength=npix
651
+ )
652
+ helio_efficiency[i, :] = np.bincount(
653
+ hpix_idx, weights=efficiency[i, :], minlength=npix
654
+ )
655
+ helio_geometric_factors[i, :] = np.bincount(
656
+ hpix_idx, weights=geometric_factor[i, :], minlength=npix
417
657
  )
418
658
 
419
- return helio_exposure
420
-
421
-
422
- def get_spacecraft_sensitivity(
423
- efficiencies: pandas.DataFrame,
424
- geometric_function: pandas.DataFrame,
425
- ) -> tuple[pandas.DataFrame, NDArray, NDArray, NDArray]:
426
- """
427
- Compute sensitivity as efficiency * geometric factor.
428
-
429
- Parameters
430
- ----------
431
- efficiencies : pandas.DataFrame
432
- Efficiencies at different energy levels.
433
- geometric_function : pandas.DataFrame
434
- Geometric function.
435
-
436
- Returns
437
- -------
438
- pointing_sensitivity : pandas.DataFrame
439
- Sensitivity with dimensions (HEALPIX pixel_number, energy).
440
- energy_vals : NDArray
441
- Energy values of dataframe.
442
- right_ascension : NDArray
443
- Right ascension (longitude/azimuth) values of dataframe (0 - 360 degrees).
444
- declination : NDArray
445
- Declination (latitude/elevation) values of dataframe (-90 to 90 degrees).
446
- """
447
- # Exclude "Right Ascension (deg)" and "Declination (deg)" from the multiplication
448
- energy_columns = [
449
- col
450
- for col in efficiencies.columns
451
- if col not in ["Right Ascension (deg)", "Declination (deg)"]
452
- ]
453
- sensitivity = efficiencies[energy_columns].mul(
454
- geometric_function["Response (cm2-sr)"].values, axis=0
455
- )
456
-
457
- right_ascension = efficiencies["Right Ascension (deg)"]
458
- declination = efficiencies["Declination (deg)"]
459
-
460
- energy_vals = np.array([float(col.replace("keV", "")) for col in energy_columns])
461
-
462
- return sensitivity, energy_vals, right_ascension, declination
463
-
464
-
465
- def grid_sensitivity(
466
- efficiencies: pandas.DataFrame,
467
- geometric_function: pandas.DataFrame,
468
- energy: float,
469
- ) -> NDArray:
470
- """
471
- Grid the sensitivity.
472
-
473
- Parameters
474
- ----------
475
- efficiencies : pandas.DataFrame
476
- Efficiencies at different energy levels.
477
- geometric_function : pandas.DataFrame
478
- Geometric function.
479
- energy : float
480
- Energy to which we are interpolating.
481
-
482
- Returns
483
- -------
484
- interpolated_sensitivity : np.ndarray
485
- Sensitivity with dimensions (HEALPIX pixel_number, 1).
486
- """
487
- sensitivity, energy_vals, right_ascension, declination = get_spacecraft_sensitivity(
488
- efficiencies, geometric_function
489
- )
490
-
491
- # Create interpolator over energy dimension for each pixel (axis=1)
492
- interp_func = interp1d(
493
- energy_vals,
494
- sensitivity.values,
495
- axis=1,
496
- bounds_error=False,
497
- fill_value=np.nan,
498
- )
499
-
500
- # Interpolate to energy
501
- interpolated = interp_func(energy)
502
- interpolated = np.where(np.isnan(interpolated), FILLVAL_FLOAT32, interpolated)
503
-
504
- return interpolated
505
-
506
-
507
- def interpolate_sensitivity(
508
- efficiencies: pd.DataFrame,
509
- geometric_function: pd.DataFrame,
510
- nside: int = 128,
511
- ) -> NDArray:
512
- """
513
- Interpolate the sensitivity and bin it in HEALPix space.
514
-
515
- Parameters
516
- ----------
517
- efficiencies : pandas.DataFrame
518
- Efficiencies at different energy levels.
519
- geometric_function : pandas.DataFrame
520
- Geometric function.
521
- nside : int, optional
522
- Healpix nside resolution (default is 128).
523
-
524
- Returns
525
- -------
526
- interpolated_sensitivity : np.ndarray
527
- Array of shape (n_energy_bins, n_healpix_pixels).
528
- """
529
- _, _, energy_bin_geometric_means = build_energy_bins()
530
- npix = hp.nside2npix(nside)
531
-
532
- interpolated_sensitivity = np.full(
533
- (len(energy_bin_geometric_means), npix), FILLVAL_FLOAT32
534
- )
535
-
536
- for i, energy in enumerate(energy_bin_geometric_means):
537
- pixel_sensitivity = grid_sensitivity(
538
- efficiencies, geometric_function, energy
539
- ).flatten()
540
- interpolated_sensitivity[i, :] = pixel_sensitivity
541
-
542
- return interpolated_sensitivity
659
+ return helio_exposure, helio_efficiency, helio_geometric_factors
543
660
 
544
661
 
545
- def get_helio_sensitivity(
546
- time: np.ndarray,
547
- efficiencies: pandas.DataFrame,
548
- geometric_function: pandas.DataFrame,
662
+ def get_spacecraft_background_rates(
663
+ rates_dataset: xr.Dataset,
664
+ sensor: str,
665
+ ancillary_files: dict,
666
+ energy_bin_edges: list[tuple[float, float]],
667
+ cullingmask_spin_number: NDArray,
549
668
  nside: int = 128,
550
- nested: bool = False,
551
669
  ) -> NDArray:
552
670
  """
553
- Compute a 2D (Healpix index, energy) array of sensitivity in the helio frame.
671
+ Calculate background rates based on the provided parameters.
554
672
 
555
673
  Parameters
556
674
  ----------
557
- time : np.ndarray
558
- Median time of pointing in et.
559
- efficiencies : pandas.DataFrame
560
- Efficiencies at different energy levels.
561
- geometric_function : pandas.DataFrame
562
- Geometric function.
675
+ rates_dataset : xr.Dataset
676
+ Rates dataset.
677
+ sensor : str
678
+ Sensor name: "ultra45" or "ultra90".
679
+ ancillary_files : dict[Path]
680
+ Ancillary files containing the lookup tables.
681
+ energy_bin_edges : list[tuple[float, float]]
682
+ Energy bin edges.
683
+ cullingmask_spin_number : NDArray
684
+ Goodtime spins.
685
+ Ex. imap_ultra_l1b_45sensor-cullingmask[0]["spin_number"]
686
+ This is used to determine the number of pulses per spin.
563
687
  nside : int, optional
564
688
  The nside parameter of the Healpix tessellation (default is 128).
565
- nested : bool, optional
566
- Whether the Healpix tessellation is nested (default is False).
567
689
 
568
690
  Returns
569
691
  -------
570
- helio_sensitivity : np.ndarray
571
- A 2D array of shape (npix, n_energy_bins).
692
+ background_rates : NDArray of shape (n_energy_bins, n_HEALPix pixels)
693
+ Calculated background rates.
572
694
 
573
695
  Notes
574
696
  -----
575
- These calculations are performed once per pointing.
697
+ See Eqn. 3, 8, and 20 in the Algorithm Document for the equation.
576
698
  """
577
- # Get energy midpoints.
578
- _, energy_midpoints, _ = build_energy_bins()
579
-
580
- # Get sensitivity on the spacecraft grid
581
- _, _, ra, dec = get_spacecraft_sensitivity(efficiencies, geometric_function)
582
-
583
- # The Cartesian state vector representing the position and velocity of the
584
- # IMAP spacecraft.
585
- state = imap_state(time, ref_frame=SpiceFrame.IMAP_DPS)
586
-
587
- # Extract the velocity part of the state vector
588
- spacecraft_velocity = state[3:6]
589
- # Convert (RA, Dec) angles into 3D unit vectors.
590
- # Each unit vector represents a direction in the sky where the spacecraft observed
591
- # and accumulated sensitivity.
592
- unit_dirs = hp.ang2vec(ra, dec, lonlat=True).T # Shape (N, 3)
593
-
594
- # Initialize output array.
595
- # Each row corresponds to a HEALPix pixel, and each column to an energy bin.
596
- npix = hp.nside2npix(nside)
597
- helio_sensitivity = np.zeros((len(energy_midpoints), npix))
598
-
599
- # Loop through energy bins and compute transformed sensitivity.
600
- for i, energy in enumerate(energy_midpoints):
601
- # Convert the midpoint energy to a velocity (km/s).
602
- # Based on kinetic energy equation: E = 1/2 * m * v^2.
603
- energy_velocity = (
604
- np.sqrt(2 * energy * UltraConstants.KEV_J / UltraConstants.MASS_H) / 1e3
605
- )
606
-
607
- # Use Galilean Transform to transform the velocity wrt spacecraft
608
- # to the velocity wrt heliosphere.
609
- # energy_velocity * cartesian -> apply the magnitude of the velocity
610
- # to every position on the grid in the despun grid.
611
- helio_velocity = spacecraft_velocity.reshape(1, 3) + energy_velocity * unit_dirs
699
+ pulses = get_pulses_per_spin(rates_dataset)
700
+ # Pulses for the pointing.
701
+ etof_min = get_image_params("eTOFMin", sensor, ancillary_files)
702
+ etof_max = get_image_params("eTOFMax", sensor, ancillary_files)
703
+ spin_number, _ = get_spin_and_duration(
704
+ rates_dataset["shcoarse"], rates_dataset["spin"]
705
+ )
612
706
 
613
- # Normalized vectors representing the direction of the heliocentric velocity.
614
- helio_normalized = helio_velocity / np.linalg.norm(
615
- helio_velocity, axis=1, keepdims=True
616
- )
707
+ # Get dmin for PH (mm).
708
+ dmin_ctof = UltraConstants.DMIN_PH_CTOF
617
709
 
618
- # Convert Cartesian heliocentric vectors into spherical coordinates.
619
- # Result: azimuth (longitude) and elevation (latitude) in degrees.
620
- helio_spherical = cartesian_to_spherical(helio_normalized)
621
- az, el = helio_spherical[:, 1], helio_spherical[:, 2]
710
+ # Compute number of HEALPix pixels that cover the sphere
711
+ n_pix = hp.nside2npix(nside)
622
712
 
623
- # Convert azimuth/elevation directions to HEALPix pixel indices.
624
- hpix_idx = hp.ang2pix(nside, az, el, nest=nested, lonlat=True)
625
- gridded_sensitivity = grid_sensitivity(efficiencies, geometric_function, energy)
713
+ # Initialize background rate array: (n_energy_bins, n_HEALPix pixels)
714
+ background_rates = np.zeros((len(energy_bin_edges), n_pix))
626
715
 
627
- # Accumulate sensitivity values into HEALPix pixels for this energy bin.
628
- helio_sensitivity[i, :] = np.bincount(
629
- hpix_idx, weights=gridded_sensitivity, minlength=npix
630
- )
716
+ # Only select pulses from goodtimes.
717
+ goodtime_mask = np.isin(spin_number, cullingmask_spin_number)
718
+ mean_start_pulses = np.mean(pulses.start_pulses[goodtime_mask])
719
+ mean_stop_pulses = np.mean(pulses.stop_pulses[goodtime_mask])
720
+ mean_coin_pulses = np.mean(pulses.coin_pulses[goodtime_mask])
631
721
 
632
- return helio_sensitivity
722
+ for i, (e_min, e_max) in enumerate(energy_bin_edges):
723
+ # Calculate ctof for the energy bin boundaries by combining Eqn. 3 and 8.
724
+ # Compute speed for min and max energy using E = 1/2mv^2 -> v = sqrt(2E/m)
725
+ vmin = np.sqrt(2 * e_min * UltraConstants.KEV_J / UltraConstants.MASS_H) # m/s
726
+ vmax = np.sqrt(2 * e_max * UltraConstants.KEV_J / UltraConstants.MASS_H) # m/s
727
+ # Compute cTOF = dmin / v
728
+ # Multiply times 1e-3 to convert to m.
729
+ ctof_min = dmin_ctof * 1e-3 / vmax * 1e-9 # Convert to ns
730
+ ctof_max = dmin_ctof * 1e-3 / vmin * 1e-9 # Convert to ns
731
+
732
+ background_rates[i, :] = (
733
+ np.abs(ctof_max - ctof_min)
734
+ * (etof_max - etof_min)
735
+ * mean_start_pulses
736
+ * mean_stop_pulses
737
+ * mean_coin_pulses
738
+ ) / 30.0
739
+
740
+ return background_rates