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,27 +1,47 @@
1
1
  """Calculate Pointing Set Grids."""
2
2
 
3
+ import logging
4
+
5
+ import astropy_healpix.healpy as hp
3
6
  import numpy as np
4
- import pandas as pd
5
7
  import xarray as xr
6
8
 
7
- from imap_processing.spice.time import sct_to_et
9
+ from imap_processing.quality_flags import ImapPSETUltraFlags
10
+ from imap_processing.spice.repoint import get_pointing_times
11
+ from imap_processing.spice.time import (
12
+ et_to_met,
13
+ met_to_ttj2000ns,
14
+ ttj2000ns_to_et,
15
+ )
16
+ from imap_processing.ultra.l1b.ultra_l1b_culling import get_de_rejection_mask
17
+ from imap_processing.ultra.l1c.l1c_lookup_utils import (
18
+ calculate_fwhm_spun_scattering,
19
+ get_spacecraft_pointing_lookup_tables,
20
+ )
21
+ from imap_processing.ultra.l1c.ultra_l1c_culling import compute_culling_mask
8
22
  from imap_processing.ultra.l1c.ultra_l1c_pset_bins import (
9
23
  build_energy_bins,
10
- get_helio_background_rates,
11
- get_helio_exposure_times,
12
- get_helio_sensitivity,
24
+ get_efficiencies_and_geometric_function,
25
+ get_energy_delta_minus_plus,
26
+ get_helio_adjusted_data,
27
+ get_spacecraft_exposure_times,
13
28
  get_spacecraft_histogram,
14
29
  )
15
30
  from imap_processing.ultra.utils.ultra_l1_utils import create_dataset
16
31
 
32
+ logger = logging.getLogger(__name__)
33
+
17
34
 
18
35
  def calculate_helio_pset(
19
36
  de_dataset: xr.Dataset,
20
- extendedspin_dataset: xr.Dataset,
21
- cullingmask_dataset: xr.Dataset,
37
+ goodtimes_dataset: xr.Dataset,
38
+ rates_dataset: xr.Dataset,
39
+ params_dataset: xr.Dataset,
22
40
  name: str,
23
41
  ancillary_files: dict,
24
- ) -> xr.Dataset:
42
+ instrument_id: int,
43
+ species_id: list,
44
+ ) -> xr.Dataset | None:
25
45
  """
26
46
  Create dictionary with defined datatype for Pointing Set Grid Data.
27
47
 
@@ -29,14 +49,20 @@ def calculate_helio_pset(
29
49
  ----------
30
50
  de_dataset : xarray.Dataset
31
51
  Dataset containing de data.
32
- extendedspin_dataset : xarray.Dataset
33
- Dataset containing extendedspin data.
34
- cullingmask_dataset : xarray.Dataset
35
- Dataset containing cullingmask data.
52
+ goodtimes_dataset : xarray.Dataset
53
+ Dataset containing goodtimes data.
54
+ rates_dataset : xarray.Dataset
55
+ Dataset containing image rates data.
56
+ params_dataset : xarray.Dataset
57
+ Dataset containing image parameters data.
36
58
  name : str
37
59
  Name of the dataset.
38
60
  ancillary_files : dict
39
61
  Ancillary files.
62
+ instrument_id : int
63
+ Instrument ID, either 45 or 90.
64
+ species_id : List
65
+ Species ID.
40
66
 
41
67
  Returns
42
68
  -------
@@ -44,56 +70,132 @@ def calculate_helio_pset(
44
70
  Dataset containing the data.
45
71
  """
46
72
  pset_dict: dict[str, np.ndarray] = {}
73
+ # Select only the species we are interested in.
74
+ indices = np.where(np.isin(de_dataset["e_bin"].values, species_id))[0]
75
+ species_dataset = de_dataset.isel(epoch=indices)
76
+
77
+ rejected = get_de_rejection_mask(
78
+ species_dataset["quality_scattering"].values,
79
+ species_dataset["quality_outliers"].values,
80
+ )
81
+ species_dataset = species_dataset.isel(epoch=~rejected)
47
82
 
