imap-processing 0.18.0__py3-none-any.whl → 0.19.2__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 (122) 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_global_cdf_attrs.yaml +6 -0
  4. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +221 -1057
  5. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +307 -283
  6. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1044 -203
  7. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  8. imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +11 -0
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +15 -1
  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_idex_l2a_variable_attrs.yaml +33 -4
  13. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +8 -91
  14. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +106 -16
  15. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +5 -4
  16. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  17. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  18. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +85 -2
  19. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  20. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +20 -8
  21. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +45 -35
  22. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +110 -7
  23. imap_processing/cli.py +138 -93
  24. imap_processing/codice/codice_l0.py +2 -1
  25. imap_processing/codice/codice_l1a.py +167 -69
  26. imap_processing/codice/codice_l1b.py +42 -32
  27. imap_processing/codice/codice_l2.py +215 -9
  28. imap_processing/codice/constants.py +790 -603
  29. imap_processing/codice/data/lo_stepping_values.csv +1 -1
  30. imap_processing/decom.py +1 -4
  31. imap_processing/ena_maps/ena_maps.py +71 -43
  32. imap_processing/ena_maps/utils/corrections.py +291 -0
  33. imap_processing/ena_maps/utils/map_utils.py +20 -4
  34. imap_processing/ena_maps/utils/naming.py +8 -2
  35. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  37. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  38. imap_processing/glows/ancillary/imap_glows_pipeline-settings_20250923_v002.json +54 -0
  39. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  40. imap_processing/glows/l1b/glows_l1b.py +123 -18
  41. imap_processing/glows/l1b/glows_l1b_data.py +358 -47
  42. imap_processing/glows/l2/glows_l2.py +11 -0
  43. imap_processing/hi/hi_l1a.py +124 -3
  44. imap_processing/hi/hi_l1b.py +154 -71
  45. imap_processing/hi/hi_l1c.py +4 -109
  46. imap_processing/hi/hi_l2.py +104 -60
  47. imap_processing/hi/utils.py +262 -8
  48. imap_processing/hit/l0/constants.py +3 -0
  49. imap_processing/hit/l0/decom_hit.py +3 -6
  50. imap_processing/hit/l1a/hit_l1a.py +311 -21
  51. imap_processing/hit/l1b/hit_l1b.py +54 -126
  52. imap_processing/hit/l2/hit_l2.py +6 -6
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +12 -2
  55. imap_processing/ialirt/generate_coverage.py +15 -2
  56. imap_processing/ialirt/l0/ialirt_spice.py +6 -2
  57. imap_processing/ialirt/l0/parse_mag.py +293 -42
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/process_ephemeris.py +70 -14
  61. imap_processing/ialirt/utils/create_xarray.py +1 -1
  62. imap_processing/idex/idex_l0.py +2 -2
  63. imap_processing/idex/idex_l1a.py +2 -3
  64. imap_processing/idex/idex_l1b.py +2 -3
  65. imap_processing/idex/idex_l2a.py +130 -4
  66. imap_processing/idex/idex_l2b.py +158 -143
  67. imap_processing/idex/idex_utils.py +1 -3
  68. imap_processing/lo/ancillary_data/imap_lo_hydrogen-geometric-factor_v001.csv +75 -0
  69. imap_processing/lo/ancillary_data/imap_lo_oxygen-geometric-factor_v001.csv +75 -0
  70. imap_processing/lo/l0/lo_science.py +25 -24
  71. imap_processing/lo/l1b/lo_l1b.py +93 -19
  72. imap_processing/lo/l1c/lo_l1c.py +273 -93
  73. imap_processing/lo/l2/lo_l2.py +949 -135
  74. imap_processing/lo/lo_ancillary.py +55 -0
  75. imap_processing/mag/l1a/mag_l1a.py +1 -0
  76. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  77. imap_processing/mag/l1b/mag_l1b.py +3 -2
  78. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  79. imap_processing/mag/l1c/mag_l1c.py +23 -6
  80. imap_processing/mag/l1d/mag_l1d.py +57 -14
  81. imap_processing/mag/l1d/mag_l1d_data.py +202 -32
  82. imap_processing/mag/l2/mag_l2.py +2 -0
  83. imap_processing/mag/l2/mag_l2_data.py +14 -5
  84. imap_processing/quality_flags.py +23 -1
  85. imap_processing/spice/geometry.py +89 -39
  86. imap_processing/spice/pointing_frame.py +4 -8
  87. imap_processing/spice/repoint.py +78 -2
  88. imap_processing/spice/spin.py +28 -8
  89. imap_processing/spice/time.py +12 -22
  90. imap_processing/swapi/l1/swapi_l1.py +10 -4
  91. imap_processing/swapi/l2/swapi_l2.py +15 -17
  92. imap_processing/swe/l1b/swe_l1b.py +1 -2
  93. imap_processing/ultra/constants.py +30 -24
  94. imap_processing/ultra/l0/ultra_utils.py +9 -11
  95. imap_processing/ultra/l1a/ultra_l1a.py +1 -2
  96. imap_processing/ultra/l1b/badtimes.py +35 -11
  97. imap_processing/ultra/l1b/de.py +95 -31
  98. imap_processing/ultra/l1b/extendedspin.py +31 -16
  99. imap_processing/ultra/l1b/goodtimes.py +112 -0
  100. imap_processing/ultra/l1b/lookup_utils.py +281 -28
  101. imap_processing/ultra/l1b/quality_flag_filters.py +10 -1
  102. imap_processing/ultra/l1b/ultra_l1b.py +7 -7
  103. imap_processing/ultra/l1b/ultra_l1b_culling.py +169 -7
  104. imap_processing/ultra/l1b/ultra_l1b_extended.py +311 -69
  105. imap_processing/ultra/l1c/helio_pset.py +139 -37
  106. imap_processing/ultra/l1c/l1c_lookup_utils.py +289 -0
  107. imap_processing/ultra/l1c/spacecraft_pset.py +140 -29
  108. imap_processing/ultra/l1c/ultra_l1c.py +33 -24
  109. imap_processing/ultra/l1c/ultra_l1c_culling.py +92 -0
  110. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +400 -292
  111. imap_processing/ultra/l2/ultra_l2.py +54 -11
  112. imap_processing/ultra/utils/ultra_l1_utils.py +37 -7
  113. imap_processing/utils.py +3 -4
  114. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/METADATA +2 -2
  115. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/RECORD +118 -109
  116. imap_processing/idex/idex_l2c.py +0 -84
  117. imap_processing/spice/kernels.py +0 -187
  118. imap_processing/ultra/l1b/cullingmask.py +0 -87
  119. imap_processing/ultra/l1c/histogram.py +0 -36
  120. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/LICENSE +0 -0
  121. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/WHEEL +0 -0
  122. {imap_processing-0.18.0.dist-info → imap_processing-0.19.2.dist-info}/entry_points.txt +0 -0
