imap-processing 0.17.0__py3-none-any.whl → 0.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of imap-processing might be problematic. Click here for more details.

Files changed (141) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/ccsds/excel_to_xtce.py +12 -0
  4. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -6
  5. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +312 -274
  6. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +39 -28
  7. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1048 -183
  8. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +12 -0
  10. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +5 -0
  11. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +10 -4
  12. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +163 -100
  13. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +4 -4
  14. imap_processing/cdf/config/imap_ialirt_l1_variable_attrs.yaml +97 -54
  15. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  16. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +44 -44
  17. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +77 -61
  18. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +30 -0
  19. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  20. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  21. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +99 -2
  22. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  23. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +60 -0
  24. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +99 -11
  25. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +50 -7
  26. imap_processing/cli.py +121 -44
  27. imap_processing/codice/codice_l1a.py +165 -77
  28. imap_processing/codice/codice_l1b.py +1 -1
  29. imap_processing/codice/codice_l2.py +118 -19
  30. imap_processing/codice/constants.py +1217 -1089
  31. imap_processing/decom.py +1 -4
  32. imap_processing/ena_maps/ena_maps.py +32 -25
  33. imap_processing/ena_maps/utils/naming.py +8 -2
  34. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  35. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  37. imap_processing/glows/ancillary/imap_glows_pipeline_settings_20250923_v002.json +54 -0
  38. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  39. imap_processing/glows/l1b/glows_l1b.py +99 -9
  40. imap_processing/glows/l1b/glows_l1b_data.py +350 -38
  41. imap_processing/glows/l2/glows_l2.py +11 -0
  42. imap_processing/hi/hi_l1a.py +124 -3
  43. imap_processing/hi/hi_l1b.py +154 -71
  44. imap_processing/hi/hi_l2.py +84 -51
  45. imap_processing/hi/utils.py +153 -8
  46. imap_processing/hit/l0/constants.py +3 -0
  47. imap_processing/hit/l0/decom_hit.py +5 -8
  48. imap_processing/hit/l1a/hit_l1a.py +375 -45
  49. imap_processing/hit/l1b/constants.py +5 -0
  50. imap_processing/hit/l1b/hit_l1b.py +61 -131
  51. imap_processing/hit/l2/constants.py +1 -1
  52. imap_processing/hit/l2/hit_l2.py +10 -11
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +32 -1
  55. imap_processing/ialirt/generate_coverage.py +201 -0
  56. imap_processing/ialirt/l0/ialirt_spice.py +5 -2
  57. imap_processing/ialirt/l0/parse_mag.py +337 -29
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/l0/process_swe.py +23 -7
  61. imap_processing/ialirt/process_ephemeris.py +70 -14
  62. imap_processing/ialirt/utils/constants.py +22 -16
  63. imap_processing/ialirt/utils/create_xarray.py +42 -19
  64. imap_processing/idex/idex_constants.py +1 -5
  65. imap_processing/idex/idex_l0.py +2 -2
  66. imap_processing/idex/idex_l1a.py +2 -3
  67. imap_processing/idex/idex_l1b.py +2 -3
  68. imap_processing/idex/idex_l2a.py +130 -4
  69. imap_processing/idex/idex_l2b.py +313 -119
  70. imap_processing/idex/idex_utils.py +1 -3
  71. imap_processing/lo/l0/lo_apid.py +1 -0
  72. imap_processing/lo/l0/lo_science.py +25 -24
  73. imap_processing/lo/l1a/lo_l1a.py +44 -0
  74. imap_processing/lo/l1b/lo_l1b.py +3 -3
  75. imap_processing/lo/l1c/lo_l1c.py +116 -50
  76. imap_processing/lo/l2/lo_l2.py +29 -29
  77. imap_processing/lo/lo_ancillary.py +55 -0
  78. imap_processing/lo/packet_definitions/lo_xtce.xml +5359 -106
  79. imap_processing/mag/constants.py +1 -0
  80. imap_processing/mag/l1a/mag_l1a.py +1 -0
  81. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  82. imap_processing/mag/l1b/mag_l1b.py +3 -2
  83. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  84. imap_processing/mag/l1c/mag_l1c.py +23 -6
  85. imap_processing/mag/l1d/__init__.py +0 -0
  86. imap_processing/mag/l1d/mag_l1d.py +176 -0
  87. imap_processing/mag/l1d/mag_l1d_data.py +725 -0
  88. imap_processing/mag/l2/__init__.py +0 -0
  89. imap_processing/mag/l2/mag_l2.py +25 -20
  90. imap_processing/mag/l2/mag_l2_data.py +199 -130
  91. imap_processing/quality_flags.py +28 -2
  92. imap_processing/spice/geometry.py +101 -36
  93. imap_processing/spice/pointing_frame.py +1 -7
  94. imap_processing/spice/repoint.py +29 -2
  95. imap_processing/spice/spin.py +32 -8
  96. imap_processing/spice/time.py +60 -19
  97. imap_processing/swapi/l1/swapi_l1.py +10 -4
  98. imap_processing/swapi/l2/swapi_l2.py +66 -24
  99. imap_processing/swapi/swapi_utils.py +1 -1
  100. imap_processing/swe/l1b/swe_l1b.py +3 -6
  101. imap_processing/ultra/constants.py +28 -3
  102. imap_processing/ultra/l0/decom_tools.py +15 -8
  103. imap_processing/ultra/l0/decom_ultra.py +35 -11
  104. imap_processing/ultra/l0/ultra_utils.py +102 -12
  105. imap_processing/ultra/l1a/ultra_l1a.py +26 -6
  106. imap_processing/ultra/l1b/cullingmask.py +6 -3
  107. imap_processing/ultra/l1b/de.py +122 -26
  108. imap_processing/ultra/l1b/extendedspin.py +29 -2
  109. imap_processing/ultra/l1b/lookup_utils.py +424 -50
  110. imap_processing/ultra/l1b/quality_flag_filters.py +23 -0
  111. imap_processing/ultra/l1b/ultra_l1b_culling.py +356 -5
  112. imap_processing/ultra/l1b/ultra_l1b_extended.py +534 -90
  113. imap_processing/ultra/l1c/helio_pset.py +127 -7
  114. imap_processing/ultra/l1c/l1c_lookup_utils.py +256 -0
  115. imap_processing/ultra/l1c/spacecraft_pset.py +90 -15
  116. imap_processing/ultra/l1c/ultra_l1c.py +6 -0
  117. imap_processing/ultra/l1c/ultra_l1c_culling.py +85 -0
  118. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +446 -341
  119. imap_processing/ultra/l2/ultra_l2.py +0 -1
  120. imap_processing/ultra/utils/ultra_l1_utils.py +40 -3
  121. imap_processing/utils.py +3 -4
  122. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/METADATA +3 -3
  123. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/RECORD +126 -126
  124. imap_processing/idex/idex_l2c.py +0 -250
  125. imap_processing/spice/kernels.py +0 -187
  126. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +0 -526
  127. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +0 -526
  128. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +0 -526
  129. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +0 -524
  130. imap_processing/ultra/lookup_tables/EgyNorm.mem.csv +0 -32769
  131. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  132. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  133. imap_processing/ultra/lookup_tables/dps_grid45_compressed.cdf +0 -0
  134. imap_processing/ultra/lookup_tables/ultra45_back-pos-luts.csv +0 -4097
  135. imap_processing/ultra/lookup_tables/ultra45_tdc_norm.csv +0 -2050
  136. imap_processing/ultra/lookup_tables/ultra90_back-pos-luts.csv +0 -4097
  137. imap_processing/ultra/lookup_tables/ultra90_tdc_norm.csv +0 -2050
  138. imap_processing/ultra/lookup_tables/yadjust.csv +0 -257
  139. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/LICENSE +0 -0
  140. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/WHEEL +0 -0
  141. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -3,16 +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
