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