@@ -1,23 +1,40 @@
1
1
  """Module to create pointing sets."""
2
2
 
3
+ import logging
4
+
3
5
  import astropy_healpix.healpy as hp
4
6
  import numpy as np
5
- import pandas
6
- import pandas as pd
7
7
  import xarray as xr
8
8
  from numpy.typing import NDArray
9
- from scipy.interpolate import interp1d
9
+ from scipy import interpolate
10
10
 
11
11
  from imap_processing.spice.geometry import (
12
12
  SpiceFrame,
13
13
  cartesian_to_spherical,
14
14
  imap_state,
15
15
  )
16
+ from imap_processing.spice.spin import get_spacecraft_spin_phase, get_spin_angle
17
+ from imap_processing.spice.time import ttj2000ns_to_met
16
18
  from imap_processing.ultra.constants import UltraConstants
19
+ from imap_processing.ultra.l1b.lookup_utils import (
20
+ get_geometric_factor,
21
+ get_image_params,
22
+ load_geometric_factor_tables,
23
+ )
24
+ from imap_processing.ultra.l1b.ultra_l1b_culling import (
25
+ get_pulses_per_spin,
26
+ get_spin_and_duration,
27
+ )
28
+ from imap_processing.ultra.l1b.ultra_l1b_extended import (
29
+ get_efficiency,
30
+ get_efficiency_interpolator,
31
+ )
17
32
 
18
33
  # TODO: add species binning.
19
34
  FILLVAL_FLOAT32 = -1.0e31
20
35
 
36
+ logger = logging.getLogger(__name__)
37
+
21
38
 
22
39
  def build_energy_bins() -> tuple[list[tuple[float, float]], np.ndarray, np.ndarray]:
23
40
  """
@@ -32,15 +49,8 @@ def build_energy_bins() -> tuple[list[tuple[float, float]], np.ndarray, np.ndarr
32
49
  energy_bin_geometric_means : np.ndarray
33
50
  Array of geometric means of energy bins.
34
51
  """