6
+ import xarray as xr
7
7
  from numpy.typing import NDArray
8
- from scipy.interpolate import interp1d
8
+ from scipy import interpolate
9
9
 
10
10
  from imap_processing.spice.geometry import (
11
11
  SpiceFrame,
12
12
  cartesian_to_spherical,
13
13
  imap_state,
14
14
  )
15
+ from imap_processing.spice.spin import get_spacecraft_spin_phase, get_spin_angle
15
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
+ )
16
30
 
17
31
  # TODO: add species binning.
18
32
  FILLVAL_FLOAT32 = -1.0e31
@@ -31,15 +45,8 @@ def build_energy_bins() -> tuple[list[tuple[float, float]], np.ndarray, np.ndarr
31
45
  energy_bin_geometric_means : np.ndarray
32
46
  Array of geometric means of energy bins.
33
47
  """
34
- # Calculate energy step
35
- energy_step = (1 + UltraConstants.ALPHA / 2) / (1 - UltraConstants.ALPHA / 2)
36
-
37
48
  # Create energy bins.
38
- energy_bin_edges = UltraConstants.ENERGY_START * energy_step ** np.arange(
39
- UltraConstants.N_BINS + 1
40
- )
41
- # Add a zero to the left side for outliers and round to nearest 3 decimal places.
42
- energy_bin_edges = np.around(np.insert(energy_bin_edges, 0, 0), 3)
49
+ energy_bin_edges = np.array(UltraConstants.CULLING_ENERGY_BIN_EDGES)
43
50
  energy_midpoints = (energy_bin_edges[:-1] + energy_bin_edges[1:]) / 2
44
51
 
45
52
  intervals = [
@@ -72,7 +79,7 @@ def get_energy_delta_minus_plus() -> tuple[NDArray, NDArray]:
72
79
  """
73
80
  bins, _, bin_geom_means = build_energy_bins()
74
81
  bins_energy_delta_plus, bins_energy_delta_minus = [], []
75
- 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):
76
83
  bins_energy_delta_plus.append(bin_edges[1] - bin_geom_mean)
