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,17 +1,23 @@
|
|
|
1
1
|
# mypy: disable-error-code="unused-ignore"
|
|
2
2
|
"""Data classes for MAG L1D processing."""
|
|
3
3
|
|
|
4
|
+
import logging
|
|
4
5
|
from dataclasses import InitVar, dataclass
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import xarray as xr
|
|
8
9
|
|
|
10
|
+
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
|
|
11
|
+
from imap_processing.mag import imap_mag_sdc_configuration_v001 as configuration
|
|
9
12
|
from imap_processing.mag.constants import FILLVAL, DataMode
|
|
10
13
|
from imap_processing.mag.l1c.interpolation_methods import linear
|
|
11
14
|
from imap_processing.mag.l2.mag_l2 import retrieve_matrix_from_l2_calibration
|
|
12
15
|
from imap_processing.mag.l2.mag_l2_data import MagL2L1dBase, ValidFrames
|
|
13
16
|
from imap_processing.spice import spin
|
|
14
17
|
from imap_processing.spice.geometry import frame_transform
|
|
18
|
+
from imap_processing.spice.time import ttj2000ns_to_et, ttj2000ns_to_met
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
@dataclass
|
|
@@ -57,7 +63,7 @@ class MagL1dConfiguration:
|
|
|
57
63
|
mago_calibration: np.ndarray
|
|
58
64
|
magi_calibration: np.ndarray
|
|
59
65
|
spin_count_calibration: int
|
|
60
|
-
quality_flag_threshold:
|
|
66
|
+
quality_flag_threshold: float
|
|
61
67
|
spin_average_application_factor: np.float64
|
|
62
68
|
gradiometer_factor: np.ndarray
|
|
63
69
|
apply_gradiometry: bool = True
|
|
@@ -144,8 +150,6 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
144
150
|
truncate the data to exactly 24 hours.
|
|
145
151
|
"""
|
|
146
152
|
|
|
147
|
-
# TODO Quality flags
|
|
148
|
-
# TODO generate and output ancillary files
|
|
149
153
|
magi_vectors: np.ndarray
|
|
150
154
|
magi_range: np.ndarray
|
|
151
155
|
magi_epoch: np.ndarray
|
|
@@ -165,6 +169,9 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
165
169
|
The day we are processing, in np.datetime64[D] format. This is used to
|
|
166
170
|
truncate the data to exactly 24 hours.
|
|
167
171
|
"""
|
|
172
|
+
# The main data frame is MAGO, even though we have MAGI data included.
|
|
173
|
+
self.frame = ValidFrames.MAGO
|
|
174
|
+
|
|
168
175
|
# set the magnitude before truncating
|
|
169
176
|
self.magnitude = np.zeros(self.vectors.shape[0], dtype=np.float64) # type: ignore[has-type]
|
|
170
177
|
self.truncate_to_24h(day)
|
|
@@ -182,7 +189,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
182
189
|
|
|
183
190
|
self.vectors = self.apply_spin_offsets(
|
|
184
191
|
self.spin_offsets,
|
|
185
|
-
self.epoch,
|
|
192
|
+
self.epoch, # type: ignore[has-type]
|
|
186
193
|
self.vectors,
|
|
187
194
|
self.config.spin_average_application_factor,
|
|
188
195
|
)
|
|
@@ -198,7 +205,11 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
198
205
|
|
|
199
206
|
if self.config.apply_gradiometry:
|
|
200
207
|
self.gradiometry_offsets = self.calculate_gradiometry_offsets(
|
|
201
|
-
self.vectors,
|
|
208
|
+
self.vectors,
|
|
209
|
+
self.epoch, # type: ignore[has-type]
|
|
210
|
+
self.magi_vectors,
|
|
211
|
+
self.magi_epoch,
|
|
212
|
+
self.config.quality_flag_threshold,
|
|
202
213
|
)
|
|
203
214
|
self.vectors = self.apply_gradiometry_offsets(
|
|
204
215
|
self.gradiometry_offsets, self.vectors, self.config.gradiometer_factor
|
|
@@ -207,6 +218,54 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
207
218
|
self.magnitude = MagL2L1dBase.calculate_magnitude(vectors=self.vectors)
|
|
208
219
|
self.is_l1d = True
|
|
209
220
|
|
|
221
|
+
def generate_dataset(
|
|
222
|
+
self,
|
|
223
|
+
attribute_manager: ImapCdfAttributes,
|
|
224
|
+
day: np.datetime64,
|
|
225
|
+
) -> xr.Dataset:
|
|
226
|
+
"""
|
|
227
|
+
Generate an xarray dataset from the dataclass.
|
|
228
|
+
|
|
229
|
+
This overrides the parent method to conditionally swap MAGO/MAGI data
|
|
230
|
+
based on the always_output_mago configuration setting.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
attribute_manager : ImapCdfAttributes
|
|
235
|
+
CDF attributes object for the correct level.
|
|
236
|
+
day : np.datetime64
|
|
237
|
+
The 24 hour day to process, as a numpy datetime format.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
xr.Dataset
|
|
242
|
+
Complete dataset ready to write to CDF file.
|
|
243
|
+
"""
|
|
244
|
+
always_output_mago = configuration.ALWAYS_OUTPUT_MAGO
|
|
245
|
+
|
|
246
|
+
if not always_output_mago:
|
|
247
|
+
# Swap vectors and epochs to use MAGI data instead of MAGO
|
|
248
|
+
original_vectors: np.ndarray = self.vectors.copy()
|
|
249
|
+
original_epoch: np.ndarray = self.epoch.copy() # type: ignore[has-type]
|
|
250
|
+
original_range: np.ndarray = self.range.copy() # type: ignore[has-type]
|
|
251
|
+
|
|
252
|
+
self.vectors = self.magi_vectors # type: ignore[no-redef]
|
|
253
|
+
self.epoch = self.magi_epoch # type: ignore[no-redef]
|
|
254
|
+
self.range = self.magi_range # type: ignore[no-redef]
|
|
255
|
+
|
|
256
|
+
# Call parent generate_dataset method
|
|
257
|
+
dataset = super().generate_dataset(attribute_manager, day)
|
|
258
|
+
|
|
259
|
+
# Restore original vectors for any further processing
|
|
260
|
+
self.vectors = original_vectors
|
|
261
|
+
self.epoch = original_epoch
|
|
262
|
+
self.range = original_range
|
|
263
|
+
else:
|
|
264
|
+
# Use MAGO data (default behavior)
|
|
265
|
+
dataset = super().generate_dataset(attribute_manager, day)
|
|
266
|
+
|
|
267
|
+
return dataset
|
|
268
|
+
|
|
210
269
|
def rotate_frame(self, end_frame: ValidFrames) -> None:
|
|
211
270
|
"""
|
|
212
271
|
Rotate the vectors to the desired frame.
|
|
@@ -219,15 +278,42 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
219
278
|
end_frame : ValidFrames
|
|
220
279
|
The frame to rotate to. Should be one of the ValidFrames enum.
|
|
221
280
|
"""
|
|
281
|
+
# Self.frame should refer to the main data in self.vectors, which is MAGO
|
|
282
|
+
# data. For most frames, MAGO and MAGI are in the same frame, except the
|
|
283
|
+
# instrument reference frame.
|
|
284
|
+
if ValidFrames.MAGI in (self.frame, end_frame):
|
|
285
|
+
raise ValueError(
|
|
286
|
+
"MAGL1d.frame should never be equal to MAGI frame. If the "
|
|
287
|
+
"data is in the instrument frame, use MAGO."
|
|
288
|
+
)
|
|
289
|
+
|
|
222
290
|
start_frame = self.frame
|
|
223
|
-
|
|
291
|
+
|
|
292
|
+
if self.epoch_et is None:
|
|
293
|
+
self.epoch_et: np.ndarray = ttj2000ns_to_et(self.epoch)
|
|
294
|
+
self.magi_epoch_et: np.ndarray = ttj2000ns_to_et(self.magi_epoch)
|
|
295
|
+
|
|
296
|
+
self.vectors = frame_transform(
|
|
297
|
+
self.epoch_et,
|
|
298
|
+
self.vectors,
|
|
299
|
+
from_frame=start_frame.value,
|
|
300
|
+
to_frame=end_frame.value,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# If we were in MAGO frame, we need to rotate MAGI vectors from MAGI to
|
|
304
|
+
# end_frame
|
|
305
|
+
if start_frame == ValidFrames.MAGO:
|
|
306
|
+
start_frame = ValidFrames.MAGI
|
|
307
|
+
|
|
224
308
|
self.magi_vectors = frame_transform(
|
|
225
|
-
self.
|
|
309
|
+
self.magi_epoch_et,
|
|
226
310
|
self.magi_vectors,
|
|
227
311
|
from_frame=start_frame.value,
|
|
228
312
|
to_frame=end_frame.value,
|
|
229
313
|
)
|
|
230
314
|
|
|
315
|
+
self.frame = end_frame
|
|
316
|
+
|
|
231
317
|
def _calibrate_and_offset_vectors(
|
|
232
318
|
self,
|
|
233
319
|
mago_calibration: np.ndarray,
|
|
@@ -312,7 +398,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
312
398
|
The offset vector, shape (4,) where the last element is unchanged.
|
|
313
399
|
"""
|
|
314
400
|
# Offsets are in shape (sensor, range, axis)
|
|
315
|
-
updated_vector = input_vector.copy()
|
|
401
|
+
updated_vector = input_vector.copy()
|
|
316
402
|
rng = int(input_vector[3])
|
|
317
403
|
x_y_z = input_vector[:3]
|
|
318
404
|
updated_vector[:3] = x_y_z - offsets[int(is_magi), rng, :]
|
|
@@ -348,64 +434,132 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
348
434
|
"Spin offsets can only be calculated in NORM mode and SRF frame."
|
|
349
435
|
)
|
|
350
436
|
|
|
351
|
-
|
|
352
|
-
sc_spin_phase
|
|
437
|
+
epoch_met = ttj2000ns_to_met(self.epoch)
|
|
438
|
+
sc_spin_phase = spin.get_spacecraft_spin_phase(epoch_met)
|
|
353
439
|
# mark vectors as nan where they are nan in sc_spin_phase
|
|
354
440
|
vectors = self.vectors.copy().astype(np.float64)
|
|
355
441
|
|
|
356
442
|
vectors[np.isnan(sc_spin_phase), :] = np.nan
|
|
357
443
|
|
|
358
|
-
# TODO: currently fully skipping spins with no valid data (not including
|
|
359
|
-
# them in the averaging OR IN SPIN COUNTING!) is this correct?
|
|
360
|
-
|
|
361
444
|
# first timestamp where spin phase is less than the previous value
|
|
362
445
|
# this is when the spin crosses zero
|
|
363
446
|
spin_starts = np.where(np.diff(sc_spin_phase) < 0)[0] + 1
|
|
364
447
|
|
|
365
|
-
# if the value switches from nan to a number,
|
|
366
|
-
#
|
|
367
|
-
nan_to_number = (
|
|
368
|
-
np.where(np.isnan(sc_spin_phase[:-1]) & ~np.isnan(sc_spin_phase[1:]))[0] + 1
|
|
369
|
-
)
|
|
448
|
+
# if the value switches from nan to a number, or from a number to nan, that
|
|
449
|
+
# is also a spin start
|
|
450
|
+
nan_to_number = np.where(np.diff(np.isnan(sc_spin_phase)) != 0)[0] + 1
|
|
370
451
|
|
|
371
452
|
# find the places spins start while skipping over invalid or missing data
|
|
372
453
|
# (marked as nan by get_spacecraft_spin_phase)
|
|
373
454
|
spin_starts = np.sort(np.concatenate((spin_starts, nan_to_number)))
|
|
374
455
|
|
|
456
|
+
# Get the expected spin period from the spin table
|
|
457
|
+
# Convert to nanoseconds to match epoch
|
|
458
|
+
spin_data = spin.get_spin_data()
|
|
459
|
+
# Use the median spin period as the expected value
|
|
460
|
+
expected_spin = np.median(spin_data["spin_period_sec"]) * 1e9
|
|
461
|
+
|
|
462
|
+
paired_nans = nan_to_number.reshape(-1, 2)
|
|
463
|
+
|
|
464
|
+
for start_of_gap, end_of_gap in paired_nans:
|
|
465
|
+
# in nan_to_number, we have the start and end for every nan gap
|
|
466
|
+
# if this gap spans more than 1 spin period, we need to insert
|
|
467
|
+
# additional spin_starts into spin_starts.
|
|
468
|
+
|
|
469
|
+
gap_start_time = self.epoch[start_of_gap]
|
|
470
|
+
gap_end_time = self.epoch[end_of_gap]
|
|
471
|
+
|
|
472
|
+
# Calculate the number of spins in this gap
|
|
473
|
+
number_of_spins = int((gap_end_time - gap_start_time) // expected_spin)
|
|
474
|
+
if number_of_spins > 1:
|
|
475
|
+
# Insert new spin starts into spin_starts
|
|
476
|
+
for i in range(1, number_of_spins):
|
|
477
|
+
estimated_start = gap_start_time + i * expected_spin
|
|
478
|
+
new_spin_index = (np.abs(self.epoch - estimated_start)).argmin()
|
|
479
|
+
|
|
480
|
+
spin_starts = np.append(spin_starts, new_spin_index)
|
|
481
|
+
|
|
482
|
+
# Now spin_starts contains all the indices where spins begin, including
|
|
483
|
+
# estimating skipped or missing spins.
|
|
484
|
+
spin_starts = np.sort(spin_starts)
|
|
485
|
+
|
|
375
486
|
chunk_start = 0
|
|
376
487
|
offset_epochs = []
|
|
377
|
-
|
|
378
|
-
|
|
488
|
+
x_avg_calcs: list[np.float64] = []
|
|
489
|
+
y_avg_calcs: list[np.float64] = []
|
|
490
|
+
validity_start_times = []
|
|
491
|
+
validity_end_times = []
|
|
492
|
+
start_spin_counters = []
|
|
493
|
+
end_spin_counters = []
|
|
494
|
+
|
|
379
495
|
while chunk_start < len(spin_starts):
|
|
380
496
|
# Take self.spin_count_calibration number of spins and put them into a chunk
|
|
381
497
|
chunk_indices = spin_starts[
|
|
382
498
|
chunk_start : chunk_start + self.config.spin_count_calibration + 1
|
|
383
499
|
]
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
# If we are in the end of the chunk, just grab all remaining data
|
|
387
|
-
if chunk_start >= len(spin_starts):
|
|
388
|
-
chunk_indices = np.append(chunk_indices, len(self.epoch))
|
|
500
|
+
chunk_start_idx = chunk_start
|
|
389
501
|
|
|
390
502
|
chunk_vectors = self.vectors[chunk_indices[0] : chunk_indices[-1]]
|
|
391
503
|
chunk_epoch = self.epoch[chunk_indices[0] : chunk_indices[-1]]
|
|
392
504
|
|
|
505
|
+
# Check if more than half of the chunk data is NaN before processing
|
|
506
|
+
x_valid_count: int = int(np.sum(~np.isnan(chunk_vectors[:, 0])))
|
|
507
|
+
y_valid_count: int = int(np.sum(~np.isnan(chunk_vectors[:, 1])))
|
|
508
|
+
total_points = len(chunk_vectors)
|
|
509
|
+
|
|
393
510
|
# average the x and y axes (z is fixed, as the spin axis)
|
|
394
|
-
# TODO: is z the correct axis here?
|
|
395
511
|
avg_x = np.nanmean(chunk_vectors[:, 0])
|
|
396
512
|
avg_y = np.nanmean(chunk_vectors[:, 1])
|
|
397
513
|
|
|
514
|
+
# Skip chunk if more than half of x or y data is NaN, or if we have less
|
|
515
|
+
# than half a spin.
|
|
516
|
+
# in this case, we should reuse the previous averages.
|
|
517
|
+
if (
|
|
518
|
+
x_valid_count <= total_points / 2
|
|
519
|
+
or y_valid_count <= total_points / 2
|
|
520
|
+
or total_points <= self.config.spin_count_calibration / 2
|
|
521
|
+
):
|
|
522
|
+
avg_x = x_avg_calcs[-1] if x_avg_calcs else np.float64(FILLVAL)
|
|
523
|
+
avg_y = y_avg_calcs[-1] if y_avg_calcs else np.float64(FILLVAL)
|
|
524
|
+
|
|
398
525
|
if not np.isnan(avg_x) and not np.isnan(avg_y):
|
|
399
526
|
offset_epochs.append(chunk_epoch[0])
|
|
400
|
-
|
|
401
|
-
|
|
527
|
+
x_avg_calcs.append(avg_x)
|
|
528
|
+
y_avg_calcs.append(avg_y)
|
|
529
|
+
|
|
530
|
+
# Add validity time range for this chunk
|
|
531
|
+
validity_start_times.append(chunk_epoch[0])
|
|
532
|
+
validity_end_times.append(chunk_epoch[-1])
|
|
533
|
+
|
|
534
|
+
# Add spin counter information
|
|
535
|
+
start_spin_counters.append(chunk_start_idx)
|
|
536
|
+
end_spin_counters.append(
|
|
537
|
+
min(
|
|
538
|
+
chunk_start_idx + self.config.spin_count_calibration - 1,
|
|
539
|
+
len(spin_starts) - 1,
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
chunk_start = chunk_start + self.config.spin_count_calibration
|
|
402
544
|
|
|
403
545
|
spin_epoch_dataarray = xr.DataArray(np.array(offset_epochs))
|
|
404
546
|
|
|
405
547
|
spin_offsets = xr.Dataset(coords={"epoch": spin_epoch_dataarray})
|
|
406
548
|
|
|
407
|
-
spin_offsets["x_offset"] = xr.DataArray(np.array(
|
|
408
|
-
spin_offsets["y_offset"] = xr.DataArray(np.array(
|
|
549
|
+
spin_offsets["x_offset"] = xr.DataArray(np.array(x_avg_calcs), dims=["epoch"])
|
|
550
|
+
spin_offsets["y_offset"] = xr.DataArray(np.array(y_avg_calcs), dims=["epoch"])
|
|
551
|
+
spin_offsets["validity_start_time"] = xr.DataArray(
|
|
552
|
+
np.array(validity_start_times), dims=["epoch"]
|
|
553
|
+
)
|
|
554
|
+
spin_offsets["validity_end_time"] = xr.DataArray(
|
|
555
|
+
np.array(validity_end_times), dims=["epoch"]
|
|
556
|
+
)
|
|
557
|
+
spin_offsets["start_spin_counter"] = xr.DataArray(
|
|
558
|
+
np.array(start_spin_counters), dims=["epoch"]
|
|
559
|
+
)
|
|
560
|
+
spin_offsets["end_spin_counter"] = xr.DataArray(
|
|
561
|
+
np.array(end_spin_counters), dims=["epoch"]
|
|
562
|
+
)
|
|
409
563
|
|
|
410
564
|
return spin_offsets
|
|
411
565
|
|
|
@@ -462,7 +616,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
462
616
|
if spin_offsets is None:
|
|
463
617
|
raise ValueError("No spin offsets calculated to apply.")
|
|
464
618
|
|
|
465
|
-
output_vectors = np.full(vectors.shape, FILLVAL, dtype=np.
|
|
619
|
+
output_vectors = np.full(vectors.shape, FILLVAL, dtype=np.float64)
|
|
466
620
|
|
|
467
621
|
for index in range(spin_offsets["epoch"].data.shape[0] - 1):
|
|
468
622
|
timestamp = spin_offsets["epoch"].data[index]
|
|
@@ -483,7 +637,6 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
483
637
|
if not np.any(mask):
|
|
484
638
|
continue
|
|
485
639
|
|
|
486
|
-
# TODO: should vectors be a float?
|
|
487
640
|
x_offset = (
|
|
488
641
|
spin_offsets["x_offset"].data[index] * spin_average_application_factor
|
|
489
642
|
)
|
|
@@ -504,6 +657,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
504
657
|
mago_epoch: np.ndarray,
|
|
505
658
|
magi_vectors: np.ndarray,
|
|
506
659
|
magi_epoch: np.ndarray,
|
|
660
|
+
quality_flag_threshold: float = np.inf,
|
|
507
661
|
) -> xr.Dataset:
|
|
508
662
|
"""
|
|
509
663
|
Calculate the gradiometry offsets between MAGo and MAGi.
|
|
@@ -525,6 +679,10 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
525
679
|
The MAGi vectors, shape (N, 3).
|
|
526
680
|
magi_epoch : np.ndarray
|
|
527
681
|
The MAGi epoch values, shape (N,).
|
|
682
|
+
quality_flag_threshold : np.float64, optional
|
|
683
|
+
Threshold for quality flags. If the magnitude of gradiometer offset
|
|
684
|
+
exceeds this threshold, quality flag will be set. Default is np.inf
|
|
685
|
+
(no quality flags set).
|
|
528
686
|
|
|
529
687
|
Returns
|
|
530
688
|
-------
|
|
@@ -532,6 +690,8 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
532
690
|
The gradiometer offsets dataset, with variables:
|
|
533
691
|
- epoch: the timestamp of the MAGo data
|
|
534
692
|
- gradiometer_offsets: the offset values (MAGi - MAGo) for each axis
|
|
693
|
+
- gradiometer_offset_magnitude: magnitude of the offset vector
|
|
694
|
+
- quality_flags: quality flags (1 if magnitude > threshold, 0 otherwise)
|
|
535
695
|
"""
|
|
536
696
|
aligned_magi = linear(
|
|
537
697
|
magi_vectors,
|
|
@@ -541,10 +701,20 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
541
701
|
|
|
542
702
|
diff = aligned_magi - mago_vectors
|
|
543
703
|
|
|
704
|
+
# Calculate magnitude of gradiometer offset for each vector
|
|
705
|
+
magnitude = np.linalg.norm(diff, axis=1)
|
|
706
|
+
|
|
707
|
+
# Set quality flags: 0 = good data (below threshold), 1 = bad data
|
|
708
|
+
quality_flags = (magnitude > quality_flag_threshold).astype(int)
|
|
709
|
+
|
|
544
710
|
grad_epoch = xr.DataArray(mago_epoch, dims=["epoch"])
|
|
545
711
|
direction = xr.DataArray(["x", "y", "z"], dims=["axis"])
|
|
546
712
|
grad_ds = xr.Dataset(coords={"epoch": grad_epoch, "direction": direction})
|
|
547
713
|
grad_ds["gradiometer_offsets"] = xr.DataArray(diff, dims=["epoch", "direction"])
|
|
714
|
+
grad_ds["gradiometer_offset_magnitude"] = xr.DataArray(
|
|
715
|
+
magnitude, dims=["epoch"]
|
|
716
|
+
)
|
|
717
|
+
grad_ds["quality_flags"] = xr.DataArray(quality_flags, dims=["epoch"])
|
|
548
718
|
|
|
549
719
|
return grad_ds
|
|
550
720
|
|
imap_processing/mag/l2/mag_l2.py
CHANGED
|
@@ -90,6 +90,7 @@ def mag_l2(
|
|
|
90
90
|
)
|
|
91
91
|
# level 2 vectors don't include range
|
|
92
92
|
vectors = cal_vectors[:, :3]
|
|
93
|
+
instrument_frame = ValidFrames.MAGO if always_output_mago else ValidFrames.MAGI
|
|
93
94
|
|
|
94
95
|
l2_data = MagL2(
|
|
95
96
|
vectors=vectors,
|
|
@@ -101,6 +102,7 @@ def mag_l2(
|
|
|
101
102
|
data_mode=mode,
|
|
102
103
|
offsets=offsets_dataset["offsets"].data,
|
|
103
104
|
timedelta=offsets_dataset["timedeltas"].data,
|
|
105
|
+
frame=instrument_frame,
|
|
104
106
|
)
|
|
105
107
|
|
|
106
108
|
attributes = ImapCdfAttributes()
|
|
@@ -13,16 +13,19 @@ from imap_processing.spice.geometry import SpiceFrame, frame_transform
|
|
|
13
13
|
from imap_processing.spice.time import (
|
|
14
14
|
et_to_ttj2000ns,
|
|
15
15
|
str_to_et,
|
|
16
|
+
ttj2000ns_to_et,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class ValidFrames(Enum):
|
|
20
21
|
"""SPICE reference frames for output."""
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
MAGO = SpiceFrame.IMAP_MAG_O
|
|
24
|
+
MAGI = SpiceFrame.IMAP_MAG_I
|
|
23
25
|
DSRF = SpiceFrame.IMAP_DPS
|
|
24
26
|
SRF = SpiceFrame.IMAP_SPACECRAFT
|
|
25
|
-
|
|
27
|
+
GSE = SpiceFrame.IMAP_GSE
|
|
28
|
+
RTN = SpiceFrame.IMAP_RTN
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
@dataclass(kw_only=True)
|
|
@@ -54,7 +57,10 @@ class MagL2L1dBase:
|
|
|
54
57
|
Quality bitmask for each vector. Should be of length n. Copied from offset
|
|
55
58
|
file in L2, marked as good always in L1D.
|
|
56
59
|
frame:
|
|
57
|
-
The reference frame of the input vectors.
|
|
60
|
+
The reference frame of the input vectors. Defaults to the MAGO instrument frame.
|
|
61
|
+
epoch_et: np.ndarray
|
|
62
|
+
The epoch timestamps converted to ET format. Used for frame transformations.
|
|
63
|
+
Calculated on first use and then saved. Should not be passed in.
|
|
58
64
|
"""
|
|
59
65
|
|
|
60
66
|
vectors: np.ndarray
|
|
@@ -65,7 +71,8 @@ class MagL2L1dBase:
|
|
|
65
71
|
quality_bitmask: np.ndarray
|
|
66
72
|
data_mode: DataMode
|
|
67
73
|
magnitude: np.ndarray = field(init=False)
|
|
68
|
-
frame: ValidFrames = ValidFrames.
|
|
74
|
+
frame: ValidFrames = ValidFrames.MAGO
|
|
75
|
+
epoch_et: np.ndarray | None = field(init=False, default=None)
|
|
69
76
|
|
|
70
77
|
def generate_dataset(
|
|
71
78
|
self,
|
|
@@ -301,8 +308,10 @@ class MagL2L1dBase:
|
|
|
301
308
|
The frame to rotate the data to. Must be one of the ValidFrames enum
|
|
302
309
|
values.
|
|
303
310
|
"""
|
|
311
|
+
if self.epoch_et is None:
|
|
312
|
+
self.epoch_et = ttj2000ns_to_et(self.epoch)
|
|
304
313
|
self.vectors = frame_transform(
|
|
305
|
-
self.
|
|
314
|
+
self.epoch_et,
|
|
306
315
|
self.vectors,
|
|
307
316
|
from_frame=self.frame.value,
|
|
308
317
|
to_frame=end_frame.value,
|
imap_processing/quality_flags.py
CHANGED
|
@@ -37,12 +37,13 @@ class ENAFlags(FlagNameMixin):
|
|
|
37
37
|
BADSPIN = 2**2 # bit 2, Bad spin
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
class
|
|
40
|
+
class ImapDEOutliersUltraFlags(FlagNameMixin):
|
|
41
41
|
"""IMAP Ultra flags."""
|
|
42
42
|
|
|
43
43
|
NONE = CommonFlags.NONE
|
|
44
44
|
FOV = 2**0 # bit 0
|
|
45
45
|
PHCORR = 2**1 # bit 1
|
|
46
|
+
COINPH = 2**2 # bit 4 # Event validity
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
class ImapHkUltraFlags(FlagNameMixin):
|
|
@@ -75,6 +76,21 @@ class ImapRatesUltraFlags(FlagNameMixin):
|
|
|
75
76
|
PARTIALSPIN = 2**2 # bit 2
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
class ImapDEScatteringUltraFlags(FlagNameMixin):
|
|
80
|
+
"""IMAP Ultra Scattering flags."""
|
|
81
|
+
|
|
82
|
+
NONE = CommonFlags.NONE
|
|
83
|
+
ABOVE_THRESHOLD = 2**0 # bit 0
|
|
84
|
+
NAN_PHI_OR_THETA = 2**1 # bit 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ImapPSETUltraFlags(FlagNameMixin):
|
|
88
|
+
"""IMAP Ultra Rates flags."""
|
|
89
|
+
|
|
90
|
+
NONE = CommonFlags.NONE
|
|
91
|
+
EARTH_FOV = 2**0 # bit 0
|
|
92
|
+
|
|
93
|
+
|
|
78
94
|
class ImapInstrumentUltraFlags(FlagNameMixin):
|
|
79
95
|
"""IMAP Ultra flags using other instruments."""
|
|
80
96
|
|
|
@@ -123,3 +139,9 @@ class SWAPIFlags(
|
|
|
123
139
|
SCEM_V_ST = 2**12 # bit 12
|
|
124
140
|
SCEM_I_ST = 2**13 # bit 13
|
|
125
141
|
SCEM_INT_ST = 2**14 # bit 14
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class GLOWSL1bFlags(FlagNameMixin):
|
|
145
|
+
"""Glows L1b flags."""
|
|
146
|
+
|
|
147
|
+
NONE = CommonFlags.NONE
|