35
- # Calculate energy step
36
- energy_step = (1 + UltraConstants.ALPHA / 2) / (1 - UltraConstants.ALPHA / 2)
37
-
38
52
  # 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)
53
+ energy_bin_edges = np.array(UltraConstants.PSET_ENERGY_BIN_EDGES)
44
54
  energy_midpoints = (energy_bin_edges[:-1] + energy_bin_edges[1:]) / 2
45
55
 
46
56
  intervals = [
@@ -73,7 +83,7 @@ def get_energy_delta_minus_plus() -> tuple[NDArray, NDArray]:
73
83
  """
74
84
  bins, _, bin_geom_means = build_energy_bins()
75
85
  bins_energy_delta_plus, bins_energy_delta_minus = [], []
76
- for bin_edges, bin_geom_mean in zip(bins, bin_geom_means):
86
+ for bin_edges, bin_geom_mean in zip(bins, bin_geom_means, strict=False):
77
87
  bins_energy_delta_plus.append(bin_edges[1] - bin_geom_mean)
78
88
  bins_energy_delta_minus.append(bin_geom_mean - bin_edges[0])
79
89
  return abs(np.array(bins_energy_delta_minus)), abs(np.array(bins_energy_delta_plus))
@@ -87,7 +97,7 @@ def get_spacecraft_histogram(
87
97
  nested: bool = False,
88
98
  ) -> tuple[NDArray, NDArray, NDArray, NDArray]:
89
99
  """
90
- Compute a 3D histogram of the particle data using HEALPix binning.
100
+ Compute a 2D histogram of the particle data using HEALPix binning.
91
101
 
92
102
  Parameters
93
103
  ----------
@@ -106,7 +116,7 @@ def get_spacecraft_histogram(
106
116
  Returns
107
117
  -------
108
118
  hist : np.ndarray
109
- A 3D histogram array with shape (n_pix, n_energy_bins).
119
+ A 2D histogram array with shape (n_pix, n_energy_bins).
110
120
  latitude : np.ndarray
111
121
  Array of latitude values.
112
122
  longitude : np.ndarray
@@ -152,66 +162,44 @@ def get_spacecraft_histogram(
152
162
  return hist, latitude, longitude, n_pix
153
163
 
154
164
 
155
- def get_spacecraft_background_rates(
156
- nside: int = 128,
157
- ) -> NDArray:
165
+ def get_spacecraft_count_rate_uncertainty(hist: NDArray, exposure: NDArray) -> NDArray:
158
166
  """
159
- Calculate background rates.
167
+ Calculate the count rate uncertainty for HEALPix-binned data.
160
168
 
161
169
  Parameters
162
170
  ----------
163
- nside : int, optional
164
- The nside parameter of the Healpix tessellation (default is 128).
171
+ hist : NDArray
172
+ A 2D histogram array with shape (n_pix, n_energy_bins).
173
+ exposure : NDArray
174
+ A 2D array of exposure times with shape (n_pix, n_energy_bins).
165
175
 
166
176
  Returns
167
177
  -------
168
- background_rates : np.ndarray
169
- Array of background rates.
178
+ count_rate_uncertainty : NDArray
179
+ Rate uncertainty with shape (n_pix, n_energy_bins) (counts/sec).
170
180
 
171
181
  Notes
172
182
  -----
173
- This is a placeholder.
174
- TODO: background rates to be provided by IT.
183
+ These calculations were based on Eqn 15 from the IMAP-Ultra Algorithm Document.
175
184
  """
176
- npix = hp.nside2npix(nside)
177
- _, energy_midpoints, _ = build_energy_bins()
178
- background = np.zeros((len(energy_midpoints), npix))
179
- return background
180
-
185
+ count_uncertainty = np.sqrt(hist)
181
186
 
182
- def get_helio_background_rates(
183
- nside: int = 128,
184
- ) -> NDArray:
185
- """
186
- Calculate background rates.
187
+ rate_uncertainty = np.zeros_like(hist)
188
+ valid = exposure > 0
189
+ rate_uncertainty[valid] = count_uncertainty[valid] / exposure[valid]
187
190
 
188
- Parameters
189
- ----------
190
- nside : int, optional
191
- The nside parameter of the Healpix tessellation (default is 128).
191
+ return rate_uncertainty
192
192
 
193
- Returns
194
- -------
195
- background_rates : np.ndarray
196
- Array of background rates.
197
193
 
198
- Notes
199
- -----
200
- This is a placeholder.
201
- TODO: background rates to be provided by IT.
202
- """
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:
194
+ def get_deadtime_ratios(sectored_rates_ds: xr.Dataset) -> xr.DataArray:
210
195
  """
211
- Compute the dead time correction factor at each sector.
196
+ Compute the dead time ratio at each sector.
212
197
 
213
- Further description is available in section 3.4.3 of the IMAP-Ultra Algorithm
214
- Document.
198
+ A reduction in exposure time (duty cycle) is caused by the flight hardware listening
199
+ for coincidence events that never occur, due to singles starts predominantly from UV
200
+ radiation. The static exposure time for a given Pointing should be reduced by this
201
+ spatially dependent exposure time reduction factor (the dead time). Further
202
+ description is available in section 3.4.3 of the IMAP-Ultra Algorithm Document.
215
203
 
216
204
  Parameters
217
205
  ----------
@@ -238,7 +226,6 @@ def get_deadtime_correction_factors(sectored_rates_ds: xr.Dataset) -> xr.DataArr
238
226
  - sectored_rates_ds.stop_tn
239
227
  - sectored_rates_ds.stop_bn
240
228
  )
241
-
242
229
  corrected_valid_events = b * np.exp(1e-7 * 8 * coin_stop_nd)
243
230
 
244
231
  # Compute dead time ratio
@@ -268,47 +255,181 @@ def get_sectored_rates(rates_ds: xr.Dataset, params_ds: xr.Dataset) -> xr.Datase
268
255
 
269
256
  # This means that data was collected as a function of spin allowing for fine grained
270
257
  # rate analysis.
271
- sector_mode_start_inds = np.where(params_ds["imageratescadence"] == 3)[0]
258
+ # Only get unique combinations of epoch and imageratescadence
259
+ params = params_ds.groupby(["epoch", "imageratescadence"]).first()
260
+
261
+ sector_mode_start_inds = np.where(params["imageratescadence"] == 3)[0]
262
+ if len(sector_mode_start_inds) == 0:
263
+ raise ValueError("No sector mode data found in the parameters dataset.")
272
264
  # get the sector mode start and stop indices
273
265
  sector_mode_stop_inds = sector_mode_start_inds + 1
274
266
  # get the sector mode start and stop times
275
- mode_3_start = params_ds["epoch"].values[sector_mode_start_inds]
276
-
267
+ mode_3_start = params["epoch"].values[sector_mode_start_inds]
277
268
  # if the last mode is a sector mode, we can assume that the sector data goes through
278
269
  # the end of the dataset, so we append np.inf to the end of the last time range.
279
- if sector_mode_stop_inds[-1] == len(params_ds["epoch"]):
270
+ if sector_mode_stop_inds[-1] == len(params["epoch"]):
280
271
  mode_3_end = np.append(
281
- params_ds["epoch"].values[sector_mode_stop_inds[:-1]], np.inf
272
+ params["epoch"].values[sector_mode_stop_inds[:-1]], np.inf
282
273
  )
283
274
  else:
284
- mode_3_end = params_ds["epoch"].values[sector_mode_stop_inds]
285
-
275
+ mode_3_end = params["epoch"].values[sector_mode_stop_inds]
286
276
  # Build a list of conditions for each sector mode time range
287
277
  conditions = [
288
278
  (rates_ds["epoch"] >= start) & (rates_ds["epoch"] < end)
289
- for start, end in zip(mode_3_start, mode_3_end)
279
+ for start, end in zip(mode_3_start, mode_3_end, strict=False)
290
280
  ]
291
281
 
292
282
  sector_mode_mask = np.logical_or.reduce(conditions)
293
283
  return rates_ds.isel(epoch=sector_mode_mask)
294
284
 
295
285
 
286
+ def get_deadtime_ratios_by_spin_phase(
287
+ sectored_rates: xr.Dataset,
288
+ ) -> np.ndarray:
289
+ """
290
+ Calculate nominal deadtime ratios at every spin phase step (1ms res).
291
+
292
+ Parameters
293
+ ----------
294
+ sectored_rates : xarray.Dataset
295
+ Dataset containing sector mode image rates data.
296
+
297
+ Returns
298
+ -------
299
+ numpy.ndarray
300
+ Nominal deadtime ratios at every spin phase step (1ms res).
301
+ """
302
+ deadtime_ratios = get_deadtime_ratios(sectored_rates)
303
+ # Get the spin phase at the start of each sector rate measurement
304
+ met_times = ttj2000ns_to_met(sectored_rates.epoch.data)
305
+ spin_phases = np.asarray(
306
+ get_spin_angle(get_spacecraft_spin_phase(met_times), degrees=True)
307
+ )
308
+ # Assume the sectored rate data is evenly spaced in time, and find the middle spin
309
+ # phase value for each sector.
310
+ # The center spin phase is the closest / most accurate spin phase.
311
+ # There are 24 spin phases per sector so the nominal middle sector spin phases
312
+ # would be: array([ 12., 36., ..., 300., 324.]) for 15 sectors.
313
+ spin_phases_centered = (spin_phases[:-1] + spin_phases[1:]) / 2
314
+ # Assume the last sector is nominal because we dont have enough data to determine
315
+ # the spin phase at the end of the last sector.
316
+ # TODO: is this assumption valid?
317
+ # Add the last spin phase value + half of a nominal sector.
318
+ spin_phases_centered = np.append(spin_phases_centered, spin_phases[-1] + 12)
319
+ # Wrap any spin phases > 360 back to [0, 360]
320
+ spin_phases_centered = spin_phases_centered % 360
321
+ # Create a dataset with spin phases and dead time ratios
322
+ deadtime_by_spin_phase = xr.Dataset(
323
+ {"deadtime_ratio": deadtime_ratios},
324
+ coords={
325
+ "spin_phase": xr.DataArray(np.array(spin_phases_centered), dims="epoch")
326
+ },
327
+ )
328
+
329
+ # Sort the dataset by spin phase (ascending order)
330
+ deadtime_by_spin_phase = deadtime_by_spin_phase.sortby("spin_phase")
331
+ # Group by spin phase and calculate the median dead time ratio for each phase
332
+ deadtime_medians = deadtime_by_spin_phase.groupby("spin_phase").median(skipna=True)
333
+ if np.any(np.isnan(deadtime_medians["deadtime_ratio"].values)):
334
+ if not np.any(np.isfinite(deadtime_medians["deadtime_ratio"].values)):
335
+ raise ValueError("All dead time ratios are NaN, cannot interpolate.")
336
+ logger.warning(
337
+ "Dead time ratios contain NaN values, filtering data to only include "
338
+ "finite values."
339
+ )
340
+ deadtime_medians = deadtime_medians.where(
341
+ np.isfinite(deadtime_medians["deadtime_ratio"]), drop=True
342
+ )
343
+ interpolator = interpolate.PchipInterpolator(
344
+ deadtime_medians["spin_phase"].values, deadtime_medians["deadtime_ratio"].values
345
+ )
346
+ # Calculate the nominal spin phases at 1 ms resolution and query the pchip
347
+ # interpolator to get the deadtime ratios.
348
+ steps = 15 * 1000 # 15 seconds at 1 ms resolution
349
+ nominal_spin_phases_1ms_res = np.arange(0, 360, 360 / steps)
350
+ return interpolator(nominal_spin_phases_1ms_res)
351
+
352
+
353
+ def calculate_exposure_time(
354
+ deadtime_ratios: np.ndarray,
355
+ pixels_below_scattering: list,
356
+ boundary_scale_factors: NDArray,
357
+ n_pix: int,
358
+ ) -> np.ndarray:
359
+ """
360
+ Adjust the exposure time at each pixel to account for dead time.
361
+
362
+ Parameters
363
+ ----------
364
+ deadtime_ratios : PchipInterpolator
365
+ Interpolating function for dead time ratios.
366
+ pixels_below_scattering : list
367
+ A Nested list of arrays indicating pixels within the scattering threshold.
368
+ The outer list indicates spin phase steps, the middle list indicates energy
369
+ bins, and the inner arrays contain indices indicating pixels that are below
370
+ the FWHM scattering threshold.
371
+ boundary_scale_factors : np.ndarray
372
+ Boundary scale factors for each pixel at each spin phase.
373
+ n_pix : int
374
+ Number of HEALPix pixels.
375
+
376
+ Returns
377
+ -------
378
+ exposure_pointing_adjusted : np.ndarray
379
+ Adjusted exposure times accounting for dead time.
380
+ """
381
+ # Get energy bin geometric means
382
+ energy_bin_geometric_means = build_energy_bins()[2]
383
+ # Exposure time should now be of shape (energy, npix)
384
+ exposure_pointing = np.zeros((len(energy_bin_geometric_means), n_pix))
385
+ # nominal spin phase step.
386
+ nominal_ms_step = 15 / len(pixels_below_scattering) # time step
387
+ # Query the dead-time ratio and apply the nominal exposure time to pixels in the FOR
388
+ # and below the scattering threshold
389
+ # Loop through the spin phase steps. This is spinning the spacecraft by nominal
390
+ # 1 ms steps in the despun frame.
391
+ for i, pixels_at_spin in enumerate(pixels_below_scattering):
392
+ # Loop through energy bins
393
+ for energy_bin_idx in range(len(energy_bin_geometric_means)):
394
+ pixels_at_energy_and_spin = pixels_at_spin[energy_bin_idx]
395
+ if pixels_at_energy_and_spin.size == 0:
396
+ continue
397
+ # Apply the nominal exposure time (1 ms) scaled by the deadtime ratio to
398
+ # every pixel in the FOR, that is below the FWHM scattering threshold,
399
+ exposure_pointing[energy_bin_idx, pixels_at_energy_and_spin] += (
400
+ nominal_ms_step
401
+ * deadtime_ratios[i]
402
+ * boundary_scale_factors[pixels_at_energy_and_spin, i]
403
+ )
404
+
405
+ return exposure_pointing
406
+
407
+
296
408
  def get_spacecraft_exposure_times(
297
- constant_exposure: pandas.DataFrame,
298
409
  rates_dataset: xr.Dataset,
299
410
  params_dataset: xr.Dataset,
300
- ) -> NDArray:
411
+ pixels_below_scattering: list[list],
412
+ boundary_scale_factors: NDArray,
413
+ n_pix: int,
414
+ ) -> tuple[NDArray, NDArray]:
301
415
  """
302
416
  Compute exposure times for HEALPix pixels.
303
417
 
304
418
  Parameters
305
419
  ----------
306
- constant_exposure : pandas.DataFrame
307
- Exposure data.
308
420
  rates_dataset : xarray.Dataset
309
421
  Dataset containing image rates data.
310
422
  params_dataset : xarray.Dataset
311
423
  Dataset containing image parameters data.
424
+ pixels_below_scattering : list
425
+ List of lists indicating pixels within the scattering threshold.
426
+ The outer list indicates spin phase steps, the middle list indicates energy
427
+ bins, and the inner list contains pixel indices indicating pixels that are
428
+ below the FWHM scattering threshold.
429
+ boundary_scale_factors : np.ndarray
430
+ Boundary scale factors for each pixel at each spin phase.
431
+ n_pix : int
432
+ Number of HEALPix pixels.
312
433
 
313
434
  Returns
314
435
  -------
@@ -316,36 +437,139 @@ def get_spacecraft_exposure_times(
316
437
  Total exposure times of pixels in a
317
438
  Healpix tessellation of the sky
318
439
  in the pointing (dps) frame.
440
+ nominal_deadtime_ratios : np.ndarray
441
+ Deadtime ratios at each spin phase step (1ms res).
319
442
  """
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
443
  # TODO: use the universal spin table and
326
444
  # universal pointing table here to determine actual number of spins
327
- exposure_pointing = (
328
- constant_exposure["Exposure Time"] * 5760
329
- ) # 5760 spins per pointing (for now)
445
+ sectored_rates = get_sectored_rates(rates_dataset, params_dataset)
446
+ nominal_deadtime_ratios = get_deadtime_ratios_by_spin_phase(sectored_rates)
447
+ exposure_pointing_adjusted = calculate_exposure_time(
448
+ nominal_deadtime_ratios, pixels_below_scattering, boundary_scale_factors, n_pix
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.
465
+
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.
333
483
 
334
- def get_helio_exposure_times(
335
- time: np.ndarray,
336
- df_exposure: pd.DataFrame,
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
+ goodtimes_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
+ goodtimes_spin_number : NDArray
684
+ Goodtime spins.
685
+ Ex. imap_ultra_l1b_45sensor-goodtimes[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
- )
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
+ )
606
706
 
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
707
+ # Get dmin for PH (mm).
708
+ dmin_ctof = UltraConstants.DMIN_PH_CTOF
612
709
 
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
- )
617
-
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, goodtimes_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