77
84
  bins_energy_delta_minus.append(bin_geom_mean - bin_edges[0])
78
85
  return abs(np.array(bins_energy_delta_minus)), abs(np.array(bins_energy_delta_plus))
@@ -86,7 +93,7 @@ def get_spacecraft_histogram(
86
93
  nested: bool = False,
87
94
  ) -> tuple[NDArray, NDArray, NDArray, NDArray]:
88
95
  """
89
- Compute a 3D histogram of the particle data using HEALPix binning.
96
+ Compute a 2D histogram of the particle data using HEALPix binning.
90
97
 
91
98
  Parameters
92
99
  ----------
@@ -105,7 +112,7 @@ def get_spacecraft_histogram(
105
112
  Returns
106
113
  -------
107
114
  hist : np.ndarray
108
- A 3D histogram array with shape (n_pix, n_energy_bins).
115
+ A 2D histogram array with shape (n_pix, n_energy_bins).
109
116
  latitude : np.ndarray
110
117
  Array of latitude values.
111
118
  longitude : np.ndarray
@@ -151,165 +158,254 @@ def get_spacecraft_histogram(
151
158
  return hist, latitude, longitude, n_pix
152
159
 
153
160
 
154
- def get_helio_histogram(
155
- time: NDArray,
156
- vhat: NDArray,
157
- energy: NDArray,
158
- energy_bin_edges: list[tuple[float, float]],
159
- nside: int = 128,
160
- nested: bool = False,
161
- ) -> tuple[NDArray, NDArray, NDArray, NDArray]:
161
+ def get_spacecraft_count_rate_uncertainty(hist: NDArray, exposure: NDArray) -> NDArray:
162
162
  """
163
- Compute a 3D histogram of the particle data using HEALPix binning.
163
+ Calculate the count rate uncertainty for HEALPix-binned data.
164
164
 
165
165
  Parameters
166
166
  ----------
167
- time : np.ndarray
168
- Median time of pointing in et.
169
- vhat : tuple[np.ndarray, np.ndarray, np.ndarray]
170
- The x,y,z-components of the unit velocity vector.
171
- energy : np.ndarray
172
- The particle energy.
173
- energy_bin_edges : list[tuple[float, float]]
174
- Array of energy bin edges.
175
- nside : int, optional
176
- The nside parameter of the Healpix tessellation.
177
- Default is 128.
178
- nested : bool, optional
179
- Whether the Healpix tessellation is nested. Default is False.
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).
180
171
 
181
172
  Returns
182
173
  -------
183
- hist : np.ndarray
184
- A 3D histogram array with shape (n_pix, n_energy_bins).
185
- latitude : np.ndarray
186
- Array of latitude values.
187
- longitude : np.ndarray
188
- Array of longitude values.
189
- n_pix : int
190
- Number of healpix pixels.
174
+ count_rate_uncertainty : NDArray
175
+ Rate uncertainty with shape (n_pix, n_energy_bins) (counts/sec).
191
176
 
192
177
  Notes
193
178
  -----
