imap-processing 0.18.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/cdf/config/imap_codice_l1a_variable_attrs.yaml +301 -274
- imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +28 -28
- 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_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_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_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_l1b_variable_attrs.yaml +12 -4
- imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +50 -7
- imap_processing/cli.py +95 -41
- imap_processing/codice/codice_l1a.py +131 -31
- imap_processing/codice/codice_l2.py +118 -10
- imap_processing/codice/constants.py +740 -595
- 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 +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 +5 -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/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/l0/lo_science.py +25 -24
- 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/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 +167 -30
- imap_processing/mag/l2/mag_l2_data.py +10 -2
- imap_processing/quality_flags.py +9 -1
- imap_processing/spice/geometry.py +76 -33
- imap_processing/spice/pointing_frame.py +0 -6
- imap_processing/spice/repoint.py +29 -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 +1 -24
- imap_processing/ultra/l0/ultra_utils.py +9 -11
- imap_processing/ultra/l1a/ultra_l1a.py +1 -2
- imap_processing/ultra/l1b/cullingmask.py +6 -3
- imap_processing/ultra/l1b/de.py +81 -23
- imap_processing/ultra/l1b/extendedspin.py +13 -10
- 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_culling.py +161 -3
- imap_processing/ultra/l1b/ultra_l1b_extended.py +253 -47
- imap_processing/ultra/l1c/helio_pset.py +97 -24
- imap_processing/ultra/l1c/l1c_lookup_utils.py +256 -0
- imap_processing/ultra/l1c/spacecraft_pset.py +83 -16
- imap_processing/ultra/l1c/ultra_l1c.py +6 -2
- imap_processing/ultra/l1c/ultra_l1c_culling.py +85 -0
- imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +385 -277
- imap_processing/ultra/l2/ultra_l2.py +0 -1
- imap_processing/ultra/utils/ultra_l1_utils.py +28 -3
- imap_processing/utils.py +3 -4
- {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/METADATA +2 -2
- {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/RECORD +102 -95
- imap_processing/idex/idex_l2c.py +0 -84
- imap_processing/spice/kernels.py +0 -187
- {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/LICENSE +0 -0
- {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/WHEEL +0 -0
- {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""MAG L1C processing module."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import numpy as np
|
|
7
6
|
import xarray as xr
|
|
@@ -125,6 +124,23 @@ def mag_l1c(
|
|
|
125
124
|
try:
|
|
126
125
|
global_attributes["is_mago"] = normal_mode_dataset.attrs["is_mago"]
|
|
127
126
|
global_attributes["is_active"] = normal_mode_dataset.attrs["is_active"]
|
|
127
|
+
|
|
128
|
+
# Check if all vectors are primary in both normal and burst datasets
|
|
129
|
+
is_mago = normal_mode_dataset.attrs.get("is_mago", "False") == "True"
|
|
130
|
+
normal_all_primary = normal_mode_dataset.attrs.get("all_vectors_primary", False)
|
|
131
|
+
|
|
132
|
+
# Default for missing burst dataset: 1 if MAGO (expected primary), 0 if MAGI
|
|
133
|
+
burst_all_primary = is_mago
|
|
134
|
+
if burst_mode_dataset is not None:
|
|
135
|
+
burst_all_primary = burst_mode_dataset.attrs.get(
|
|
136
|
+
"all_vectors_primary", False
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Both datasets must have all vectors primary for the combined result to be True
|
|
140
|
+
global_attributes["all_vectors_primary"] = (
|
|
141
|
+
normal_all_primary and burst_all_primary
|
|
142
|
+
)
|
|
143
|
+
|
|
128
144
|
global_attributes["missing_sequences"] = normal_mode_dataset.attrs[
|
|
129
145
|
"missing_sequences"
|
|
130
146
|
]
|
|
@@ -162,8 +178,9 @@ def mag_l1c(
|
|
|
162
178
|
output_core_dims=[[]],
|
|
163
179
|
vectorize=True,
|
|
164
180
|
)
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
output_dataset[
|
|
182
|
+
"vector_magnitude"
|
|
183
|
+
].attrs = attribute_manager.get_variable_attributes("vector_magnitude_attrs")
|
|
167
184
|
|
|
168
185
|
output_dataset["compression_flags"] = xr.DataArray(
|
|
169
186
|
completed_timeline[:, 6:8],
|
|
@@ -176,14 +193,14 @@ def mag_l1c(
|
|
|
176
193
|
completed_timeline[:, 5],
|
|
177
194
|
name="generated_flag",
|
|
178
195
|
dims=["epoch"],
|
|
179
|
-
|
|
196
|
+
attrs=attribute_manager.get_variable_attributes("generated_flag_attrs"),
|
|
180
197
|
)
|
|
181
198
|
|
|
182
199
|
return output_dataset
|
|
183
200
|
|
|
184
201
|
|
|
185
202
|
def select_datasets(
|
|
186
|
-
first_input_dataset: xr.Dataset, second_input_dataset:
|
|
203
|
+
first_input_dataset: xr.Dataset, second_input_dataset: xr.Dataset | None = None
|
|
187
204
|
) -> tuple[xr.Dataset, xr.Dataset]:
|
|
188
205
|
"""
|
|
189
206
|
Given one or two datasets, assign one to norm and one to burst.
|
|
@@ -503,7 +520,7 @@ def generate_timeline(epoch_data: np.ndarray, gaps: np.ndarray) -> np.ndarray:
|
|
|
503
520
|
|
|
504
521
|
|
|
505
522
|
def find_all_gaps(
|
|
506
|
-
epoch_data: np.ndarray, vecsec_dict:
|
|
523
|
+
epoch_data: np.ndarray, vecsec_dict: dict | None = None
|
|
507
524
|
) -> np.ndarray:
|
|
508
525
|
"""
|
|
509
526
|
Find all the gaps in the epoch data.
|
|
@@ -9,7 +9,7 @@ from imap_processing.mag.l1d.mag_l1d_data import MagL1d, MagL1dConfiguration
|
|
|
9
9
|
from imap_processing.mag.l2.mag_l2_data import ValidFrames
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def mag_l1d(
|
|
12
|
+
def mag_l1d( # noqa: PLR0912
|
|
13
13
|
science_data: list[xr.Dataset],
|
|
14
14
|
calibration_dataset: xr.Dataset,
|
|
15
15
|
day_to_process: np.datetime64,
|
|
@@ -45,16 +45,18 @@ def mag_l1d(
|
|
|
45
45
|
input_mago_burst = None
|
|
46
46
|
for dataset in science_data:
|
|
47
47
|
source = dataset.attrs.get("Logical_source", "")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
instrument_mode = source.split("_")[-1]
|
|
49
|
+
match instrument_mode:
|
|
50
|
+
case "norm-magi":
|
|
51
|
+
input_magi_norm = dataset
|
|
52
|
+
case "norm-mago":
|
|
53
|
+
input_mago_norm = dataset
|
|
54
|
+
case "burst-magi":
|
|
55
|
+
input_magi_burst = dataset
|
|
56
|
+
case "burst-mago":
|
|
57
|
+
input_mago_burst = dataset
|
|
58
|
+
case _:
|
|
59
|
+
raise ValueError(f"Input data has invalid logical source {source}")
|
|
58
60
|
|
|
59
61
|
if input_magi_norm is None or input_mago_norm is None:
|
|
60
62
|
raise ValueError(
|
|
@@ -72,8 +74,9 @@ def mag_l1d(
|
|
|
72
74
|
mago_vectors = input_mago_norm["vectors"].data[:, :3]
|
|
73
75
|
magi_vectors = input_magi_norm["vectors"].data[:, :3]
|
|
74
76
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
+
# Verify that MAGO is primary sensor for all vectors before applying gradiometry
|
|
78
|
+
if not input_mago_norm.attrs.get("all_vectors_primary", 1):
|
|
79
|
+
config.apply_gradiometry = False
|
|
77
80
|
|
|
78
81
|
# TODO: L1D attributes
|
|
79
82
|
attributes = ImapCdfAttributes()
|
|
@@ -95,12 +98,21 @@ def mag_l1d(
|
|
|
95
98
|
day=day,
|
|
96
99
|
)
|
|
97
100
|
|
|
101
|
+
# Nominally, this is expected to create MAGO data. However, if the configuration
|
|
102
|
+
# setting for always_output_mago is set to False, it will create MAGI data.
|
|
103
|
+
|
|
98
104
|
l1d_norm.rotate_frame(ValidFrames.SRF)
|
|
99
105
|
norm_srf_dataset = l1d_norm.generate_dataset(attributes, day_to_process)
|
|
100
106
|
l1d_norm.rotate_frame(ValidFrames.DSRF)
|
|
101
107
|
norm_dsrf_dataset = l1d_norm.generate_dataset(attributes, day_to_process)
|
|
108
|
+
l1d_norm.rotate_frame(ValidFrames.GSE)
|
|
109
|
+
norm_gse_dataset = l1d_norm.generate_dataset(attributes, day_to_process)
|
|
110
|
+
l1d_norm.rotate_frame(ValidFrames.RTN)
|
|
111
|
+
norm_rtn_dataset = l1d_norm.generate_dataset(attributes, day_to_process)
|
|
102
112
|
output_datasets.append(norm_srf_dataset)
|
|
103
113
|
output_datasets.append(norm_dsrf_dataset)
|
|
114
|
+
output_datasets.append(norm_gse_dataset)
|
|
115
|
+
output_datasets.append(norm_rtn_dataset)
|
|
104
116
|
|
|
105
117
|
if input_mago_burst is not None and input_magi_burst is not None:
|
|
106
118
|
# If burst data is provided, use it to create the burst L1d dataset
|
|
@@ -122,12 +134,43 @@ def mag_l1d(
|
|
|
122
134
|
spin_offsets=l1d_norm.spin_offsets,
|
|
123
135
|
day=day,
|
|
124
136
|
)
|
|
137
|
+
|
|
138
|
+
# TODO: frame specific attributes may be required
|
|
125
139
|
l1d_burst.rotate_frame(ValidFrames.SRF)
|
|
126
140
|
burst_srf_dataset = l1d_burst.generate_dataset(attributes, day_to_process)
|
|
127
141
|
l1d_burst.rotate_frame(ValidFrames.DSRF)
|
|
128
142
|
burst_dsrf_dataset = l1d_burst.generate_dataset(attributes, day_to_process)
|
|
143
|
+
l1d_burst.rotate_frame(ValidFrames.GSE)
|
|
144
|
+
burst_gse_dataset = l1d_burst.generate_dataset(attributes, day_to_process)
|
|
145
|
+
l1d_burst.rotate_frame(ValidFrames.RTN)
|
|
146
|
+
burst_rtn_dataset = l1d_burst.generate_dataset(attributes, day_to_process)
|
|
129
147
|
output_datasets.append(burst_srf_dataset)
|
|
130
148
|
output_datasets.append(burst_dsrf_dataset)
|
|
149
|
+
output_datasets.append(burst_gse_dataset)
|
|
150
|
+
output_datasets.append(burst_rtn_dataset)
|
|
151
|
+
|
|
152
|
+
# Output ancillary files
|
|
153
|
+
# Add spin offsets dataset from normal mode processing
|
|
154
|
+
if l1d_norm.spin_offsets is not None:
|
|
155
|
+
spin_offset_dataset = l1d_norm.generate_spin_offset_dataset()
|
|
156
|
+
spin_offset_dataset.attrs["Logical_source"] = "imap_mag_l1d-spin-offsets"
|
|
157
|
+
output_datasets.append(spin_offset_dataset)
|
|
158
|
+
|
|
159
|
+
# Add gradiometry offsets dataset if gradiometry was applied
|
|
160
|
+
if l1d_norm.config.apply_gradiometry and hasattr(l1d_norm, "gradiometry_offsets"):
|
|
161
|
+
gradiometry_dataset = l1d_norm.gradiometry_offsets.copy()
|
|
162
|
+
gradiometry_dataset.attrs["Logical_source"] = (
|
|
163
|
+
"imap_mag_l1d-gradiometry-offsets-norm"
|
|
164
|
+
)
|
|
165
|
+
output_datasets.append(gradiometry_dataset)
|
|
166
|
+
|
|
167
|
+
# Also add burst gradiometry offsets if burst data was processed
|
|
168
|
+
if input_mago_burst is not None and input_magi_burst is not None:
|
|
169
|
+
if hasattr(l1d_burst, "gradiometry_offsets"):
|
|
170
|
+
burst_gradiometry_dataset = l1d_burst.gradiometry_offsets.copy()
|
|
171
|
+
burst_gradiometry_dataset.attrs["Logical_source"] = (
|
|
172
|
+
"imap_mag_l1d-gradiometry-offsets-burst"
|
|
173
|
+
)
|
|
174
|
+
output_datasets.append(burst_gradiometry_dataset)
|
|
131
175
|
|
|
132
|
-
# TODO: Output ancillary files
|
|
133
176
|
return output_datasets
|
|
@@ -6,12 +6,15 @@ from dataclasses import InitVar, dataclass
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import xarray as xr
|
|
8
8
|
|
|
9
|
+
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
|
|
10
|
+
from imap_processing.mag import imap_mag_sdc_configuration_v001 as configuration
|
|
9
11
|
from imap_processing.mag.constants import FILLVAL, DataMode
|
|
10
12
|
from imap_processing.mag.l1c.interpolation_methods import linear
|
|
11
13
|
from imap_processing.mag.l2.mag_l2 import retrieve_matrix_from_l2_calibration
|
|
12
14
|
from imap_processing.mag.l2.mag_l2_data import MagL2L1dBase, ValidFrames
|
|
13
15
|
from imap_processing.spice import spin
|
|
14
16
|
from imap_processing.spice.geometry import frame_transform
|
|
17
|
+
from imap_processing.spice.time import ttj2000ns_to_met
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
@dataclass
|
|
@@ -57,7 +60,7 @@ class MagL1dConfiguration:
|
|
|
57
60
|
mago_calibration: np.ndarray
|
|
58
61
|
magi_calibration: np.ndarray
|
|
59
62
|
spin_count_calibration: int
|
|
60
|
-
quality_flag_threshold:
|
|
63
|
+
quality_flag_threshold: float
|
|
61
64
|
spin_average_application_factor: np.float64
|
|
62
65
|
gradiometer_factor: np.ndarray
|
|
63
66
|
apply_gradiometry: bool = True
|
|
@@ -144,8 +147,6 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
144
147
|
truncate the data to exactly 24 hours.
|
|
145
148
|
"""
|
|
146
149
|
|
|
147
|
-
# TODO Quality flags
|
|
148
|
-
# TODO generate and output ancillary files
|
|
149
150
|
magi_vectors: np.ndarray
|
|
150
151
|
magi_range: np.ndarray
|
|
151
152
|
magi_epoch: np.ndarray
|
|
@@ -182,7 +183,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
182
183
|
|
|
183
184
|
self.vectors = self.apply_spin_offsets(
|
|
184
185
|
self.spin_offsets,
|
|
185
|
-
self.epoch,
|
|
186
|
+
self.epoch, # type: ignore[has-type]
|
|
186
187
|
self.vectors,
|
|
187
188
|
self.config.spin_average_application_factor,
|
|
188
189
|
)
|
|
@@ -198,7 +199,11 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
198
199
|
|
|
199
200
|
if self.config.apply_gradiometry:
|
|
200
201
|
self.gradiometry_offsets = self.calculate_gradiometry_offsets(
|
|
201
|
-
self.vectors,
|
|
202
|
+
self.vectors,
|
|
203
|
+
self.epoch, # type: ignore[has-type]
|
|
204
|
+
self.magi_vectors,
|
|
205
|
+
self.magi_epoch,
|
|
206
|
+
self.config.quality_flag_threshold,
|
|
202
207
|
)
|
|
203
208
|
self.vectors = self.apply_gradiometry_offsets(
|
|
204
209
|
self.gradiometry_offsets, self.vectors, self.config.gradiometer_factor
|
|
@@ -207,6 +212,54 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
207
212
|
self.magnitude = MagL2L1dBase.calculate_magnitude(vectors=self.vectors)
|
|
208
213
|
self.is_l1d = True
|
|
209
214
|
|
|
215
|
+
def generate_dataset(
|
|
216
|
+
self,
|
|
217
|
+
attribute_manager: ImapCdfAttributes,
|
|
218
|
+
day: np.datetime64,
|
|
219
|
+
) -> xr.Dataset:
|
|
220
|
+
"""
|
|
221
|
+
Generate an xarray dataset from the dataclass.
|
|
222
|
+
|
|
223
|
+
This overrides the parent method to conditionally swap MAGO/MAGI data
|
|
224
|
+
based on the always_output_mago configuration setting.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
attribute_manager : ImapCdfAttributes
|
|
229
|
+
CDF attributes object for the correct level.
|
|
230
|
+
day : np.datetime64
|
|
231
|
+
The 24 hour day to process, as a numpy datetime format.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
xr.Dataset
|
|
236
|
+
Complete dataset ready to write to CDF file.
|
|
237
|
+
"""
|
|
238
|
+
always_output_mago = configuration.ALWAYS_OUTPUT_MAGO
|
|
239
|
+
|
|
240
|
+
if not always_output_mago:
|
|
241
|
+
# Swap vectors and epochs to use MAGI data instead of MAGO
|
|
242
|
+
original_vectors: np.ndarray = self.vectors.copy()
|
|
243
|
+
original_epoch: np.ndarray = self.epoch.copy() # type: ignore[has-type]
|
|
244
|
+
original_range: np.ndarray = self.range.copy() # type: ignore[has-type]
|
|
245
|
+
|
|
246
|
+
self.vectors = self.magi_vectors # type: ignore[no-redef]
|
|
247
|
+
self.epoch = self.magi_epoch # type: ignore[no-redef]
|
|
248
|
+
self.range = self.magi_range # type: ignore[no-redef]
|
|
249
|
+
|
|
250
|
+
# Call parent generate_dataset method
|
|
251
|
+
dataset = super().generate_dataset(attribute_manager, day)
|
|
252
|
+
|
|
253
|
+
# Restore original vectors for any further processing
|
|
254
|
+
self.vectors = original_vectors
|
|
255
|
+
self.epoch = original_epoch
|
|
256
|
+
self.range = original_range
|
|
257
|
+
else:
|
|
258
|
+
# Use MAGO data (default behavior)
|
|
259
|
+
dataset = super().generate_dataset(attribute_manager, day)
|
|
260
|
+
|
|
261
|
+
return dataset
|
|
262
|
+
|
|
210
263
|
def rotate_frame(self, end_frame: ValidFrames) -> None:
|
|
211
264
|
"""
|
|
212
265
|
Rotate the vectors to the desired frame.
|
|
@@ -312,7 +365,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
312
365
|
The offset vector, shape (4,) where the last element is unchanged.
|
|
313
366
|
"""
|
|
314
367
|
# Offsets are in shape (sensor, range, axis)
|
|
315
|
-
updated_vector = input_vector.copy()
|
|
368
|
+
updated_vector = input_vector.copy()
|
|
316
369
|
rng = int(input_vector[3])
|
|
317
370
|
x_y_z = input_vector[:3]
|
|
318
371
|
updated_vector[:3] = x_y_z - offsets[int(is_magi), rng, :]
|
|
@@ -348,64 +401,132 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
348
401
|
"Spin offsets can only be calculated in NORM mode and SRF frame."
|
|
349
402
|
)
|
|
350
403
|
|
|
351
|
-
|
|
352
|
-
sc_spin_phase
|
|
404
|
+
epoch_met = ttj2000ns_to_met(self.epoch)
|
|
405
|
+
sc_spin_phase = spin.get_spacecraft_spin_phase(epoch_met)
|
|
353
406
|
# mark vectors as nan where they are nan in sc_spin_phase
|
|
354
407
|
vectors = self.vectors.copy().astype(np.float64)
|
|
355
408
|
|
|
356
409
|
vectors[np.isnan(sc_spin_phase), :] = np.nan
|
|
357
410
|
|
|
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
411
|
# first timestamp where spin phase is less than the previous value
|
|
362
412
|
# this is when the spin crosses zero
|
|
363
413
|
spin_starts = np.where(np.diff(sc_spin_phase) < 0)[0] + 1
|
|
364
414
|
|
|
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
|
-
)
|
|
415
|
+
# if the value switches from nan to a number, or from a number to nan, that
|
|
416
|
+
# is also a spin start
|
|
417
|
+
nan_to_number = np.where(np.diff(np.isnan(sc_spin_phase)) != 0)[0] + 1
|
|
370
418
|
|
|
371
419
|
# find the places spins start while skipping over invalid or missing data
|
|
372
420
|
# (marked as nan by get_spacecraft_spin_phase)
|
|
373
421
|
spin_starts = np.sort(np.concatenate((spin_starts, nan_to_number)))
|
|
374
422
|
|
|
423
|
+
# Get the expected spin period from the spin table
|
|
424
|
+
# Convert to nanoseconds to match epoch
|
|
425
|
+
spin_data = spin.get_spin_data()
|
|
426
|
+
# Use the median spin period as the expected value
|
|
427
|
+
expected_spin = np.median(spin_data["spin_period_sec"]) * 1e9
|
|
428
|
+
|
|
429
|
+
paired_nans = nan_to_number.reshape(-1, 2)
|
|
430
|
+
|
|
431
|
+
for start_of_gap, end_of_gap in paired_nans:
|
|
432
|
+
# in nan_to_number, we have the start and end for every nan gap
|
|
433
|
+
# if this gap spans more than 1 spin period, we need to insert
|
|
434
|
+
# additional spin_starts into spin_starts.
|
|
435
|
+
|
|
436
|
+
gap_start_time = self.epoch[start_of_gap]
|
|
437
|
+
gap_end_time = self.epoch[end_of_gap]
|
|
438
|
+
|
|
439
|
+
# Calculate the number of spins in this gap
|
|
440
|
+
number_of_spins = int((gap_end_time - gap_start_time) // expected_spin)
|
|
441
|
+
if number_of_spins > 1:
|
|
442
|
+
# Insert new spin starts into spin_starts
|
|
443
|
+
for i in range(1, number_of_spins):
|
|
444
|
+
estimated_start = gap_start_time + i * expected_spin
|
|
445
|
+
new_spin_index = (np.abs(self.epoch - estimated_start)).argmin()
|
|
446
|
+
|
|
447
|
+
spin_starts = np.append(spin_starts, new_spin_index)
|
|
448
|
+
|
|
449
|
+
# Now spin_starts contains all the indices where spins begin, including
|
|
450
|
+
# estimating skipped or missing spins.
|
|
451
|
+
spin_starts = np.sort(spin_starts)
|
|
452
|
+
|
|
375
453
|
chunk_start = 0
|
|
376
454
|
offset_epochs = []
|
|
377
|
-
|
|
378
|
-
|
|
455
|
+
x_avg_calcs: list[np.float64] = []
|
|
456
|
+
y_avg_calcs: list[np.float64] = []
|
|
457
|
+
validity_start_times = []
|
|
458
|
+
validity_end_times = []
|
|
459
|
+
start_spin_counters = []
|
|
460
|
+
end_spin_counters = []
|
|
461
|
+
|
|
379
462
|
while chunk_start < len(spin_starts):
|
|
380
463
|
# Take self.spin_count_calibration number of spins and put them into a chunk
|
|
381
464
|
chunk_indices = spin_starts[
|
|
382
465
|
chunk_start : chunk_start + self.config.spin_count_calibration + 1
|
|
383
466
|
]
|
|
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))
|
|
467
|
+
chunk_start_idx = chunk_start
|
|
389
468
|
|
|
390
469
|
chunk_vectors = self.vectors[chunk_indices[0] : chunk_indices[-1]]
|
|
391
470
|
chunk_epoch = self.epoch[chunk_indices[0] : chunk_indices[-1]]
|
|
392
471
|
|
|
472
|
+
# Check if more than half of the chunk data is NaN before processing
|
|
473
|
+
x_valid_count: int = int(np.sum(~np.isnan(chunk_vectors[:, 0])))
|
|
474
|
+
y_valid_count: int = int(np.sum(~np.isnan(chunk_vectors[:, 1])))
|
|
475
|
+
total_points = len(chunk_vectors)
|
|
476
|
+
|
|
393
477
|
# average the x and y axes (z is fixed, as the spin axis)
|
|
394
|
-
# TODO: is z the correct axis here?
|
|
395
478
|
avg_x = np.nanmean(chunk_vectors[:, 0])
|
|
396
479
|
avg_y = np.nanmean(chunk_vectors[:, 1])
|
|
397
480
|
|
|
481
|
+
# Skip chunk if more than half of x or y data is NaN, or if we have less
|
|
482
|
+
# than half a spin.
|
|
483
|
+
# in this case, we should reuse the previous averages.
|
|
484
|
+
if (
|
|
485
|
+
x_valid_count <= total_points / 2
|
|
486
|
+
or y_valid_count <= total_points / 2
|
|
487
|
+
or total_points <= self.config.spin_count_calibration / 2
|
|
488
|
+
):
|
|
489
|
+
avg_x = x_avg_calcs[-1] if x_avg_calcs else np.float64(FILLVAL)
|
|
490
|
+
avg_y = y_avg_calcs[-1] if y_avg_calcs else np.float64(FILLVAL)
|
|
491
|
+
|
|
398
492
|
if not np.isnan(avg_x) and not np.isnan(avg_y):
|
|
399
493
|
offset_epochs.append(chunk_epoch[0])
|
|
400
|
-
|
|
401
|
-
|
|
494
|
+
x_avg_calcs.append(avg_x)
|
|
495
|
+
y_avg_calcs.append(avg_y)
|
|
496
|
+
|
|
497
|
+
# Add validity time range for this chunk
|
|
498
|
+
validity_start_times.append(chunk_epoch[0])
|
|
499
|
+
validity_end_times.append(chunk_epoch[-1])
|
|
500
|
+
|
|
501
|
+
# Add spin counter information
|
|
502
|
+
start_spin_counters.append(chunk_start_idx)
|
|
503
|
+
end_spin_counters.append(
|
|
504
|
+
min(
|
|
505
|
+
chunk_start_idx + self.config.spin_count_calibration - 1,
|
|
506
|
+
len(spin_starts) - 1,
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
chunk_start = chunk_start + self.config.spin_count_calibration
|
|
402
511
|
|
|
403
512
|
spin_epoch_dataarray = xr.DataArray(np.array(offset_epochs))
|
|
404
513
|
|
|
405
514
|
spin_offsets = xr.Dataset(coords={"epoch": spin_epoch_dataarray})
|
|
406
515
|
|
|
407
|
-
spin_offsets["x_offset"] = xr.DataArray(np.array(
|
|
408
|
-
spin_offsets["y_offset"] = xr.DataArray(np.array(
|
|
516
|
+
spin_offsets["x_offset"] = xr.DataArray(np.array(x_avg_calcs), dims=["epoch"])
|
|
517
|
+
spin_offsets["y_offset"] = xr.DataArray(np.array(y_avg_calcs), dims=["epoch"])
|
|
518
|
+
spin_offsets["validity_start_time"] = xr.DataArray(
|
|
519
|
+
np.array(validity_start_times), dims=["epoch"]
|
|
520
|
+
)
|
|
521
|
+
spin_offsets["validity_end_time"] = xr.DataArray(
|
|
522
|
+
np.array(validity_end_times), dims=["epoch"]
|
|
523
|
+
)
|
|
524
|
+
spin_offsets["start_spin_counter"] = xr.DataArray(
|
|
525
|
+
np.array(start_spin_counters), dims=["epoch"]
|
|
526
|
+
)
|
|
527
|
+
spin_offsets["end_spin_counter"] = xr.DataArray(
|
|
528
|
+
np.array(end_spin_counters), dims=["epoch"]
|
|
529
|
+
)
|
|
409
530
|
|
|
410
531
|
return spin_offsets
|
|
411
532
|
|
|
@@ -462,7 +583,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
462
583
|
if spin_offsets is None:
|
|
463
584
|
raise ValueError("No spin offsets calculated to apply.")
|
|
464
585
|
|
|
465
|
-
output_vectors = np.full(vectors.shape, FILLVAL, dtype=np.
|
|
586
|
+
output_vectors = np.full(vectors.shape, FILLVAL, dtype=np.float64)
|
|
466
587
|
|
|
467
588
|
for index in range(spin_offsets["epoch"].data.shape[0] - 1):
|
|
468
589
|
timestamp = spin_offsets["epoch"].data[index]
|
|
@@ -483,7 +604,6 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
483
604
|
if not np.any(mask):
|
|
484
605
|
continue
|
|
485
606
|
|
|
486
|
-
# TODO: should vectors be a float?
|
|
487
607
|
x_offset = (
|
|
488
608
|
spin_offsets["x_offset"].data[index] * spin_average_application_factor
|
|
489
609
|
)
|
|
@@ -504,6 +624,7 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
504
624
|
mago_epoch: np.ndarray,
|
|
505
625
|
magi_vectors: np.ndarray,
|
|
506
626
|
magi_epoch: np.ndarray,
|
|
627
|
+
quality_flag_threshold: float = np.inf,
|
|
507
628
|
) -> xr.Dataset:
|
|
508
629
|
"""
|
|
509
630
|
Calculate the gradiometry offsets between MAGo and MAGi.
|
|
@@ -525,6 +646,10 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
525
646
|
The MAGi vectors, shape (N, 3).
|
|
526
647
|
magi_epoch : np.ndarray
|
|
527
648
|
The MAGi epoch values, shape (N,).
|
|
649
|
+
quality_flag_threshold : np.float64, optional
|
|
650
|
+
Threshold for quality flags. If the magnitude of gradiometer offset
|
|
651
|
+
exceeds this threshold, quality flag will be set. Default is np.inf
|
|
652
|
+
(no quality flags set).
|
|
528
653
|
|
|
529
654
|
Returns
|
|
530
655
|
-------
|
|
@@ -532,6 +657,8 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
532
657
|
The gradiometer offsets dataset, with variables:
|
|
533
658
|
- epoch: the timestamp of the MAGo data
|
|
534
659
|
- gradiometer_offsets: the offset values (MAGi - MAGo) for each axis
|
|
660
|
+
- gradiometer_offset_magnitude: magnitude of the offset vector
|
|
661
|
+
- quality_flags: quality flags (1 if magnitude > threshold, 0 otherwise)
|
|
535
662
|
"""
|
|
536
663
|
aligned_magi = linear(
|
|
537
664
|
magi_vectors,
|
|
@@ -541,10 +668,20 @@ class MagL1d(MagL2L1dBase): # type: ignore[misc]
|
|
|
541
668
|
|
|
542
669
|
diff = aligned_magi - mago_vectors
|
|
543
670
|
|
|
671
|
+
# Calculate magnitude of gradiometer offset for each vector
|
|
672
|
+
magnitude = np.linalg.norm(diff, axis=1)
|
|
673
|
+
|
|
674
|
+
# Set quality flags: 0 = good data (below threshold), 1 = bad data
|
|
675
|
+
quality_flags = (magnitude > quality_flag_threshold).astype(int)
|
|
676
|
+
|
|
544
677
|
grad_epoch = xr.DataArray(mago_epoch, dims=["epoch"])
|
|
545
678
|
direction = xr.DataArray(["x", "y", "z"], dims=["axis"])
|
|
546
679
|
grad_ds = xr.Dataset(coords={"epoch": grad_epoch, "direction": direction})
|
|
547
680
|
grad_ds["gradiometer_offsets"] = xr.DataArray(diff, dims=["epoch", "direction"])
|
|
681
|
+
grad_ds["gradiometer_offset_magnitude"] = xr.DataArray(
|
|
682
|
+
magnitude, dims=["epoch"]
|
|
683
|
+
)
|
|
684
|
+
grad_ds["quality_flags"] = xr.DataArray(quality_flags, dims=["epoch"])
|
|
548
685
|
|
|
549
686
|
return grad_ds
|
|
550
687
|
|
|
@@ -13,6 +13,7 @@ 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
|
|
|
@@ -22,7 +23,8 @@ class ValidFrames(Enum):
|
|
|
22
23
|
MAG = SpiceFrame.IMAP_MAG
|
|
23
24
|
DSRF = SpiceFrame.IMAP_DPS
|
|
24
25
|
SRF = SpiceFrame.IMAP_SPACECRAFT
|
|
25
|
-
|
|
26
|
+
GSE = SpiceFrame.IMAP_GSE
|
|
27
|
+
RTN = SpiceFrame.IMAP_RTN
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@dataclass(kw_only=True)
|
|
@@ -55,6 +57,9 @@ class MagL2L1dBase:
|
|
|
55
57
|
file in L2, marked as good always in L1D.
|
|
56
58
|
frame:
|
|
57
59
|
The reference frame of the input vectors. Starts as the MAG instrument frame.
|
|
60
|
+
epoch_et: np.ndarray
|
|
61
|
+
The epoch timestamps converted to ET format. Used for frame transformations.
|
|
62
|
+
Calculated on first use and then saved. Should not be passed in.
|
|
58
63
|
"""
|
|
59
64
|
|
|
60
65
|
vectors: np.ndarray
|
|
@@ -66,6 +71,7 @@ class MagL2L1dBase:
|
|
|
66
71
|
data_mode: DataMode
|
|
67
72
|
magnitude: np.ndarray = field(init=False)
|
|
68
73
|
frame: ValidFrames = ValidFrames.MAG
|
|
74
|
+
epoch_et: np.ndarray | None = field(init=False, default=None)
|
|
69
75
|
|
|
70
76
|
def generate_dataset(
|
|
71
77
|
self,
|
|
@@ -301,8 +307,10 @@ class MagL2L1dBase:
|
|
|
301
307
|
The frame to rotate the data to. Must be one of the ValidFrames enum
|
|
302
308
|
values.
|
|
303
309
|
"""
|
|
310
|
+
if self.epoch_et is None:
|
|
311
|
+
self.epoch_et = ttj2000ns_to_et(self.epoch)
|
|
304
312
|
self.vectors = frame_transform(
|
|
305
|
-
self.
|
|
313
|
+
self.epoch_et,
|
|
306
314
|
self.vectors,
|
|
307
315
|
from_frame=self.frame.value,
|
|
308
316
|
to_frame=end_frame.value,
|
imap_processing/quality_flags.py
CHANGED
|
@@ -37,7 +37,7 @@ 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
|
|
@@ -75,6 +75,14 @@ class ImapRatesUltraFlags(FlagNameMixin):
|
|
|
75
75
|
PARTIALSPIN = 2**2 # bit 2
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
class ImapDEScatteringUltraFlags(FlagNameMixin):
|
|
79
|
+
"""IMAP Ultra Scattering flags."""
|
|
80
|
+
|
|
81
|
+
NONE = CommonFlags.NONE
|
|
82
|
+
ABOVE_THRESHOLD = 2**0 # bit 0
|
|
83
|
+
NAN_PHI_OR_THETA = 2**1 # bit 1
|
|
84
|
+
|
|
85
|
+
|
|
78
86
|
class ImapInstrumentUltraFlags(FlagNameMixin):
|
|
79
87
|
"""IMAP Ultra flags using other instruments."""
|
|
80
88
|
|