imap-processing 1.0.0__py3-none-any.whl → 1.0.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.
Files changed (68) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +13 -1
  3. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +97 -254
  4. imap_processing/cdf/config/imap_codice_l2-hi-omni_variable_attrs.yaml +635 -0
  5. imap_processing/cdf/config/imap_codice_l2-hi-sectored_variable_attrs.yaml +422 -0
  6. imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +29 -22
  7. imap_processing/cdf/config/imap_enamaps_l2-healpix_variable_attrs.yaml +2 -0
  8. imap_processing/cdf/config/imap_enamaps_l2-rectangular_variable_attrs.yaml +12 -2
  9. imap_processing/cdf/config/imap_swapi_variable_attrs.yaml +2 -13
  10. imap_processing/cdf/utils.py +2 -2
  11. imap_processing/cli.py +10 -27
  12. imap_processing/codice/codice_l1a_lo_angular.py +362 -0
  13. imap_processing/codice/codice_l1a_lo_species.py +282 -0
  14. imap_processing/codice/codice_l1b.py +62 -97
  15. imap_processing/codice/codice_l2.py +801 -174
  16. imap_processing/codice/codice_new_l1a.py +64 -0
  17. imap_processing/codice/constants.py +96 -0
  18. imap_processing/codice/utils.py +270 -0
  19. imap_processing/ena_maps/ena_maps.py +157 -95
  20. imap_processing/ena_maps/utils/coordinates.py +5 -0
  21. imap_processing/ena_maps/utils/corrections.py +450 -0
  22. imap_processing/ena_maps/utils/map_utils.py +143 -42
  23. imap_processing/ena_maps/utils/naming.py +3 -1
  24. imap_processing/hi/hi_l1c.py +34 -12
  25. imap_processing/hi/hi_l2.py +82 -44
  26. imap_processing/ialirt/constants.py +7 -1
  27. imap_processing/ialirt/generate_coverage.py +3 -1
  28. imap_processing/ialirt/l0/parse_mag.py +1 -0
  29. imap_processing/ialirt/l0/process_codice.py +66 -0
  30. imap_processing/ialirt/l0/process_hit.py +1 -0
  31. imap_processing/ialirt/l0/process_swapi.py +1 -0
  32. imap_processing/ialirt/l0/process_swe.py +2 -0
  33. imap_processing/ialirt/process_ephemeris.py +6 -2
  34. imap_processing/ialirt/utils/create_xarray.py +4 -2
  35. imap_processing/idex/idex_l2a.py +2 -2
  36. imap_processing/idex/idex_l2b.py +1 -1
  37. imap_processing/lo/l1c/lo_l1c.py +62 -4
  38. imap_processing/lo/l2/lo_l2.py +85 -15
  39. imap_processing/mag/l1a/mag_l1a.py +2 -2
  40. imap_processing/mag/l1a/mag_l1a_data.py +71 -13
  41. imap_processing/mag/l1c/interpolation_methods.py +34 -13
  42. imap_processing/mag/l1c/mag_l1c.py +117 -67
  43. imap_processing/mag/l1d/mag_l1d_data.py +3 -1
  44. imap_processing/quality_flags.py +1 -0
  45. imap_processing/spice/geometry.py +11 -9
  46. imap_processing/spice/pointing_frame.py +77 -50
  47. imap_processing/swapi/constants.py +4 -0
  48. imap_processing/swapi/l1/swapi_l1.py +59 -24
  49. imap_processing/swapi/l2/swapi_l2.py +17 -3
  50. imap_processing/swe/utils/swe_constants.py +7 -7
  51. imap_processing/ultra/l1a/ultra_l1a.py +121 -72
  52. imap_processing/ultra/l1b/de.py +57 -1
  53. imap_processing/ultra/l1b/extendedspin.py +1 -1
  54. imap_processing/ultra/l1b/ultra_l1b_annotated.py +0 -1
  55. imap_processing/ultra/l1b/ultra_l1b_culling.py +2 -2
  56. imap_processing/ultra/l1b/ultra_l1b_extended.py +25 -12
  57. imap_processing/ultra/l1c/helio_pset.py +29 -6
  58. imap_processing/ultra/l1c/l1c_lookup_utils.py +4 -2
  59. imap_processing/ultra/l1c/spacecraft_pset.py +10 -6
  60. imap_processing/ultra/l1c/ultra_l1c.py +6 -6
  61. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +82 -20
  62. imap_processing/ultra/l2/ultra_l2.py +2 -2
  63. imap_processing-1.0.2.dist-info/METADATA +121 -0
  64. {imap_processing-1.0.0.dist-info → imap_processing-1.0.2.dist-info}/RECORD +67 -61
  65. imap_processing-1.0.0.dist-info/METADATA +0 -120
  66. {imap_processing-1.0.0.dist-info → imap_processing-1.0.2.dist-info}/LICENSE +0 -0
  67. {imap_processing-1.0.0.dist-info → imap_processing-1.0.2.dist-info}/WHEEL +0 -0
  68. {imap_processing-1.0.0.dist-info → imap_processing-1.0.2.dist-info}/entry_points.txt +0 -0