194
- The histogram will work properly for overlapping energy bins, i.e.
195
- the same energy value can fall into multiple bins if the intervals overlap.
196
-
197
- azimuthal angle [0, 360], elevation angle [-90, 90]
179
+ These calculations were based on Eqn 15 from the IMAP-Ultra Algorithm Document.
198
180
  """
199
- # Compute number of HEALPix pixels that cover the sphere
200
- n_pix = hp.nside2npix(nside)
181
+ count_uncertainty = np.sqrt(hist)
201
182
 
202
- # Calculate the corresponding longitude (az) latitude (el)
203
- # center coordinates
204
- longitude, latitude = hp.pix2ang(nside, np.arange(n_pix), lonlat=True)
183
+ rate_uncertainty = np.zeros_like(hist)
184
+ valid = exposure > 0
185
+ rate_uncertainty[valid] = count_uncertainty[valid] / exposure[valid]
205
186
 
206
- # The Cartesian state vector representing the position and velocity of the
207
- # IMAP spacecraft.
208
- state = imap_state(time, ref_frame=SpiceFrame.IMAP_DPS)
187
+ return rate_uncertainty
209
188
 
210
- # Extract the velocity part of the state vector
211
- spacecraft_velocity = state[3:6]
212
189
 
213
- # Initialize histogram: (n_energy_bins, n_HEALPix pixels)
214
- hist = np.zeros((len(energy_bin_edges), n_pix))
190
+ def get_deadtime_ratios(sectored_rates_ds: xr.Dataset) -> xr.DataArray:
191
+ """
192
+ Compute the dead time ratio at each sector.
215
193
 
216
- # Bin data in energy & HEALPix space
217
- for i, (e_min, e_max) in enumerate(energy_bin_edges):
218
- # Convert the midpoint energy to a velocity (km/s).
219
- # Based on kinetic energy equation: E = 1/2 * m * v^2.
220
- energy_midpoint = (e_min + e_max) / 2
221
- energy_velocity = (
222
- np.sqrt(2 * energy_midpoint * UltraConstants.KEV_J / UltraConstants.MASS_H)
223
- / 1e3
224
- )
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.
225
199
 
226
- # Use Galilean Transform to transform the velocity wrt spacecraft
227
- # to the velocity wrt heliosphere.
228
- # energy_velocity * cartesian -> apply the magnitude of the velocity
229
- # to every position on the grid in the despun grid.
230
- mask = (energy >= e_min) & (energy < e_max)
231
- vx, vy, vz = vhat.T
200
+ Parameters
201
+ ----------
202
+ sectored_rates_ds : xarray.Dataset
203
+ Dataset containing sector mode image rates data.
232
204
 
233
- # Select only the particles that fall within the energy bin.
234
- vx_bin, vy_bin, vz_bin = vx[mask], vy[mask], vz[mask]
235
- vhat_bin = np.stack((vx_bin, vy_bin, vz_bin), axis=1)
236
- helio_velocity = spacecraft_velocity.reshape(1, 3) + energy_velocity * vhat_bin
205
+ Returns
206
+ -------
207
+ dead_time_ratio : xarray.DataArray
208
+ Dead time correction factor for each sector.
209
+ """
210
+ # Compute the correction factor at each sector
211
+ a = sectored_rates_ds.fifo_valid_events / (
212
+ 1
213
+ - (sectored_rates_ds.event_active_time + 2 * sectored_rates_ds.start_pos) * 1e-7
214
+ )
237
215
 
238
- # Normalized vectors representing the direction of the heliocentric velocity.
239
- helio_normalized = -helio_velocity / np.linalg.norm(
240
- helio_velocity, axis=1, keepdims=True
241
- )
216
+ start_full = sectored_rates_ds.start_rf + sectored_rates_ds.start_lf
217
+ b = a * np.exp(start_full * 1e-7 * 5)
242
218
 
243
- # Convert Cartesian heliocentric vectors into spherical coordinates.
244
- # Result: azimuth (longitude) and elevation (latitude) in degrees.
245
- helio_spherical = cartesian_to_spherical(np.squeeze(helio_normalized))
246
- helio_spherical = np.atleast_2d(helio_spherical)
247
- az, el = helio_spherical[:, 1], helio_spherical[:, 2]
219
+ coin_stop_nd = (
220
+ sectored_rates_ds.coin_tn
221
+ + sectored_rates_ds.coin_bn
222
+ - sectored_rates_ds.stop_tn
223
+ - sectored_rates_ds.stop_bn
224
+ )
248
225
 
249
- # Convert azimuth/elevation directions to HEALPix pixel indices.
250
- hpix_idx = hp.ang2pix(nside, az, el, nest=nested, lonlat=True)
226
+ corrected_valid_events = b * np.exp(1e-7 * 8 * coin_stop_nd)
251
227
 
252
- # Only count the events that fall within the energy bin
253
- hist[i, :] += np.bincount(hpix_idx, minlength=n_pix).astype(np.float64)
228
+ # Compute dead time ratio
229
+ dead_time_ratios = sectored_rates_ds.fifo_valid_events / corrected_valid_events
254
230
 
255
- return hist, latitude, longitude, n_pix
231
+ return dead_time_ratios
256
232
 
257
233
 
258
- def get_spacecraft_background_rates(
259
- nside: int = 128,
260
- ) -> NDArray:
234
+ def get_sectored_rates(rates_ds: xr.Dataset, params_ds: xr.Dataset) -> xr.Dataset:
261
235
  """