48
83
  v_mag_helio_spacecraft = np.linalg.norm(
49
- de_dataset["velocity_dps_helio"].values, axis=1
84
+ species_dataset["velocity_dps_helio"].values, axis=1
50
85
  )
51
86
  vhat_dps_helio = (
52
- de_dataset["velocity_dps_helio"].values / v_mag_helio_spacecraft[:, np.newaxis]
87
+ species_dataset["velocity_dps_helio"].values
88
+ / v_mag_helio_spacecraft[:, np.newaxis]
53
89
  )
54
90
  intervals, _, energy_bin_geometric_means = build_energy_bins()
91
+ # Get lookup table for FOR indices by spin phase step
92
+ (
93
+ for_indices_by_spin_phase,
94
+ theta_vals,
95
+ phi_vals,
96
+ ra_and_dec,
97
+ boundary_scale_factors,
98
+ ) = get_spacecraft_pointing_lookup_tables(ancillary_files, instrument_id)
99
+
100
+ logger.info("calculating spun FWHM scattering values.")
101
+ pixels_below_scattering, scattering_theta, scattering_phi, scattering_thresholds = (
102
+ calculate_fwhm_spun_scattering(
103
+ for_indices_by_spin_phase,
104
+ theta_vals,
105
+ phi_vals,
106
+ ancillary_files,
107
+ instrument_id,
108
+ )
109
+ )
110
+
111
+ nside = hp.npix2nside(for_indices_by_spin_phase.shape[0])
55
112
  counts, latitude, longitude, n_pix = get_spacecraft_histogram(
56
113
  vhat_dps_helio,
57
- de_dataset["energy_heliosphere"].values,
114
+ species_dataset["energy_heliosphere"].values,
58
115
  intervals,
59
- nside=128,
116
+ nside=nside,
117
+ )
118
+ helio_pset_quality_flags = np.full(
119
+ n_pix, ImapPSETUltraFlags.NONE.value, dtype=np.uint16
60
120
  )
61
-
62
121
  healpix = np.arange(n_pix)
63
122
 
64
- # calculate background rates
65
- background_rates = get_helio_background_rates()
66
-
67
- efficiencies = ancillary_files["l1c-90sensor-efficiencies"]
68
- geometric_function = ancillary_files["l1c-90sensor-gf"]
123
+ logger.info("Calculating spacecraft exposure times with deadtime correction.")
124
+ exposure_time, deadtime_ratios = get_spacecraft_exposure_times(
125
+ rates_dataset,
126
+ params_dataset,
127
+ pixels_below_scattering,
128
+ boundary_scale_factors,
129
+ n_pix=n_pix,
130
+ )
131
+ logger.info("Calculating spun efficiencies and geometric function.")
132
+ # calculate efficiency and geometric function as a function of energy
133
+ efficiencies, geometric_function = get_efficiencies_and_geometric_function(
134
+ pixels_below_scattering,
135
+ boundary_scale_factors,
136
+ theta_vals,
137
+ phi_vals,
138
+ n_pix,
139
+ ancillary_files,
140
+ )
141
+ # Get midpoint timestamp for pointing.
142
+ pointing_start, pointing_stop = get_pointing_times(
143
+ et_to_met(species_dataset["event_times"].data[0])
144
+ )
145
+ mid_time = ttj2000ns_to_et(met_to_ttj2000ns((pointing_start + pointing_stop) / 2))
69
146
 
