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