262
- Calculate background rates.
236
+ Filter rates dataset to only include sector mode data.
263
237
 
264
238
  Parameters
265
239
  ----------
266
- nside : int, optional
267
- The nside parameter of the Healpix tessellation (default is 128).
240
+ rates_ds : xarray.Dataset
241
+ Dataset containing image rates data.
242
+ params_ds : xarray.Dataset
243
+ Dataset containing image parameters data.
268
244
 
269
245
  Returns
270
246
  -------
271
- background_rates : np.ndarray
272
- Array of background rates.
273
-
274
- Notes
275
- -----
276
- This is a placeholder.
277
- TODO: background rates to be provided by IT.
247
+ rates : xarray.Dataset
248
+ Rates dataset with only the sector mode data.
278
249
  """
279
- npix = hp.nside2npix(nside)
280
- _, energy_midpoints, _ = build_energy_bins()
281
- background = np.zeros((len(energy_midpoints), npix))
282
- return background
250
+ # Find indices in which the parameters dataset, indicates that ULTRA was in
251
+ # sector mode. At the normal 15-second spin period, each 24° sector takes ~1 second.
252
+
253
+ # This means that data was collected as a function of spin allowing for fine grained
254
+ # rate analysis.
255
+ sector_mode_start_inds = np.where(params_ds["imageratescadence"] == 3)[0]
256
+ # get the sector mode start and stop indices
257
+ sector_mode_stop_inds = sector_mode_start_inds + 1
258
+ # get the sector mode start and stop times
259
+ mode_3_start = params_ds["epoch"].values[sector_mode_start_inds]
260
+
261
+ # if the last mode is a sector mode, we can assume that the sector data goes through
262
+ # the end of the dataset, so we append np.inf to the end of the last time range.
263
+ if sector_mode_stop_inds[-1] == len(params_ds["epoch"]):
264
+ mode_3_end = np.append(
265
+ params_ds["epoch"].values[sector_mode_stop_inds[:-1]], np.inf
266
+ )
267
+ else:
268
+ mode_3_end = params_ds["epoch"].values[sector_mode_stop_inds]
283
269
 
270
+ # Build a list of conditions for each sector mode time range
271
+ conditions = [
272
+ (rates_ds["epoch"] >= start) & (rates_ds["epoch"] < end)
273
+ for start, end in zip(mode_3_start, mode_3_end, strict=False)
274
+ ]
284
275
 
285
- def get_helio_background_rates(
286
- nside: int = 128,
287
- ) -> NDArray:
276
+ sector_mode_mask = np.logical_or.reduce(conditions)
277
+ return rates_ds.isel(epoch=sector_mode_mask)
278
+
279
+
280
+ def get_deadtime_ratios_by_spin_phase(
281
+ sectored_rates: xr.Dataset,
282
+ ) -> np.ndarray:
288
283
  """
289
- Calculate background rates.
284
+ Calculate nominal deadtime ratios at every spin phase step (1ms res).
290
285
 
291
286
  Parameters
292
287
  ----------
293
- nside : int, optional
294
- The nside parameter of the Healpix tessellation (default is 128).
288
+ sectored_rates : xarray.Dataset
289
+ Dataset containing sector mode image rates data.
295
290
 
296
291
  Returns
297
292
  -------
298
- background_rates : np.ndarray
299
- Array of background rates.
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
+ )
300
323
 
301
- Notes
302
- -----
303
- This is a placeholder.
304
- TODO: background rates to be provided by IT.
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:
305
349
  """
306
- npix = hp.nside2npix(nside)
307
- _, energy_midpoints, _ = build_energy_bins()
308
- background = np.zeros((len(energy_midpoints), npix))
309
- return background
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
310
400
 
311
401
 
312
- def get_spacecraft_exposure_times(constant_exposure: pandas.DataFrame) -> NDArray:
402
+ def get_spacecraft_exposure_times(
403
+ constant_exposure: pandas.DataFrame,
404
+ rates_dataset: xr.Dataset,
405
+ params_dataset: xr.Dataset,
406
+ pixels_below_scattering: list[list],
407
+ boundary_scale_factors: NDArray,
408
+ ) -> tuple[NDArray, NDArray]:
313
409
  """
314
410
  Compute exposure times for HEALPix pixels.
315
411
 
@@ -317,6 +413,17 @@ def get_spacecraft_exposure_times(constant_exposure: pandas.DataFrame) -> NDArra
317
413
  ----------