70
- df_efficiencies = pd.read_csv(efficiencies)
71
- df_geometric_function = pd.read_csv(geometric_function)
72
- mid_time = sct_to_et(np.median(de_dataset["event_times"].data))
73
- sensitivity = get_helio_sensitivity(
147
+ logger.info("Adjusting data for helio frame.")
148
+ exposure_time, efficiency, geometric_function = get_helio_adjusted_data(
74
149
  mid_time,
75
- df_efficiencies,
76
- df_geometric_function,
150
+ exposure_time,
151
+ geometric_function,
152
+ efficiencies,
153
+ ra_and_dec[:, 0],
154
+ ra_and_dec[:, 1],
155
+ nside=nside,
77
156
  )
157
+ sensitivity = efficiencies * geometric_function
78
158
 
79
- # Calculate exposure
80
- constant_exposure = ancillary_files["l1c-90sensor-dps-exposure"]
81
- df_exposure = pd.read_csv(constant_exposure)
82
- exposure_pointing = get_helio_exposure_times(mid_time, df_exposure)
159
+ start: float = np.min(species_dataset["event_times"].values)
160
+ end: float = np.max(species_dataset["event_times"].values)
83
161
 
84
- # For ISTP, epoch should be the center of the time bin.
85
- pset_dict["epoch"] = de_dataset.epoch.data[:1].astype(np.int64)
162
+ # Time bins in 30 minute intervals
163
+ time_bins = np.arange(start, end + 1800, 1800)
164
+
165
+ # Compute mask for culling the Earth
166
+ compute_culling_mask(
167
+ time_bins,
168
+ 6378.1, # Earth radius
169
+ helio_pset_quality_flags,
170
+ nside=nside,
171
+ )
172
+ pointing_start = met_to_ttj2000ns(pointing_start)
173
+ # Epoch should be the start of the pointing
174
+ pset_dict["epoch"] = np.atleast_1d(pointing_start).astype(np.int64)
86
175
  pset_dict["counts"] = counts[np.newaxis, ...]
87
176
  pset_dict["latitude"] = latitude[np.newaxis, ...]
88
177
  pset_dict["longitude"] = longitude[np.newaxis, ...]
89
178
  pset_dict["energy_bin_geometric_mean"] = energy_bin_geometric_means
90
- pset_dict["background_rates"] = background_rates[np.newaxis, ...]
91
- pset_dict["helio_exposure_factor"] = exposure_pointing[np.newaxis, ...]
179
+ pset_dict["helio_exposure_factor"] = exposure_time
92
180
  pset_dict["pixel_index"] = healpix
93
181
  pset_dict["energy_bin_delta"] = np.diff(intervals, axis=1).squeeze()[
94
182
  np.newaxis, ...
95
183
  ]
96
- pset_dict["sensitivity"] = sensitivity[np.newaxis, ...]
184
+ pset_dict["sensitivity"] = sensitivity
185
+ pset_dict["efficiency"] = efficiencies
186
+ pset_dict["geometric_function"] = geometric_function
187
+ pset_dict["dead_time_ratio"] = deadtime_ratios
188
+ pset_dict["spin_phase_step"] = np.arange(len(deadtime_ratios))
189
+ pset_dict["quality_flags"] = helio_pset_quality_flags[np.newaxis, ...]
190
+
191
+ pset_dict["scatter_theta"] = scattering_theta
192
+ pset_dict["scatter_phi"] = scattering_phi
193
+ pset_dict["scatter_threshold"] = scattering_thresholds
194
+
195
+ # Add the energy delta plus/minus to the dataset
196
+ energy_delta_minus, energy_delta_plus = get_energy_delta_minus_plus()
197
+ pset_dict["energy_delta_minus"] = energy_delta_minus
198
+ pset_dict["energy_delta_plus"] = energy_delta_plus
97
199
 
98
200
  dataset = create_dataset(pset_dict, name, "l1c")
99
201
 
@@ -0,0 +1,289 @@
1
+ """Contains tools for lookup tables for l1c."""
2
+
3
+ import logging
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from numpy._typing import NDArray
8
+
9
+ from imap_processing.ultra.l1b.lookup_utils import (
10
+ get_scattering_coefficients,
11
+ get_scattering_thresholds,
12
+ load_scattering_lookup_tables,
13
+ )
14
+ from imap_processing.ultra.l1c.ultra_l1c_pset_bins import build_energy_bins
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def mask_below_fwhm_scattering_threshold(
20
+ theta_coeffs: np.ndarray,
21
+ phi_coeffs: np.ndarray,
22
+ energy: np.ndarray,
23
+ scattering_thresholds: np.ndarray,
24
+ ) -> tuple[NDArray, NDArray, NDArray]:
25
+ """
26
+ Determine indices of theta and phi values below the FWHM scattering threshold.
27
+
28
+ For each phi and theta, calculate the FWHM using the formula:
29
+ FWHM = A*E^g
30
+ If Phi FWHM or Theta FWHM > the scattering requirements from the table above,
31
+ mask the instrument frame pixel.
32
+
33
+ Parameters
34
+ ----------
35
+ theta_coeffs : NDArray
36
+ Coefficients for theta FWHM calculation (a and g) for each pixel.
37
+ phi_coeffs : NDArray
38
+ Coefficients for phi FWHM calculation (a and g) for each pixel.
39
+ energy : NDArray
40
+ Energy corresponding to each theta and phi val in keV.
41
+ scattering_thresholds : dict
42
+ Scattering thresholds corresponding to each energy.
43
+
44
+ Returns
45
+ -------
46
+ scattering_mask : numpy.ndarray
47
+ Boolean array indicating indices below the scattering threshold.
48
+ fwhm_theta : numpy.ndarray
49
+ Calculated FWHM values for theta.
50
+ fwhm_phi : numpy.ndarray
51
+ Calculated FWHM values for phi.
52
+ """
53
+ # Calculate FWHM for all pixels and all energies
54
+ fwhm_theta = theta_coeffs[..., 0:1] * (
55
+ energy ** theta_coeffs[..., 1:2]
56
+ ) # (npix, energy.shape[1])
57
+ fwhm_phi = phi_coeffs[..., 0:1] * (
58
+ energy ** phi_coeffs[..., 1:2]
59
+ ) # (npix, energy.shape[1])
60
+
61
+ thresholds = scattering_thresholds[np.newaxis, :] # (1, energy.shape[1])
62
+
63
+ # Combine conditions for both theta and phi.
64
+ # shape = (npix, energy.shape[1])
65
+ scattering_mask = np.logical_and(fwhm_theta <= thresholds, fwhm_phi <= thresholds)
66
+ return scattering_mask, fwhm_theta, fwhm_phi
67
+
68
+
69
+ def calculate_fwhm_spun_scattering(
70
+ for_indices_by_spin_phase: np.ndarray,
71
+ theta_vals: np.ndarray,
72
+ phi_vals: np.ndarray,
73
+ ancillary_files: dict,
74
+ instrument_id: int,
75
+ ) -> tuple[list, NDArray, NDArray, NDArray]:
76
+ """
77
+ Calculate FWHM scattering values for each pixel, energy bin, and spin phase step.
78
+
79
+ This function also calculates a mask for pixels that are below the FWHM threshold.
80
+
81
+ Parameters
82
+ ----------
83
+ for_indices_by_spin_phase : np.ndarray
84
+ A 2D boolean array where cols are spin phase steps are rows are HEALPix pixels.
85
+ True indicates pixels that are within the Field of Regard (FOR) at that
86
+ spin phase.
87
+ theta_vals : np.ndarray
88
+ A 2D array of theta values for each HEALPix pixel at each spin phase step.
89
+ phi_vals : np.ndarray
90
+ A 2D array of phi values for each HEALPix pixel at each spin phase step.
91
+ ancillary_files : dict
92
+ Dictionary containing ancillary files.
93
+ instrument_id : int,
94
+ Instrument ID, either 45 or 90.
95
+
96
+ Returns
97
+ -------
98
+ pixels_below_scattering : list
99
+ A Nested list of arrays indicating pixels within the scattering threshold.
100
+ The outer list indicates spin phase steps, the middle list indicates energy
101
+ bins, and the inner arrays contain indices indicating pixels that are below
102
+ the FWHM scattering threshold.
103
+ scattering_fwhm_theta : NDArray
104
+ Calculated FWHM scatting values for theta at each energy bin and averaged
105
+ over spin phase.
106
+ scattering_fwhm_phi : NDArray
107
+ Calculated FWHM scatting values for theta at each energy bin and averaged
108
+ over spin phase.
109
+ scattering_thresholds_for_energy_mean : NDArray
110
+ Scattering thresholds corresponding to each energy bin.
111
+ """
112
+ # Load scattering coefficient lookup table
113
+ scattering_luts = load_scattering_lookup_tables(ancillary_files, instrument_id)
114
+ pixels_below_scattering = []
115
+ # Get energy bin geometric means
116
+ energy_bin_geometric_means = build_energy_bins()[2]
117
+ # Load scattering thresholds for the energy bin geometric means
118
+ scattering_thresholds_for_energy_mean = get_scattering_thresholds_for_energy(
119
+ energy_bin_geometric_means, ancillary_files
120
+ )
121
+ # Initialize arrays to accumulate FWHM values for averaging
122
+ fwhm_theta_sum = np.zeros(
123
+ (len(energy_bin_geometric_means), for_indices_by_spin_phase.shape[0])
124
+ )
125
+ fwhm_phi_sum = np.zeros_like(fwhm_theta_sum)
126
+ sample_count = np.zeros_like(fwhm_theta_sum)
127
+
128
+ steps = for_indices_by_spin_phase.shape[1]
129
+ energies = energy_bin_geometric_means[np.newaxis, :]
130
+ # The "for_indices_by_spin_phase" lookup table contains the boolean values of each
131
+ # pixel at each spin phase step, indicating whether the pixel is inside the FOR.
132
+ # It starts at Spin-phase = 0, and increments in fine steps (1 ms), spinning the
133
+ # spacecraft in the despun frame. At each iteration, query for the pixels in the
134
+ # FOR, and calculate whether the FWHM value is below the threshold at the energy.
135
+ for i in range(steps):
136
+ # Calculate spin phase for the current iteration
137
+ for_inds = for_indices_by_spin_phase[:, i]
138
+
139
+ # Skip if no pixels in FOR
140
+ if not np.any(for_inds):
141
+ logger.info(f"No pixels found in FOR at spin phase step {i}")
142
+ pixels_below_scattering.append(
143
+ [
144
+ np.array([], dtype=int)
145
+ for _ in range(len(energy_bin_geometric_means))
146
+ ]
147
+ )
148
+ continue
149
+ # Using the lookup table, get the indices of the pixels inside the FOR at
150
+ # the current spin phase step.
151
+ theta = theta_vals[for_inds, i]
152
+ phi = phi_vals[for_inds, i]
153
+ theta_coeffs, phi_coeffs = get_scattering_coefficients(
154
+ theta, phi, lookup_tables=scattering_luts
155
+ )
156
+ # Get a mask for pixels below the FWHM scattering threshold
157
+ scattering_mask, fwhm_theta, fwhm_phi = mask_below_fwhm_scattering_threshold(
158
+ theta_coeffs,
159
+ phi_coeffs,
160
+ energies,
161
+ scattering_thresholds=scattering_thresholds_for_energy_mean,
162
+ )
163
+ # Extract pixel indices for each energy
164
+ for_pixel_indices = np.where(for_inds)[0]
165
+ pixels_below_scattering_for_energy = []
166
+
167
+ for energy_idx in range(len(energy_bin_geometric_means)):
168
+ valid_pixels = scattering_mask[:, energy_idx]
169
+ pixels_below_scattering_for_energy.append(for_pixel_indices[valid_pixels])
170
+
171
+ pixels_below_scattering.append(pixels_below_scattering_for_energy)
172
+ # Accumulate FWHM values for averaging
173
+ fwhm_theta_sum[:, for_inds] += fwhm_theta.T
174
+ fwhm_phi_sum[:, for_inds] += fwhm_phi.T
175
+ sample_count[:, for_inds] += 1
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)
179
+ return (
180
+ pixels_below_scattering,
181
+ fwhm_theta_avg,
182
+ fwhm_phi_avg,
183
+ scattering_thresholds_for_energy_mean,
184
+ )
185
+
186
+
187
+ def get_spacecraft_pointing_lookup_tables(
188
+ ancillary_files: dict, instrument_id: int
189
+ ) -> tuple[NDArray, NDArray, NDArray, NDArray, NDArray]:
190
+ """
191
+ Get indices of pixels in the nominal FOR as a function of spin phase.
192
+
193
+ This function also returns the theta / phi values in the instrument frame per spin
194
+ phase, right ascension / declination values in the SC frame, and boundary scale
195
+ factors for each pixel at each spin phase.
196
+
197
+ Parameters
198
+ ----------
199
+ ancillary_files : dict[Path]
200
+ Ancillary files.
201
+ instrument_id : int
202
+ Instrument ID, either 45 or 90.
203
+
204
+ Returns
205
+ -------
206
+ for_indices_by_spin_phase : NDArray
207
+ A 2D boolean array of shape (npix, n_spin_phase_steps).
208
+ True indicates pixels that are within the Field of Regard (FOR) at that
209
+ spin phase.
210
+ theta_vals : NDArray
211
+ A 2D array of theta values for each HEALPix pixel at each spin phase step.
212
+ phi_vals : NDArray
213
+ A 2D array of phi values for each HEALPix pixel at each spin phase step.
214
+ ra_and_dec : NDArray
215
+ A 2D array of right ascension and declination values for each HEALPix pixel.
216
+ boundary_scale_factors : NDArray
217
+ A 2D array of boundary scale factors for each HEALPix pixel at each spin phase
218
+ step.
219
+ """
220
+ theta_descriptor = f"l1c-{instrument_id}sensor-sc-pointing-theta"
221
+ phi_descriptor = f"l1c-{instrument_id}sensor-sc-pointing-phi"
222
+ index_descriptor = f"l1c-{instrument_id}sensor-sc-pointing-index"
223
+ bsf_descriptor = f"l1c-{instrument_id}sensor-sc-pointing-bsf"
224
+
225
+ theta_vals = pd.read_csv(
226
+ ancillary_files[theta_descriptor], header=None, skiprows=1
227
+ ).to_numpy(dtype=float)[:, 2:]
228
+ phi_vals = pd.read_csv(
229
+ ancillary_files[phi_descriptor], header=None, skiprows=1
230
+ ).to_numpy(dtype=float)[:, 2:]
231
+ index_grid = pd.read_csv(
232
+ ancillary_files[index_descriptor], header=None, skiprows=1
233
+ ).to_numpy(dtype=float)
234
+ boundary_scale_factors = pd.read_csv(
235
+ ancillary_files[bsf_descriptor], header=None, skiprows=1
236
+ ).to_numpy(dtype=float)[:, 2:]
237
+
238
+ ra_and_dec = index_grid[:, :2] # Shape (npix, 2)
239
+ # This array indicates whether each pixel is in the nominal FOR at each spin phase
240
+ # step (15000 steps for a full rotation with 1 ms resolution).
241
+ for_indices_by_spin_phase = np.nan_to_num(index_grid[:, 2:], nan=0).astype(
242
+ bool
243
+ ) # Shape (npix, 15000)
244
+ return (
245
+ for_indices_by_spin_phase,
246
+ theta_vals,
247
+ phi_vals,
248
+ ra_and_dec,
249
+ boundary_scale_factors,
250
+ )
251
+
252
+
253
+ def get_scattering_thresholds_for_energy(
254
+ energy: np.ndarray, ancillary_files: dict
255
+ ) -> np.ndarray:
256
+ """
257
+ Find the scattering thresholds for each energy bin.
258
+
259
+ Parameters
260
+ ----------
261
+ energy : np.ndarray
262
+ Array of energy values in keV.
263
+ ancillary_files : dict
264
+ Dictionary containing ancillary files.
265
+
266
+ Returns
267
+ -------
268
+ np.ndarray
269
+ Array of scattering thresholds for each energy bin.
270
+ """
271
+ scattering_thresholds = get_scattering_thresholds(ancillary_files)
272
+ # Get thresholds for all energies
273
+ thresholds = []
274
+ for e in energy:
275
+ try:
276
+ threshold = next(
277
+ threshold
278
+ for energy_range, threshold in scattering_thresholds.items()
279
+ if energy_range[0] <= e < energy_range[1]
280
+ )
281
+ except StopIteration:
282
+ logger.warning(
283
+ f"Energy {e} keV is out of bounds for scattering thresholds. Using"
284
+ f" zero for as threshold."
285
+ )
286
+
287
+ threshold = 0
288
+ thresholds.append(threshold)
289
+ return np.array(thresholds)