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
imap_processing/hi/hi_l2.py
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Literal
|
|
6
5
|
|
|
7
6
|
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
8
|
import xarray as xr
|
|
9
9
|
|
|
10
|
-
from imap_processing.ena_maps.ena_maps import
|
|
11
|
-
|
|
10
|
+
from imap_processing.ena_maps.ena_maps import (
|
|
11
|
+
AbstractSkyMap,
|
|
12
|
+
HiPointingSet,
|
|
13
|
+
RectangularSkyMap,
|
|
14
|
+
)
|
|
15
|
+
from imap_processing.ena_maps.utils.naming import MapDescriptor
|
|
16
|
+
from imap_processing.hi.utils import CalibrationProductConfig
|
|
12
17
|
|
|
13
18
|
logger = logging.getLogger(__name__)
|
|
14
19
|
|
|
@@ -42,24 +47,36 @@ def hi_l2(
|
|
|
42
47
|
l2_dataset : list[xarray.Dataset]
|
|
43
48
|
Level 2 IMAP-Hi dataset ready to be written to a CDF file.
|
|
44
49
|
"""
|
|
45
|
-
# TODO: parse descriptor to determine map configuration
|
|
46
|
-
sensor = "45" if "45" in descriptor else "90"
|
|
47
|
-
direction: Literal["full"] = "full"
|
|
48
50
|
cg_corrected = False
|
|
49
|
-
|
|
51
|
+
map_descriptor = MapDescriptor.from_string(descriptor)
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
sky_map = generate_hi_map(
|
|
52
54
|
psets,
|
|
53
55
|
geometric_factors_path,
|
|
54
56
|
esa_energies_path,
|
|
55
|
-
|
|
57
|
+
spin_phase=map_descriptor.spin_phase,
|
|
58
|
+
output_map=map_descriptor.to_empty_map(),
|
|
56
59
|
cg_corrected=cg_corrected,
|
|
57
|
-
map_spacing=map_spacing,
|
|
58
60
|
)
|
|
59
61
|
|
|
60
62
|
# Get the map dataset with variables/coordinates in the correct shape
|
|
61
63
|
# TODO get the correct descriptor and frame
|
|
62
|
-
|
|
64
|
+
|
|
65
|
+
if not isinstance(sky_map, RectangularSkyMap):
|
|
66
|
+
raise NotImplementedError("HEALPix map output not supported for Hi")
|
|
67
|
+
if not isinstance(map_descriptor.sensor, str):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"Invalid map_descriptor. Sensor attribute must be of type str "
|
|
70
|
+
"and be either '45' or '90'"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
l2_ds = sky_map.build_cdf_dataset(
|
|
74
|
+
"hi",
|
|
75
|
+
"l2",
|
|
76
|
+
map_descriptor.frame_descriptor,
|
|
77
|
+
descriptor,
|
|
78
|
+
sensor=map_descriptor.sensor,
|
|
79
|
+
)
|
|
63
80
|
|
|
64
81
|
return [l2_ds]
|
|
65
82
|
|
|
@@ -68,12 +85,12 @@ def generate_hi_map(
|
|
|
68
85
|
psets: list[str | Path],
|
|
69
86
|
geometric_factors_path: str | Path,
|
|
70
87
|
esa_energies_path: str | Path,
|
|
88
|
+
output_map: AbstractSkyMap,
|
|
71
89
|
cg_corrected: bool = False,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
) -> RectangularSkyMap:
|
|
90
|
+
spin_phase: str = "full",
|
|
91
|
+
) -> AbstractSkyMap:
|
|
75
92
|
"""
|
|
76
|
-
Project Hi PSET data into a
|
|
93
|
+
Project Hi PSET data into a sky map.
|
|
77
94
|
|
|
78
95
|
Parameters
|
|
79
96
|
----------
|
|
@@ -83,34 +100,28 @@ def generate_hi_map(
|
|
|
83
100
|
Where to get the geometric factors from.
|
|
84
101
|
esa_energies_path : str or pathlib.Path
|
|
85
102
|
Where to get the energies from.
|
|
103
|
+
output_map : AbstractSkyMap
|
|
104
|
+
The map object to collect data into. Determines pixel spacing,
|
|
105
|
+
coordinate system, etc.
|
|
86
106
|
cg_corrected : bool, Optional
|
|
87
107
|
Whether to apply Compton-Getting correction to the energies. Defaults to
|
|
88
108
|
False.
|
|
89
|
-
|
|
109
|
+
spin_phase : str, Optional
|
|
90
110
|
Apply filtering to PSET data include ram or anti-ram or full spin data.
|
|
91
111
|
Defaults to "full".
|
|
92
|
-
map_spacing : int, Optional
|
|
93
|
-
Pixel spacing, in degrees, of the output map in degrees. Defaults to 4.
|
|
94
112
|
|
|
95
113
|
Returns
|
|
96
114
|
-------
|
|
97
|
-
sky_map :
|
|
115
|
+
sky_map : AbstractSkyMap
|
|
98
116
|
The sky map with all the PSET data projected into the map.
|
|
99
117
|
"""
|
|
100
|
-
rect_map = RectangularSkyMap(
|
|
101
|
-
spacing_deg=map_spacing, spice_frame=SpiceFrame.ECLIPJ2000
|
|
102
|
-
)
|
|
103
|
-
|
|
104
118
|
# TODO: Implement Compton-Getting correction
|
|
105
119
|
if cg_corrected:
|
|
106
120
|
raise NotImplementedError
|
|
107
|
-
# TODO: Implement directional filtering
|
|
108
|
-
if direction != "full":
|
|
109
|
-
raise NotImplementedError
|
|
110
121
|
|
|
111
122
|
for pset_path in psets:
|
|
112
123
|
logger.info(f"Processing {pset_path}")
|
|
113
|
-
pset = HiPointingSet(pset_path)
|
|
124
|
+
pset = HiPointingSet(pset_path, spin_phase=spin_phase)
|
|
114
125
|
|
|
115
126
|
# Background rate and uncertainty are exposure time weighted means in
|
|
116
127
|
# the map.
|
|
@@ -118,7 +129,7 @@ def generate_hi_map(
|
|
|
118
129
|
pset.data[var] *= pset.data["exposure_factor"]
|
|
119
130
|
|
|
120
131
|
# Project (bin) the PSET variables into the map pixels
|
|
121
|
-
|
|
132
|
+
output_map.project_pset_values_to_map(
|
|
122
133
|
pset,
|
|
123
134
|
["counts", "exposure_factor", "bg_rates", "bg_rates_unc", "obs_date"],
|
|
124
135
|
)
|
|
@@ -127,35 +138,45 @@ def generate_hi_map(
|
|
|
127
138
|
# Allow divide by zero to fill set pixels with zero exposure time to NaN
|
|
128
139
|
with np.errstate(divide="ignore"):
|
|
129
140
|
for var in VARS_TO_EXPOSURE_TIME_AVERAGE:
|
|
130
|
-
|
|
141
|
+
output_map.data_1d[var] /= output_map.data_1d["exposure_factor"]
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
output_map.data_1d.update(calculate_ena_signal_rates(output_map.data_1d))
|
|
144
|
+
output_map.data_1d.update(
|
|
134
145
|
calculate_ena_intensity(
|
|
135
|
-
|
|
146
|
+
output_map.data_1d, geometric_factors_path, esa_energies_path
|
|
136
147
|
)
|
|
137
148
|
)
|
|
138
149
|
|
|
139
|
-
|
|
150
|
+
output_map.data_1d["obs_date"].data = output_map.data_1d["obs_date"].data.astype(
|
|
140
151
|
np.int64
|
|
141
152
|
)
|
|
142
153
|
# TODO: Figure out how to compute obs_date_range (stddev of obs_date)
|
|
143
|
-
|
|
154
|
+
output_map.data_1d["obs_date_range"] = xr.zeros_like(output_map.data_1d["obs_date"])
|
|
144
155
|
|
|
145
156
|
# Rename and convert coordinate from esa_energy_step energy
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
esa_df = esa_energy_df(
|
|
158
|
+
esa_energies_path, output_map.data_1d["esa_energy_step"].data
|
|
159
|
+
)
|
|
160
|
+
output_map.data_1d = output_map.data_1d.rename({"esa_energy_step": "energy"})
|
|
161
|
+
output_map.data_1d = output_map.data_1d.assign_coords(
|
|
162
|
+
energy=esa_df["nominal_central_energy"].values
|
|
163
|
+
)
|
|
164
|
+
# Set the energy_step_delta values to the energy bandpass half-width-half-max
|
|
165
|
+
energy_delta = esa_df["bandpass_fwhm"].values / 2
|
|
166
|
+
output_map.data_1d["energy_delta_minus"] = xr.DataArray(
|
|
167
|
+
energy_delta,
|
|
168
|
+
name="energy_delta_minus",
|
|
169
|
+
dims=["energy"],
|
|
170
|
+
)
|
|
171
|
+
output_map.data_1d["energy_delta_plus"] = xr.DataArray(
|
|
172
|
+
energy_delta,
|
|
173
|
+
name="energy_delta_plus",
|
|
174
|
+
dims=["energy"],
|
|
151
175
|
)
|
|
152
|
-
# Set the energy_step_delta values
|
|
153
|
-
# TODO: get the correct energy delta values (they are set to NaN) in
|
|
154
|
-
# rect_map.build_cdf_dataset()
|
|
155
176
|
|
|
156
|
-
|
|
177
|
+
output_map.data_1d = output_map.data_1d.drop("esa_energy_step_label")
|
|
157
178
|
|
|
158
|
-
return
|
|
179
|
+
return output_map
|
|
159
180
|
|
|
160
181
|
|
|
161
182
|
def calculate_ena_signal_rates(map_ds: xr.Dataset) -> dict[str, xr.DataArray]:
|
|
@@ -214,33 +235,31 @@ def calculate_ena_intensity(
|
|
|
214
235
|
geometric_factors_path : str or pathlib.Path
|
|
215
236
|
Where to get the geometric factors from.
|
|
216
237
|
esa_energies_path : str or pathlib.Path
|
|
217
|
-
Where to get the energies
|
|
238
|
+
Where to get the esa energies, energy deltas, and geometric factors.
|
|
218
239
|
|
|
219
240
|
Returns
|
|
220
241
|
-------
|
|
221
242
|
intensity_vars : dict[str, xarray.DataArray]
|
|
222
243
|
ENA Intensity with statistical and systematic uncertainties.
|
|
223
244
|
"""
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
245
|
+
# read calibration product configuration file
|
|
246
|
+
cal_prod_df = CalibrationProductConfig.from_csv(geometric_factors_path)
|
|
247
|
+
# reindex_like removes esa_energy_steps and calibration products not in the
|
|
248
|
+
# map_ds esa_energy_step and calibration_product coordinates
|
|
249
|
+
geometric_factor = cal_prod_df.to_xarray().reindex_like(map_ds)["geometric_factor"]
|
|
250
|
+
geometric_factor = geometric_factor.transpose(
|
|
251
|
+
*[coord for coord in map_ds.coords if coord in geometric_factor.coords]
|
|
230
252
|
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
raise NotImplementedError
|
|
234
|
-
esa_energy = xr.ones_like(map_ds["esa_energy_step"])
|
|
253
|
+
energy_df = esa_energy_df(esa_energies_path, map_ds["esa_energy_step"].data)
|
|
254
|
+
esa_energy = energy_df.to_xarray()["nominal_central_energy"]
|
|
235
255
|
|
|
236
256
|
# Convert ENA Signal Rate to Flux
|
|
257
|
+
flux_conversion_divisor = geometric_factor * esa_energy
|
|
237
258
|
intensity_vars = {
|
|
238
|
-
"ena_intensity": map_ds["ena_signal_rates"] /
|
|
259
|
+
"ena_intensity": map_ds["ena_signal_rates"] / flux_conversion_divisor,
|
|
239
260
|
"ena_intensity_stat_unc": map_ds["ena_signal_rate_stat_unc"]
|
|
240
|
-
/
|
|
241
|
-
/
|
|
242
|
-
"ena_intensity_sys_err": map_ds["bg_rates_unc"]
|
|
243
|
-
/ (geometric_factor * esa_energy),
|
|
261
|
+
/ flux_conversion_divisor,
|
|
262
|
+
"ena_intensity_sys_err": map_ds["bg_rates_unc"] / flux_conversion_divisor,
|
|
244
263
|
}
|
|
245
264
|
|
|
246
265
|
# TODO: Correctly implement combining of calibration products. For now, just sum
|
|
@@ -259,3 +278,28 @@ def calculate_ena_intensity(
|
|
|
259
278
|
)
|
|
260
279
|
|
|
261
280
|
return intensity_vars
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def esa_energy_df(
|
|
284
|
+
esa_energies_path: str | Path, esa_energy_steps: np.ndarray
|
|
285
|
+
) -> pd.DataFrame:
|
|
286
|
+
"""
|
|
287
|
+
Lookup the nominal central energy values for given esa energy steps.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
esa_energies_path : str or pathlib.Path
|
|
292
|
+
Location of the calibration csv file containing the lookup data.
|
|
293
|
+
esa_energy_steps : numpy.ndarray
|
|
294
|
+
The ESA energy steps to get energies for.
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
esa_energies_df: pandas.DataFrame
|
|
299
|
+
Full data frame from the csv file filtered to only include the
|
|
300
|
+
esa_energy_steps input.
|
|
301
|
+
"""
|
|
302
|
+
esa_energies_lut = pd.read_csv(
|
|
303
|
+
esa_energies_path, comment="#", index_col="esa_energy_step"
|
|
304
|
+
)
|
|
305
|
+
return esa_energies_lut.loc[esa_energy_steps]
|
imap_processing/hi/utils.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""IMAP-Hi utils functions."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import re
|
|
4
|
-
from collections.abc import Sequence
|
|
6
|
+
from collections.abc import Iterable, Sequence
|
|
5
7
|
from dataclasses import dataclass
|
|
6
8
|
from enum import IntEnum
|
|
7
|
-
from
|
|
9
|
+
from pathlib import Path
|
|
8
10
|
|
|
9
11
|
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
10
13
|
import xarray as xr
|
|
11
14
|
|
|
12
15
|
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
|
|
@@ -15,11 +18,13 @@ from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
|
|
|
15
18
|
class HIAPID(IntEnum):
|
|
16
19
|
"""Create ENUM for apid."""
|
|
17
20
|
|
|
21
|
+
H45_MEMDMP = 740
|
|
18
22
|
H45_APP_NHK = 754
|
|
19
23
|
H45_SCI_CNT = 769
|
|
20
24
|
H45_SCI_DE = 770
|
|
21
25
|
H45_DIAG_FEE = 772
|
|
22
26
|
|
|
27
|
+
H90_MEMDMP = 804
|
|
23
28
|
H90_APP_NHK = 818
|
|
24
29
|
H90_SCI_CNT = 833
|
|
25
30
|
H90_SCI_DE = 834
|
|
@@ -100,9 +105,9 @@ def parse_sensor_number(full_string: str) -> int:
|
|
|
100
105
|
def full_dataarray(
|
|
101
106
|
name: str,
|
|
102
107
|
attrs: dict,
|
|
103
|
-
coords:
|
|
104
|
-
shape:
|
|
105
|
-
fill_value:
|
|
108
|
+
coords: dict[str, xr.DataArray] | None = None,
|
|
109
|
+
shape: int | Sequence[int] | None = None,
|
|
110
|
+
fill_value: float | None = None,
|
|
106
111
|
) -> xr.DataArray:
|
|
107
112
|
"""
|
|
108
113
|
Generate an empty xarray.DataArray with appropriate attributes.
|
|
@@ -158,9 +163,9 @@ def full_dataarray(
|
|
|
158
163
|
|
|
159
164
|
def create_dataset_variables(
|
|
160
165
|
variable_names: list[str],
|
|
161
|
-
variable_shape:
|
|
162
|
-
coords:
|
|
163
|
-
fill_value:
|
|
166
|
+
variable_shape: int | Sequence[int] | None = None,
|
|
167
|
+
coords: dict[str, xr.DataArray] | None = None,
|
|
168
|
+
fill_value: float | None = None,
|
|
164
169
|
att_manager_lookup_str: str = "{0}",
|
|
165
170
|
) -> dict[str, xr.DataArray]:
|
|
166
171
|
"""
|
|
@@ -247,3 +252,252 @@ class CoincidenceBitmap(IntEnum):
|
|
|
247
252
|
matches = re.findall(pattern, detector_hit_str)
|
|
248
253
|
# Sum the integer value assigned to the detector name for each match
|
|
249
254
|
return sum(CoincidenceBitmap[m] for m in matches)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class EsaEnergyStepLookupTable:
|
|
258
|
+
"""Class for holding a esa_step to esa_energy lookup table."""
|
|
259
|
+
|
|
260
|
+
def __init__(self) -> None:
|
|
261
|
+
self.df = pd.DataFrame(
|
|
262
|
+
columns=["start_met", "end_met", "esa_step", "esa_energy_step"]
|
|
263
|
+
)
|
|
264
|
+
self._indexed = False
|
|
265
|
+
|
|
266
|
+
# Get the FILLVAL from the CDF attribute manager that will be returned
|
|
267
|
+
# for queries without matches
|
|
268
|
+
attr_mgr = ImapCdfAttributes()
|
|
269
|
+
attr_mgr.add_instrument_global_attrs("hi")
|
|
270
|
+
attr_mgr.add_instrument_variable_attrs(instrument="hi", level=None)
|
|
271
|
+
var_attrs = attr_mgr.get_variable_attributes(
|
|
272
|
+
"hi_de_esa_energy_step", check_schema=False
|
|
273
|
+
)
|
|
274
|
+
self._fillval = var_attrs["FILLVAL"]
|
|
275
|
+
self._esa_energy_step_dtype = var_attrs["dtype"]
|
|
276
|
+
|
|
277
|
+
def add_entry(
|
|
278
|
+
self, start_met: float, end_met: float, esa_step: int, esa_energy_step: int
|
|
279
|
+
) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Add a single entry to the lookup table.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
start_met : float
|
|
286
|
+
Start mission elapsed time of the time range.
|
|
287
|
+
end_met : float
|
|
288
|
+
End mission elapsed time of the time range.
|
|
289
|
+
esa_step : int
|
|
290
|
+
ESA step value.
|
|
291
|
+
esa_energy_step : int
|
|
292
|
+
ESA energy step value to be stored.
|
|
293
|
+
"""
|
|
294
|
+
new_row = pd.DataFrame(
|
|
295
|
+
{
|
|
296
|
+
"start_met": [start_met],
|
|
297
|
+
"end_met": [end_met],
|
|
298
|
+
"esa_step": [esa_step],
|
|
299
|
+
"esa_energy_step": [esa_energy_step],
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
self.df = pd.concat([self.df, new_row], ignore_index=True)
|
|
303
|
+
self._indexed = False
|
|
304
|
+
|
|
305
|
+
def _ensure_indexed(self) -> None:
|
|
306
|
+
"""
|
|
307
|
+
Create index for faster queries if not already done.
|
|
308
|
+
|
|
309
|
+
Notes
|
|
310
|
+
-----
|
|
311
|
+
This method sorts the internal DataFrame by start_met and esa_step
|
|
312
|
+
for improved query performance.
|
|
313
|
+
"""
|
|
314
|
+
if not self._indexed:
|
|
315
|
+
# Sort by start_met and esa_step for better query performance
|
|
316
|
+
self.df = self.df.sort_values(["start_met", "esa_step"]).reset_index(
|
|
317
|
+
drop=True
|
|
318
|
+
)
|
|
319
|
+
self._indexed = True
|
|
320
|
+
|
|
321
|
+
def query(
|
|
322
|
+
self,
|
|
323
|
+
query_met: float | Iterable[float],
|
|
324
|
+
esa_step: int | Iterable[float],
|
|
325
|
+
) -> float | np.ndarray:
|
|
326
|
+
"""
|
|
327
|
+
Query MET(s) and esa_step(s) to retrieve esa_energy_step(s).
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
query_met : float or array_like
|
|
332
|
+
Mission elapsed time value(s) to query.
|
|
333
|
+
Can be a single float or array-like of floats.
|
|
334
|
+
esa_step : int or array_like
|
|
335
|
+
ESA step value(s) to match. Can be a single int or array-like of ints.
|
|
336
|
+
Must be same type (scalar or array-like) as query_met.
|
|
337
|
+
|
|
338
|
+
Returns
|
|
339
|
+
-------
|
|
340
|
+
float or numpy.ndarray
|
|
341
|
+
- If inputs are scalars: returns float (esa_energy_step)
|
|
342
|
+
- If inputs are array-like: returns numpy array of esa_energy_steps
|
|
343
|
+
with same length as inputs.
|
|
344
|
+
Contains FILLVAL for queries with no matches.
|
|
345
|
+
|
|
346
|
+
Raises
|
|
347
|
+
------
|
|
348
|
+
ValueError
|
|
349
|
+
If one input is scalar and the other is array-like, or if both are
|
|
350
|
+
array-like but have different lengths.
|
|
351
|
+
|
|
352
|
+
Notes
|
|
353
|
+
-----
|
|
354
|
+
If multiple entries match a query, returns the first match found.
|
|
355
|
+
"""
|
|
356
|
+
self._ensure_indexed()
|
|
357
|
+
|
|
358
|
+
# Check if inputs are scalars
|
|
359
|
+
is_scalar_met = np.isscalar(query_met)
|
|
360
|
+
is_scalar_step = np.isscalar(esa_step)
|
|
361
|
+
|
|
362
|
+
# Check for mismatched input types
|
|
363
|
+
if is_scalar_met != is_scalar_step:
|
|
364
|
+
raise ValueError(
|
|
365
|
+
"query_met and esa_step must both be scalars or both be array-like"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Convert to arrays for uniform processing
|
|
369
|
+
query_mets = np.atleast_1d(query_met)
|
|
370
|
+
esa_steps = np.atleast_1d(esa_step)
|
|
371
|
+
|
|
372
|
+
# Ensure both arrays have the same shape
|
|
373
|
+
if query_mets.shape != esa_steps.shape:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
"query_met and esa_step must have the same "
|
|
376
|
+
"length when both are array-like"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
results = np.full_like(query_mets, self._fillval)
|
|
380
|
+
|
|
381
|
+
# Lookup esa_energy_steps for queries
|
|
382
|
+
for i, (qm, es) in enumerate(zip(query_mets, esa_steps, strict=False)):
|
|
383
|
+
mask = (
|
|
384
|
+
(self.df["start_met"] <= qm)
|
|
385
|
+
& (self.df["end_met"] >= qm)
|
|
386
|
+
& (self.df["esa_step"] == es)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
matches = self.df[mask]
|
|
390
|
+
if not matches.empty:
|
|
391
|
+
results[i] = matches["esa_energy_step"].iloc[0]
|
|
392
|
+
|
|
393
|
+
# Return scalar for scalar inputs, array for array inputs
|
|
394
|
+
if is_scalar_met and is_scalar_step:
|
|
395
|
+
return results.astype(self._esa_energy_step_dtype)[0]
|
|
396
|
+
else:
|
|
397
|
+
return results.astype(self._esa_energy_step_dtype)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@pd.api.extensions.register_dataframe_accessor("cal_prod_config")
|
|
401
|
+
class CalibrationProductConfig:
|
|
402
|
+
"""
|
|
403
|
+
Register custom accessor for calibration product configuration DataFrames.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
pandas_obj : pandas.DataFrame
|
|
408
|
+
Object to run validation and use accessor functions on.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
index_columns = (
|
|
412
|
+
"calibration_prod",
|
|
413
|
+
"esa_energy_step",
|
|
414
|
+
)
|
|
415
|
+
tof_detector_pairs = ("ab", "ac1", "bc1", "c1c2")
|
|
416
|
+
required_columns = (
|
|
417
|
+
"coincidence_type_list",
|
|
418
|
+
*[
|
|
419
|
+
f"tof_{det_pair}_{limit}"
|
|
420
|
+
for det_pair in tof_detector_pairs
|
|
421
|
+
for limit in ["low", "high"]
|
|
422
|
+
],
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def __init__(self, pandas_obj: pd.DataFrame) -> None:
|
|
426
|
+
self._validate(pandas_obj)
|
|
427
|
+
self._obj = pandas_obj
|
|
428
|
+
self._add_coincidence_values_column()
|
|
429
|
+
|
|
430
|
+
def _validate(self, df: pd.DataFrame) -> None:
|
|
431
|
+
"""
|
|
432
|
+
Validate the current configuration.
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
df : pandas.DataFrame
|
|
437
|
+
Object to validate.
|
|
438
|
+
|
|
439
|
+
Raises
|
|
440
|
+
------
|
|
441
|
+
AttributeError : If the dataframe does not pass validation.
|
|
442
|
+
"""
|
|
443
|
+
for index_name in self.index_columns:
|
|
444
|
+
if index_name in df.index:
|
|
445
|
+
raise AttributeError(
|
|
446
|
+
f"Required index {index_name} not present in dataframe."
|
|
447
|
+
)
|
|
448
|
+
# Verify that the Dataframe has all the required columns
|
|
449
|
+
for col in self.required_columns:
|
|
450
|
+
if col not in df.columns:
|
|
451
|
+
raise AttributeError(f"Required column {col} not present in dataframe.")
|
|
452
|
+
# TODO: Verify that the same ESA energy steps exist in all unique calibration
|
|
453
|
+
# product numbers
|
|
454
|
+
|
|
455
|
+
def _add_coincidence_values_column(self) -> None:
|
|
456
|
+
"""Generate and add the coincidence_type_values column to the dataframe."""
|
|
457
|
+
# Add a column that consists of the coincidence type strings converted
|
|
458
|
+
# to integer values
|
|
459
|
+
self._obj["coincidence_type_values"] = self._obj.apply(
|
|
460
|
+
lambda row: tuple(
|
|
461
|
+
CoincidenceBitmap.detector_hit_str_to_int(entry)
|
|
462
|
+
for entry in row["coincidence_type_list"]
|
|
463
|
+
),
|
|
464
|
+
axis=1,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
@classmethod
|
|
468
|
+
def from_csv(cls, path: str | Path) -> pd.DataFrame:
|
|
469
|
+
"""
|
|
470
|
+
Read configuration CSV file into a pandas.DataFrame.
|
|
471
|
+
|
|
472
|
+
Parameters
|
|
473
|
+
----------
|
|
474
|
+
path : str or pathlib.Path
|
|
475
|
+
Location of the Calibration Product configuration CSV file.
|
|
476
|
+
|
|
477
|
+
Returns
|
|
478
|
+
-------
|
|
479
|
+
dataframe : pandas.DataFrame
|
|
480
|
+
Validated calibration product configuration data frame.
|
|
481
|
+
"""
|
|
482
|
+
df = pd.read_csv(
|
|
483
|
+
path,
|
|
484
|
+
index_col=cls.index_columns,
|
|
485
|
+
converters={"coincidence_type_list": lambda s: tuple(s.split("|"))},
|
|
486
|
+
comment="#",
|
|
487
|
+
)
|
|
488
|
+
# Force the _init_ method to run by using the namespace
|
|
489
|
+
_ = df.cal_prod_config.number_of_products
|
|
490
|
+
return df
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def number_of_products(self) -> int:
|
|
494
|
+
"""
|
|
495
|
+
Get the number of calibration products in the current configuration.
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
number_of_products : int
|
|
500
|
+
The maximum number of calibration products defined in the list of
|
|
501
|
+
calibration product definitions.
|
|
502
|
+
"""
|
|
503
|
+
return len(self._obj.index.unique(level="calibration_prod"))
|
|
@@ -114,6 +114,9 @@ FLAG_PATTERN = np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
|
|
114
114
|
# Define size of science frame (num of packets)
|
|
115
115
|
FRAME_SIZE = len(FLAG_PATTERN)
|
|
116
116
|
|
|
117
|
+
# Mod 10 pattern
|
|
118
|
+
MOD_10_PATTERN = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
|
119
|
+
|
|
117
120
|
# Define the number of bits in the mantissa and exponent for
|
|
118
121
|
# decompressing data
|
|
119
122
|
MANTISSA_BITS = 12
|
|
@@ -260,12 +260,9 @@ def assemble_science_frames(sci_dataset: xr.Dataset) -> xr.Dataset:
|
|
|
260
260
|
height event data per valid science frame added as new
|
|
261
261
|
data variables.
|
|
262
262
|
"""
|
|
263
|
-
# TODO:
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
# from the previous file. Only discard incomplete science frames
|
|
267
|
-
# in the middle of the CCSDS file. The code currently skips all
|
|
268
|
-
# incomplete science frames.
|
|
263
|
+
# TODO: The code currently skips all incomplete science frames.
|
|
264
|
+
# Only discard incomplete science frames in the middle of the CCSDS file or
|
|
265
|
+
# use fill values?
|
|
269
266
|
|
|
270
267
|
# Convert sequence flags and counters to NumPy arrays for vectorized operations
|
|
271
268
|
seq_flgs = sci_dataset.seq_flgs.values
|