318
414
  constant_exposure : pandas.DataFrame
319
415
  Exposure data.
416
+ rates_dataset : xarray.Dataset
417
+ Dataset containing image rates data.
418
+ params_dataset : xarray.Dataset
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.
320
427
 
321
428
  Returns
322
429
  -------
@@ -324,31 +431,145 @@ def get_spacecraft_exposure_times(constant_exposure: pandas.DataFrame) -> NDArra
324
431
  Total exposure times of pixels in a
325
432
  Healpix tessellation of the sky
326
433
  in the pointing (dps) frame.
434
+ nominal_deadtime_ratios : np.ndarray
435
+ Deadtime ratios at each spin phase step (1ms res).
327
436
  """
328
437
  # TODO: use the universal spin table and
329
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)
330
441
  exposure_pointing = (
331
442
  constant_exposure["Exposure Time"] * 5760
332
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
333
451
 
334
- return exposure_pointing
335
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.
336
465
 
337
- def get_helio_exposure_times(
338
- time: np.ndarray,
339
- 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,
340
551
  nside: int = 128,
341
552
  nested: bool = False,
342
- ) -> NDArray:
553
+ ) -> tuple[NDArray, NDArray, NDArray]:
343
554
  """
344
- 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.
345
558
 
346
559
  Parameters
347
560
  ----------
348
- time : np.ndarray
561
+ time : float
349
562
  Median time of pointing in et.
350
- df_exposure : pd.DataFrame
351
- 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).
352
573
  nside : int, optional
353
574
  The nside parameter of the Healpix tessellation (default is 128).
354
575
  nested : bool, optional
@@ -357,18 +578,18 @@ def get_helio_exposure_times(
357
578
  Returns
358
579
  -------
359
580
  helio_exposure : np.ndarray
360
- 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).
361
586
 
362
587
  Notes
363
588
  -----
364
589
  These calculations are performed once per pointing.