@@ -14,7 +14,7 @@ from scipy.interpolate import LinearNDInterpolator, RegularGridInterpolator
14
14
 
15
15
  from imap_processing.quality_flags import ImapDEOutliersUltraFlags
16
16
  from imap_processing.spice.spin import get_spin_data
17
- from imap_processing.spice.time import sct_to_et
17
+ from imap_processing.spice.time import met_to_ttj2000ns, ttj2000ns_to_et
18
18
  from imap_processing.ultra.constants import UltraConstants
19
19
  from imap_processing.ultra.l1b.lookup_utils import (
20
20
  get_angular_profiles,
@@ -592,7 +592,7 @@ def get_ssd_tof(
592
592
  tof : np.ndarray
593
593
  Time of flight (tenths of a nanosecond).
594
594
  """
595
- _, tof_offset, ssd_number = get_ssd_back_position_and_tof_offset(
595
+ _, tof_offset, _ssd_number = get_ssd_back_position_and_tof_offset(
596
596
  de_dataset, sensor, ancillary_files
597
597
  )
598
598
  indices = np.nonzero(np.isin(de_dataset["stop_type"], [StopType.SSD.value]))[0]
@@ -711,7 +711,7 @@ def get_energy_pulse_height(
711
711
  ylut[indices_bottom] = (yb[indices_bottom] / 100 + 82 / 2) * 32 / 82 # mm
712
712
 
713
713
  ph_correction_top, updated_flags_top = get_ph_corrected(
714
- "ultra45",
714
+ sensor,
715
715
  "tp",
716
716
  ancillary_files,
717
717
  np.round(xlut[indices_top]),
@@ -720,7 +720,7 @@ def get_energy_pulse_height(
720
720
  )
721
721
  quality_flags[indices_top] = updated_flags_top
722
722
  ph_correction_bottom, updated_flags_bottom = get_ph_corrected(
723
- "ultra45",
723
+ sensor,
724
724
  "bt",
725
725
  ancillary_files,
726
726
  np.round(xlut[indices_bottom]),
@@ -996,6 +996,7 @@ def get_eventtimes(
996
996
  t_spin_period_sec * phase_angle/720
997
997
  """
998
998
  spin_df = get_spin_data()
999
+
999
1000
  index = np.searchsorted(spin_df["spin_number"].values, spin)
1000
1001
  spin_starts = (
1001
1002
  spin_df["spin_start_sec_sclk"].values[index]
@@ -1003,10 +1004,13 @@ def get_eventtimes(
1003
1004
  )
1004
1005
 
1005
1006
  spin_period_sec = spin_df["spin_period_sec"].values[index]
1006
-
1007
1007
  event_times = spin_starts + spin_period_sec * (phase_angle / 720)
1008
1008
 
1009
- return sct_to_et(event_times), sct_to_et(spin_starts), spin_period_sec
1009
+ return (
1010
+ ttj2000ns_to_et(met_to_ttj2000ns(event_times)),
1011
+ ttj2000ns_to_et(met_to_ttj2000ns(spin_starts)),
1012
+ spin_period_sec,
1013
+ )
1010
1014
 
1011
1015
 
1012
1016
  def interpolate_fwhm(
@@ -1108,7 +1112,9 @@ def get_fwhm(
1108
1112
  return phi_interp, theta_interp
1109
1113
 
1110
1114
 
1111
- def get_efficiency_interpolator(ancillary_files: dict) -> RegularGridInterpolator:
1115
+ def get_efficiency_interpolator(
1116
+ ancillary_files: dict,
1117
+ ) -> tuple[RegularGridInterpolator, tuple, tuple]:
1112
1118
  """
1113
1119
  Return a callable function that interpolates efficiency values for each event.
1114
1120
 
@@ -1119,8 +1125,12 @@ def get_efficiency_interpolator(ancillary_files: dict) -> RegularGridInterpolato
1119
1125
 
1120
1126
  Returns
1121
1127
  -------
1122
- efficiency : NDArray
1123
- Interpolated efficiency values.
1128
+ interpolator : RegularGridInterpolator
1129
+ Callable function to interpolate efficiency values.
1130
+ theta_min_max : tuple
1131
+ Minimum and maximum theta values in the lookup table.
1132
+ phi_min_max : tuple
1133
+ Minimum and maximum phi values in the lookup table.
1124
1134
  """
1125
1135
  lookup_table = get_energy_efficiencies(ancillary_files)
1126
1136
 
@@ -1133,6 +1143,9 @@ def get_efficiency_interpolator(ancillary_files: dict) -> RegularGridInterpolato
1133
1143
  efficiency_grid = efficiency_2d.reshape(
1134
1144
  (len(theta_vals), len(phi_vals), len(energy_vals))
1135
1145
  )
1146
+ # Find the min and max values for theta and phi
1147
+ theta_min_max = (theta_vals.min(), theta_vals.max())
1148
+ phi_min_max = (phi_vals.min(), phi_vals.max())
1136
1149
 
1137
1150
  interpolator = RegularGridInterpolator(
1138
1151
  (theta_vals, phi_vals, energy_vals),
@@ -1141,7 +1154,7 @@ def get_efficiency_interpolator(ancillary_files: dict) -> RegularGridInterpolato
1141
1154
  fill_value=FILLVAL_FLOAT32,
1142
1155
  )
1143
1156
 
1144
- return interpolator
1157
+ return interpolator, theta_min_max, phi_min_max
1145
1158
 
1146
1159
 
1147
1160
  def get_efficiency(
@@ -1174,7 +1187,7 @@ def get_efficiency(
1174
1187
  Interpolated efficiency values.
1175
1188
  """
1176
1189
  if not interpolator:
1177
- interpolator = get_efficiency_interpolator(ancillary_files)
1190
+ interpolator, _, _ = get_efficiency_interpolator(ancillary_files)
1178
1191
 
1179
1192
  return interpolator((theta_inst, phi_inst, energy))
1180
1193
 
@@ -1343,7 +1356,7 @@ def is_back_tof_valid(
1343
1356
  From page 33 of the IMAP-Ultra Flight Software Specification document.
1344
1357
  """
1345
1358
  _, _, _, _, tofx, tofy = get_ph_tof_and_back_positions(
1346
- de_dataset, xf, "ultra45", ancillary_files
1359
+ de_dataset, xf, sensor, ancillary_files
1347
1360
  )
1348
1361
  diff = tofy - tofx
1349
1362
 
@@ -6,6 +6,7 @@ import astropy_healpix.healpy as hp
6
6
  import numpy as np
7
7
  import xarray as xr
8
8
 
9
+ from imap_processing.cdf.utils import parse_filename_like
9
10
  from imap_processing.quality_flags import ImapPSETUltraFlags
10
11
  from imap_processing.spice.repoint import get_pointing_times
11
12
  from imap_processing.spice.time import (
@@ -24,6 +25,7 @@ from imap_processing.ultra.l1c.ultra_l1c_pset_bins import (
24
25
  get_efficiencies_and_geometric_function,
25
26
  get_energy_delta_minus_plus,
26
27
  get_helio_adjusted_data,
28
+ get_spacecraft_background_rates,
27
29
  get_spacecraft_exposure_times,
28
30
  get_spacecraft_histogram,
29
31
  )
@@ -69,9 +71,14 @@ def calculate_helio_pset(
69
71
  dataset : xarray.Dataset
70
72
  Dataset containing the data.
71
73
  """
74
+ sensor = parse_filename_like(name)["sensor"][0:2]
72
75
  pset_dict: dict[str, np.ndarray] = {}
73
76
  # Select only the species we are interested in.
74
77
  indices = np.where(np.isin(de_dataset["ebin"].values, species_id))[0]
78
+ if indices.size == 0:
79
+ logger.info(f"No data available for {name}")
80
+ return None
81
+
75
82
  species_dataset = de_dataset.isel(epoch=indices)
76
83
 
77
84
  rejected = get_de_rejection_mask(
@@ -120,17 +127,23 @@ def calculate_helio_pset(
120
127
  )
121
128
  healpix = np.arange(n_pix)
122
129
 
130
+ # Get midpoint timestamp for pointing.
131
+ pointing_start, pointing_stop = get_pointing_times(
132
+ et_to_met(species_dataset["event_times"].data[0])
133
+ )
123
134
  logger.info("Calculating spacecraft exposure times with deadtime correction.")
124
135
  exposure_time, deadtime_ratios = get_spacecraft_exposure_times(
125
136
  rates_dataset,
126
137
  params_dataset,
127
138
  pixels_below_scattering,
128
139
  boundary_scale_factors,
140
+ pointing_start,
141
+ pointing_stop,
129
142
  n_pix=n_pix,
130
143
  )
131
144
  logger.info("Calculating spun efficiencies and geometric function.")
132
145
  # calculate efficiency and geometric function as a function of energy
133
- efficiencies, geometric_function = get_efficiencies_and_geometric_function(
146
+ geometric_function, efficiencies = get_efficiencies_and_geometric_function(
134
147
  pixels_below_scattering,
135
148
  boundary_scale_factors,
136
149
  theta_vals,
@@ -138,14 +151,23 @@ def calculate_helio_pset(
138
151
  n_pix,
139
152
  ancillary_files,
140
153
  )
141
- # Get midpoint timestamp for pointing.
142
- pointing_start, pointing_stop = get_pointing_times(
143
- et_to_met(species_dataset["event_times"].data[0])
154
+
155
+ logger.info("Calculating background rates.")
156
+ # TODO calculate helio background rates
157
+ # Calculate background rates
158
+ background_rates = get_spacecraft_background_rates(
159
+ rates_dataset,
160
+ sensor,
161
+ ancillary_files,
162
+ intervals,
163
+ goodtimes_dataset["spin_number"].values,
164
+ nside=nside,
144
165
  )
166
+
145
167
  mid_time = ttj2000ns_to_et(met_to_ttj2000ns((pointing_start + pointing_stop) / 2))
146
168
 
147
169
  logger.info("Adjusting data for helio frame.")
148
- exposure_time, efficiency, geometric_function = get_helio_adjusted_data(
170
+ exposure_time, _efficiency, geometric_function = get_helio_adjusted_data(
149
171
  mid_time,
150
172
  exposure_time,
151
173
  geometric_function,
@@ -176,7 +198,8 @@ def calculate_helio_pset(
176
198
  pset_dict["latitude"] = latitude[np.newaxis, ...]
177
199
  pset_dict["longitude"] = longitude[np.newaxis, ...]
178
200
  pset_dict["energy_bin_geometric_mean"] = energy_bin_geometric_means
179
- pset_dict["helio_exposure_factor"] = exposure_time[np.newaxis, ...]
201
+ pset_dict["background_rates"] = background_rates[np.newaxis, ...]
202
+ pset_dict["exposure_factor"] = exposure_time[np.newaxis, ...]
180
203
  pset_dict["pixel_index"] = healpix
181
204
  pset_dict["energy_bin_delta"] = np.diff(intervals, axis=1).squeeze()[
182
205
  np.newaxis, ...
@@ -174,8 +174,10 @@ def calculate_fwhm_spun_scattering(
174
174
  fwhm_phi_sum[:, for_inds] += fwhm_phi.T
175
175
  sample_count[:, for_inds] += 1
176
176
 
177
- fwhm_phi_avg = np.divide(fwhm_theta_sum, sample_count, where=sample_count != 0)
178
- fwhm_theta_avg = np.divide(fwhm_theta_sum, sample_count, where=sample_count != 0)
177
+ fwhm_phi_avg = np.zeros_like(fwhm_phi_sum)
178
+ fwhm_theta_avg = np.zeros_like(fwhm_theta_sum)
179
+ np.divide(fwhm_phi_sum, sample_count, out=fwhm_phi_avg, where=sample_count != 0)
180
+ np.divide(fwhm_theta_sum, sample_count, out=fwhm_theta_avg, where=sample_count != 0)
179
181
  return (
180
182
  pixels_below_scattering,
181
183
  fwhm_theta_avg,
@@ -77,6 +77,7 @@ def calculate_spacecraft_pset(
77
77
 
78
78
  # If there are no species return None.
79
79
  if indices.size == 0:
80
+ logger.info(f"No data available for {name}")
80
81
  return None
81
82
 
82
83
  # Before we use the de_dataset to calculate the pointing set grid we need to filter.
@@ -100,7 +101,7 @@ def calculate_spacecraft_pset(
100
101
  for_indices_by_spin_phase,
101
102
  theta_vals,
102
103
  phi_vals,
103
- ra_and_dec,
104
+ _ra_and_dec,
104
105
  boundary_scale_factors,
105
106
  ) = get_spacecraft_pointing_lookup_tables(ancillary_files, instrument_id)
106
107
 
@@ -126,7 +127,7 @@ def calculate_spacecraft_pset(
126
127
 
127
128
  logger.info("Calculating spun efficiencies and geometric function.")
128
129
  # calculate efficiency and geometric function as a function of energy
129
- efficiencies, geometric_function = get_efficiencies_and_geometric_function(
130
+ geometric_function, efficiencies = get_efficiencies_and_geometric_function(
130
131
  pixels_below_scattering,
131
132
  boundary_scale_factors,
132
133
  theta_vals,
@@ -136,6 +137,10 @@ def calculate_spacecraft_pset(
136
137
  )
137
138
  sensitivity = efficiencies * geometric_function
138
139
 
140
+ # Get the start and stop times of the pointing period
141
+ pointing_start, pointing_stop = get_pointing_times(
142
+ float(et_to_met(species_dataset["event_times"].data[0]))
143
+ )
139
144
  # Calculate exposure times
140
145
  logger.info("Calculating spacecraft exposure times with deadtime correction.")
141
146
  exposure_pointing, deadtime_ratios = get_spacecraft_exposure_times(
@@ -143,6 +148,8 @@ def calculate_spacecraft_pset(
143
148
  params_dataset,
144
149
  pixels_below_scattering,
145
150
  boundary_scale_factors,
151
+ pointing_start,
152
+ pointing_stop,
146
153
  n_pix=n_pix,
147
154
  )
148
155
  logger.info("Calculating background rates.")
@@ -172,10 +179,7 @@ def calculate_spacecraft_pset(
172
179
  spacecraft_pset_quality_flags,
173
180
  nside=nside,
174
181
  )
175
- # Get pointing start and stop times and convert to ttj2000ns
176
- pointing_start, pointing_stop = get_pointing_times(
177
- float(et_to_met(species_dataset["event_times"].data[0]))
178
- )
182
+ # Convert pointing start time to ttj2000ns
179
183
  pointing_start = met_to_ttj2000ns(pointing_start)
180
184
  # Epoch should be the start of the pointing
181
185
  pset_dict["epoch"] = np.atleast_1d(pointing_start).astype(np.int64)
@@ -8,7 +8,7 @@ from imap_processing.ultra.l1c.spacecraft_pset import calculate_spacecraft_pset
8
8
 
9
9
 
10
10
  def ultra_l1c(
11
- data_dict: dict, ancillary_files: dict, imap_frames: bool
11
+ data_dict: dict, ancillary_files: dict, descriptor: str
12
12
  ) -> list[xr.Dataset]:
13
13
  """
14
14
  Will process ULTRA L1A and L1B data into L1C CDF files at output_filepath.
@@ -19,8 +19,8 @@ def ultra_l1c(
19
19
  The data itself and its dependent data.
20
20
  ancillary_files : dict
21
21
  Ancillary files.
22
- imap_frames : bool
23
- Whether to use IMAP frames.
22
+ descriptor : str
23
+ Job descriptor.
24
24
 
25
25
  Returns
26
26
  -------
@@ -28,15 +28,15 @@ def ultra_l1c(
28
28
  List of xarray.Dataset.
29
29
  """
30
30
  output_datasets = []
31
-
32
- # Account for possibility of having 45 and 90 in dictionary.
31
+ create_helio_pset = True if "helio" in descriptor else False
32
+ # Account for the possibility of having 45 and 90 in the dictionary.
33
33
  for instrument_id in [45, 90]:
34
34
  if (
35
35
  f"imap_ultra_l1b_{instrument_id}sensor-goodtimes" in data_dict
36
36
  and f"imap_ultra_l1b_{instrument_id}sensor-de" in data_dict
37
37
  and f"imap_ultra_l1a_{instrument_id}sensor-rates" in data_dict
38
38
  and f"imap_ultra_l1a_{instrument_id}sensor-params" in data_dict
39
- and imap_frames
39
+ and create_helio_pset
40
40
  ):
41
41
  helio_pset = calculate_helio_pset(
42
42
  data_dict[f"imap_ultra_l1b_{instrument_id}sensor-de"],
@@ -13,7 +13,11 @@ from imap_processing.spice.geometry import (
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
16
+ from imap_processing.spice.spin import (
17
+ get_spacecraft_spin_phase,
18
+ get_spin_angle,
19
+ get_spin_data,
20
+ )
17
21
  from imap_processing.spice.time import ttj2000ns_to_met
18
22
  from imap_processing.ultra.constants import UltraConstants
19
23
  from imap_processing.ultra.l1b.lookup_utils import (
@@ -381,7 +385,7 @@ def calculate_exposure_time(
381
385
  # Get energy bin geometric means
382
386
  energy_bin_geometric_means = build_energy_bins()[2]
383
387
  # Exposure time should now be of shape (energy, npix)
384
- exposure_pointing = np.zeros((len(energy_bin_geometric_means), n_pix))
388
+ counts = np.zeros((len(energy_bin_geometric_means), n_pix))
385
389
  # nominal spin phase step.
386
390
  nominal_ms_step = 15 / len(pixels_below_scattering) # time step
387
391
  # Query the dead-time ratio and apply the nominal exposure time to pixels in the FOR
@@ -396,12 +400,13 @@ def calculate_exposure_time(
396
400
  continue
397
401
  # Apply the nominal exposure time (1 ms) scaled by the deadtime ratio to
398
402
  # 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]
403
+ counts[energy_bin_idx, pixels_at_energy_and_spin] += (
404
+ deadtime_ratios[i]
402
405
  * boundary_scale_factors[pixels_at_energy_and_spin, i]
403
406
  )
404
407
 
408
+ # Multiply by the nominal spin step to get the exposure time in ms
409
+ exposure_pointing = counts * nominal_ms_step
405
410
  return exposure_pointing
406
411
 
407
412
 
@@ -410,6 +415,8 @@ def get_spacecraft_exposure_times(
410
415
  params_dataset: xr.Dataset,
411
416
  pixels_below_scattering: list[list],
412
417
  boundary_scale_factors: NDArray,
418
+ pointing_start_met: float,
419
+ pointing_stop_met: float,
413
420
  n_pix: int,
414
421
  ) -> tuple[NDArray, NDArray]:
415
422
  """
@@ -428,6 +435,10 @@ def get_spacecraft_exposure_times(
428
435
  below the FWHM scattering threshold.
429
436
  boundary_scale_factors : np.ndarray
430
437
  Boundary scale factors for each pixel at each spin phase.
438
+ pointing_start_met : float
439
+ Start time of the pointing period in mission elapsed time.
440
+ pointing_stop_met : float
441
+ Stop time of the pointing period in mission elapsed time.
431
442
  n_pix : int
432
443
  Number of HEALPix pixels.
433
444
 
@@ -440,13 +451,36 @@ def get_spacecraft_exposure_times(
440
451
  nominal_deadtime_ratios : np.ndarray
441
452
  Deadtime ratios at each spin phase step (1ms res).
442
453
  """
443
- # TODO: use the universal spin table and
444
- # universal pointing table here to determine actual number of spins
445
454
  sectored_rates = get_sectored_rates(rates_dataset, params_dataset)
446
455
  nominal_deadtime_ratios = get_deadtime_ratios_by_spin_phase(sectored_rates)
447
- exposure_pointing_adjusted = calculate_exposure_time(
456
+ # The exposure time will be approximately the same per spin, so to save
457
+ # computation time, calculate the exposure time for a single spin and then scale it
458
+ # by the number of spins in the pointing. For more information, see section 3.4.3
459
+ # of the Ultra Algorithm Document.
460
+ exposure_time = calculate_exposure_time(
448
461
  nominal_deadtime_ratios, pixels_below_scattering, boundary_scale_factors, n_pix
449
462
  )
463
+ # Use the universal spin table to determine the actual number of spins
464
+ nominal_spin_seconds = 15.0
465
+ spin_data = get_spin_data()
466
+ # Filter for spins only in pointing
467
+ spin_data = spin_data[
468
+ (spin_data["spin_start_met"] >= pointing_start_met)
469
+ & (spin_data["spin_start_met"] <= pointing_stop_met)
470
+ ]
471
+ # Get only valid spin data
472
+ valid_mask = (spin_data["spin_phase_valid"].values == 1) & (
473
+ spin_data["spin_period_valid"].values == 1
474
+ )
475
+ n_spins_in_pointing: float = np.sum(
476
+ spin_data[valid_mask].spin_period_sec / nominal_spin_seconds
477
+ )
478
+ logger.info(
479
+ f"Calculated total spins universal spin table. Found {n_spins_in_pointing} "
480
+ f"valid spins."
481
+ )
482
+ # Adjust exposure time by the actual number of valid spins in the pointing
483
+ exposure_pointing_adjusted = n_spins_in_pointing * exposure_time
450
484
  return exposure_pointing_adjusted, nominal_deadtime_ratios
451
485
 
452
486
 
@@ -483,13 +517,17 @@ def get_efficiencies_and_geometric_function(
483
517
 
484
518
  Returns
485
519
  -------
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.
520
+ gf_averaged : np.ndarray
521
+ Averaged geometric factors across all spin phases.
522
+ Shape = (n_energy_bins, npix).
523
+ eff_averaged : np.ndarray
524
+ Averaged efficiencies across all spin phases.
525
+ Shape = (n_energy_bins, npix).
490
526
  """
491
527
  # Load callable efficiency interpolator function
492
- eff_interpolator = get_efficiency_interpolator(ancillary_files)
528
+ eff_interpolator, theta_min_max, phi_min_max = get_efficiency_interpolator(
529
+ ancillary_files
530
+ )
493
531
  # load geometric factor lookup table
494
532
  geometric_lookup_table = load_geometric_factor_tables(
495
533
  ancillary_files, "l1b-sensor-gf-blades"
@@ -497,6 +535,24 @@ def get_efficiencies_and_geometric_function(
497
535
  # Get energy bin geometric means
498
536
  energy_bin_geometric_means = build_energy_bins()[2]
499
537
  energy_bins = len(energy_bin_geometric_means)
538
+ # clip arrays to avoid out of bounds errors
539
+ logger.info(
540
+ "Clipping Theta and Phi values to valid ranges for the efficiency "
541
+ "interpolation. \n"
542
+ f"Theta valid range: {theta_min_max}, Phi valid range: {phi_min_max}. \n "
543
+ f"Found "
544
+ f"{np.sum((theta_vals < theta_min_max[0]) | (theta_vals > theta_min_max[1]))}"
545
+ f" Theta values out of range. \n"
546
+ f"Found "
547
+ f"{np.sum((phi_vals < phi_min_max[0]) | (phi_vals > phi_min_max[1]))}"
548
+ f" Phi values out of range. \n"
549
+ f"Theta min and max values before clipping: "
550
+ f"{theta_vals.min()}, {theta_vals.max()} \n"
551
+ f"Phi min and max values before clipping:"
552
+ f" {phi_vals.min()}, {phi_vals.max()} \n"
553
+ )
554
+ theta_vals_clipped = np.clip(theta_vals, theta_min_max[0], theta_min_max[1])
555
+ phi_vals_clipped = np.clip(phi_vals, phi_min_max[0], phi_min_max[1])
500
556
  # Initialize summation arrays for geometric factors and efficiencies
501
557
  gf_summation = np.zeros((energy_bins, npix))
502
558
  eff_summation = np.zeros((energy_bins, npix))
@@ -507,6 +563,8 @@ def get_efficiencies_and_geometric_function(
507
563
  # Compute gf and eff for these theta/phi pairs
508
564
  theta_at_spin = theta_vals[:, i]
509
565
  phi_at_spin = phi_vals[:, i]
566
+ theta_at_spin_clipped = theta_vals_clipped[:, i]
567
+ phi_at_spin_clipped = phi_vals_clipped[:, i]
510
568
  gf_values = get_geometric_factor(
511
569
  phi=phi_at_spin,
512
570
  theta=theta_at_spin,
@@ -518,10 +576,12 @@ def get_efficiencies_and_geometric_function(
518
576
  if pixel_inds.size == 0:
519
577
  continue
520
578
  energy = energy_bin_geometric_means[energy_bin_idx]
579
+ # Clip energy to calibrated range
580
+ energy_clipped = np.clip(energy, 3.0, 80.0)
521
581
  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],
582
+ np.full(phi_at_spin[pixel_inds].shape, energy_clipped),
583
+ phi_at_spin_clipped[pixel_inds],
584
+ theta_at_spin_clipped[pixel_inds],
525
585
  ancillary_files,
526
586
  interpolator=eff_interpolator,
527
587
  )
@@ -536,8 +596,10 @@ def get_efficiencies_and_geometric_function(
536
596
 
537
597
  # return averaged geometric factors and efficiencies across all spin phases
538
598
  # 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)
599
+ gf_averaged = np.zeros_like(gf_summation)
600
+ eff_averaged = np.zeros_like(eff_summation)
601
+ np.divide(gf_summation, sample_count, out=gf_averaged, where=sample_count != 0)
602
+ np.divide(eff_summation, sample_count, out=eff_averaged, where=sample_count != 0)
541
603
  return gf_averaged, eff_averaged
542
604
 
543
605
 
@@ -698,8 +760,8 @@ def get_spacecraft_background_rates(
698
760
  """
699
761
  pulses = get_pulses_per_spin(rates_dataset)
700
762
  # Pulses for the pointing.
701
- etof_min = get_image_params("eTOFMin", sensor, ancillary_files)
702
- etof_max = get_image_params("eTOFMax", sensor, ancillary_files)
763
+ etof_min = get_image_params("eTOFMin", f"ultra{sensor}", ancillary_files)
764
+ etof_max = get_image_params("eTOFMax", f"ultra{sensor}", ancillary_files)
703
765
  spin_number, _ = get_spin_and_duration(
704
766
  rates_dataset["shcoarse"], rates_dataset["spin"]
705
767
  )
@@ -364,8 +364,8 @@ def generate_ultra_healpix_skymap( # noqa: PLR0912
364
364
  for var in pointing_indep_vars:
365
365
  skymap.data_1d[var] = skymap.data_1d[var].squeeze("epoch", drop=True)
366
366
 
367
- # Background rates must be scaled by the ratio of the solid angles of the
368
- # map pixel / pointing set pixel
367
+ # Background rates must be scaled by
368
+ # the ratio of the solid angles of the map pixel / pointing set pixel
369
369
  skymap.data_1d["background_rates"] *= skymap.solid_angle / pointing_set.solid_angle
370
370
 
371
371
  # Get the energy bin widths from a PointingSet (they will all be the same)