365
590
  """
366
591
  # Get energy midpoints.
367
- _, energy_midpoints, _ = build_energy_bins()
368
- # Extract (RA/Dec) and exposure from the spacecraft frame.
369
- ra = df_exposure["Right Ascension (deg)"].values
370
- dec = df_exposure["Declination (deg)"].values
371
- exposure_flat = df_exposure["Exposure Time"].values
592
+ _, _, energy_bin_geometric_means = build_energy_bins()
372
593
 
373
594
  # The Cartesian state vector representing the position and velocity of the
374
595
  # IMAP spacecraft.
@@ -379,19 +600,28 @@ def get_helio_exposure_times(
379
600
  # Convert (RA, Dec) angles into 3D unit vectors.
380
601
  # Each unit vector represents a direction in the sky where the spacecraft observed
381
602
  # and accumulated exposure time.
603
+ npix = hp.nside2npix(nside)
382
604
  unit_dirs = hp.ang2vec(ra, dec, lonlat=True).T # Shape (N, 3)
383
-
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
+ )
384
613
  # Initialize output array.
385
614
  # Each row corresponds to a HEALPix pixel, and each column to an energy bin.
386
- npix = hp.nside2npix(nside)
387
- helio_exposure = np.zeros((npix, len(energy_midpoints)))
615
+ helio_exposure = np.zeros(shape)
616
+ helio_efficiency = np.zeros(shape)
617
+ helio_geometric_factors = np.zeros(shape)
388
618
 
389
619
  # Loop through energy bins and compute transformed exposure.
390
- for i, energy_midpoint in enumerate(energy_midpoints):
620
+ for i, energy_mean in enumerate(energy_bin_geometric_means):
391
621
  # Convert the midpoint energy to a velocity (km/s).
392
622
  # Based on kinetic energy equation: E = 1/2 * m * v^2.
393
623
  energy_velocity = (
394
- np.sqrt(2 * energy_midpoint * UltraConstants.KEV_J / UltraConstants.MASS_H)
624
+ np.sqrt(2 * energy_mean * UltraConstants.KEV_J / UltraConstants.MASS_H)
395
625
  / 1e3
396
626
  )
397
627
 
@@ -414,222 +644,97 @@ def get_helio_exposure_times(
414
644
  # Convert azimuth/elevation directions to HEALPix pixel indices.
415
645
  hpix_idx = hp.ang2pix(nside, az, el, nest=nested, lonlat=True)
416
646
 
417
- # Accumulate exposure values into HEALPix pixels for this energy bin.
418
- helio_exposure[:, i] = np.bincount(
419
- hpix_idx, weights=exposure_flat, minlength=npix
647
+ # Accumulate exposure, eff, and gf values into HEALPix pixels for this energy
648
+ # bin.
649
+ helio_exposure[i, :] = np.bincount(
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
420
657
  )
421
658
 
422
- return helio_exposure
423
-
424
-
425
- def get_spacecraft_sensitivity(
426
- efficiencies: pandas.DataFrame,
427
- geometric_function: pandas.DataFrame,
428
- ) -> tuple[pandas.DataFrame, NDArray, NDArray, NDArray]:
429
- """
430
- Compute sensitivity as efficiency * geometric factor.
431
-
432
- Parameters
433
- ----------
434
- efficiencies : pandas.DataFrame
435
- Efficiencies at different energy levels.
436
- geometric_function : pandas.DataFrame
437
- Geometric function.
438
-
439
- Returns
440
- -------
441
- pointing_sensitivity : pandas.DataFrame
442
- Sensitivity with dimensions (HEALPIX pixel_number, energy).
443
- energy_vals : NDArray
444
- Energy values of dataframe.
445
- right_ascension : NDArray
446
- Right ascension (longitude/azimuth) values of dataframe (0 - 360 degrees).
447
- declination : NDArray
448
- Declination (latitude/elevation) values of dataframe (-90 to 90 degrees).
449
- """
450
- # Exclude "Right Ascension (deg)" and "Declination (deg)" from the multiplication
451
- energy_columns = [
452
- col
453
- for col in efficiencies.columns
454
- if col not in ["Right Ascension (deg)", "Declination (deg)"]
455
- ]
456
- sensitivity = efficiencies[energy_columns].mul(
457
- geometric_function["Response (cm2-sr)"].values, axis=0
458
- )
459
-
460
- right_ascension = efficiencies["Right Ascension (deg)"]
461
- declination = efficiencies["Declination (deg)"]
462
-
463
- energy_vals = np.array([float(col.replace("keV", "")) for col in energy_columns])
464
-
465
- return sensitivity, energy_vals, right_ascension, declination
466
-
467
-
468
- def grid_sensitivity(
469
- efficiencies: pandas.DataFrame,
470
- geometric_function: pandas.DataFrame,
471
- energy: float,
472
- ) -> NDArray:
473
- """
474
- Grid the sensitivity.
475
-
476
- Parameters
477
- ----------
478
- efficiencies : pandas.DataFrame
479
- Efficiencies at different energy levels.
480
- geometric_function : pandas.DataFrame
481
- Geometric function.
482
- energy : float
483
- Energy to which we are interpolating.
484
-
485
- Returns
486
- -------
487
- interpolated_sensitivity : np.ndarray
488
- Sensitivity with dimensions (HEALPIX pixel_number, 1).
489
- """
490
- sensitivity, energy_vals, right_ascension, declination = get_spacecraft_sensitivity(
491
- efficiencies, geometric_function
492
- )
493
-
494
- # Create interpolator over energy dimension for each pixel (axis=1)
495
- interp_func = interp1d(
496
- energy_vals,
497
- sensitivity.values,
498
- axis=1,
499
- bounds_error=False,
500
- fill_value=np.nan,
501
- )
502
-
503
- # Interpolate to energy
504
- interpolated = interp_func(energy)
505
- interpolated = np.where(np.isnan(interpolated), FILLVAL_FLOAT32, interpolated)
506
-
507
- return interpolated
508
-
509
-
510
- def interpolate_sensitivity(
511
- efficiencies: pd.DataFrame,
512
- geometric_function: pd.DataFrame,
513
- nside: int = 128,
514
- ) -> NDArray:
515
- """
516
- Interpolate the sensitivity and bin it in HEALPix space.
517
-
518
- Parameters
519
- ----------
520
- efficiencies : pandas.DataFrame
521
- Efficiencies at different energy levels.
522
- geometric_function : pandas.DataFrame
523
- Geometric function.
524
- nside : int, optional
525
- Healpix nside resolution (default is 128).
526
-
527
- Returns
528
- -------
529
- interpolated_sensitivity : np.ndarray
530
- Array of shape (n_energy_bins, n_healpix_pixels).
531
- """
532
- _, _, energy_bin_geometric_means = build_energy_bins()
533
- npix = hp.nside2npix(nside)
534
-
535
- interpolated_sensitivity = np.full(
536
- (len(energy_bin_geometric_means), npix), FILLVAL_FLOAT32
537
- )
538
-
539
- for i, energy in enumerate(energy_bin_geometric_means):
540
- pixel_sensitivity = grid_sensitivity(
541
- efficiencies, geometric_function, energy
542
- ).flatten()
543
- interpolated_sensitivity[i, :] = pixel_sensitivity
544
-
545
- return interpolated_sensitivity
659
+ return helio_exposure, helio_efficiency, helio_geometric_factors
546
660
 
547
661
 
548
- def get_helio_sensitivity(
549
- time: np.ndarray,
550
- efficiencies: pandas.DataFrame,
551
- 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,
552
668
  nside: int = 128,
553
- nested: bool = False,
554
669
  ) -> NDArray:
555
670
  """
556
- Compute a 2D (Healpix index, energy) array of sensitivity in the helio frame.
671
+ Calculate background rates based on the provided parameters.
557
672
 
558
673
  Parameters
559
674
  ----------
560
- time : np.ndarray
561
- Median time of pointing in et.
562
- efficiencies : pandas.DataFrame
563
- Efficiencies at different energy levels.
564
- geometric_function : pandas.DataFrame
565
- 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.
566
687
  nside : int, optional
567
688
  The nside parameter of the Healpix tessellation (default is 128).
568
- nested : bool, optional
569
- Whether the Healpix tessellation is nested (default is False).
570
689
 
571
690
  Returns
572
691
  -------
573
- helio_sensitivity : np.ndarray
574
- 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.
575
694
 
576
695
  Notes
577
696
  -----
578
- These calculations are performed once per pointing.
697
+ See Eqn. 3, 8, and 20 in the Algorithm Document for the equation.
579
698
  """
580
- # Get energy midpoints.
581
- _, energy_midpoints, _ = build_energy_bins()
582
-
583
- # Get sensitivity on the spacecraft grid
584
- _, _, ra, dec = get_spacecraft_sensitivity(efficiencies, geometric_function)
585
-
586
- # The Cartesian state vector representing the position and velocity of the
587
- # IMAP spacecraft.
588
- state = imap_state(time, ref_frame=SpiceFrame.IMAP_DPS)
589
-
590
- # Extract the velocity part of the state vector
591
- spacecraft_velocity = state[3:6]
592
- # Convert (RA, Dec) angles into 3D unit vectors.
593
- # Each unit vector represents a direction in the sky where the spacecraft observed
594
- # and accumulated sensitivity.
595
- unit_dirs = hp.ang2vec(ra, dec, lonlat=True).T # Shape (N, 3)
596
-
597
- # Initialize output array.
598
- # Each row corresponds to a HEALPix pixel, and each column to an energy bin.
599
- npix = hp.nside2npix(nside)
600
- helio_sensitivity = np.zeros((npix, len(energy_midpoints)))
601
-
602
- # Loop through energy bins and compute transformed sensitivity.
603
- for i, energy in enumerate(energy_midpoints):
604
- # Convert the midpoint energy to a velocity (km/s).
605
- # Based on kinetic energy equation: E = 1/2 * m * v^2.
606
- energy_velocity = (
607
- np.sqrt(2 * energy * UltraConstants.KEV_J / UltraConstants.MASS_H) / 1e3
608
- )
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
+ )
609
706
 
610
- # Use Galilean Transform to transform the velocity wrt spacecraft
611
- # to the velocity wrt heliosphere.
612
- # energy_velocity * cartesian -> apply the magnitude of the velocity
613
- # to every position on the grid in the despun grid.
614
- helio_velocity = spacecraft_velocity.reshape(1, 3) + energy_velocity * unit_dirs
707
+ # Get dmin for PH (mm).
708
+ dmin_ctof = UltraConstants.DMIN_PH_CTOF
615
709
 
616
- # Normalized vectors representing the direction of the heliocentric velocity.
617
- helio_normalized = helio_velocity / np.linalg.norm(
618
- helio_velocity, axis=1, keepdims=True
619
- )
620
-
621
- # Convert Cartesian heliocentric vectors into spherical coordinates.
622
- # Result: azimuth (longitude) and elevation (latitude) in degrees.
623
- helio_spherical = cartesian_to_spherical(helio_normalized)
624
- az, el = helio_spherical[:, 1], helio_spherical[:, 2]
710
+ # Compute number of HEALPix pixels that cover the sphere
711
+ n_pix = hp.nside2npix(nside)
625
712
 
626
- # Convert azimuth/elevation directions to HEALPix pixel indices.
627
- hpix_idx = hp.ang2pix(nside, az, el, nest=nested, lonlat=True)
628
- 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))
629
715
 
630
- # Accumulate sensitivity values into HEALPix pixels for this energy bin.
631
- helio_sensitivity[:, i] = np.bincount(
632
- hpix_idx, weights=gridded_sensitivity, minlength=npix
633
- )
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])
634
721
 
635
- 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