Skip to content

Commit 1494922

Browse files
authored
* L2 first pass * Setting up empty mag L2
1 parent f4e6f63 commit 1494922

10 files changed

Lines changed: 369 additions & 8 deletions

File tree

imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,18 @@ imap_mag_l1c_norm-magi:
8383
Data_type: L1C_norm-magi>Level 1C MAGi normal rate
8484
Logical_source: imap_mag_l1c_norm-magi
8585
Logical_source_description: IMAP Mission MAGi Normal Rate Instrument Level-1C Data.
86+
87+
imap_mag_l2_norm-dsrf:
88+
<<: *default
89+
Data_type: L2_norm-dsrf>Level 2 normal rate data in DSRF
90+
Logical_source: imap_mag_l2_norm-dsrf
91+
Logical_source_description: IMAP Mission Normal Rate Instrument Level-2 Data in
92+
Despun Spacecraft Reference Frame.
93+
94+
95+
imap_mag_l2_burst-dsrf:
96+
<<: *default
97+
Data_type: L2_burst-dsrf>Level 2 burst rate data in DSRF
98+
Logical_source: imap_mag_l2_burst-dsrf
99+
Logical_source_description: IMAP Mission Burst Rate Instrument Level-2 Data in
100+
Despun Spacecraft Reference Frame.

imap_processing/cli.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from imap_processing.mag.l1a.mag_l1a import mag_l1a
5656
from imap_processing.mag.l1b.mag_l1b import mag_l1b
5757
from imap_processing.mag.l1c.mag_l1c import mag_l1c
58+
from imap_processing.mag.l2.mag_l2 import mag_l2
5859
from imap_processing.swapi.l1.swapi_l1 import swapi_l1
5960
from imap_processing.swapi.l2.swapi_l2 import swapi_l2
6061
from imap_processing.swe.l1a.swe_l1a import swe_l1a
@@ -830,6 +831,24 @@ def do_processing(
830831
# Input datasets can be in any order
831832
datasets = [mag_l1c(input_data[0], input_data[1], self.version)]
832833

834+
if self.data_level == "l2":
835+
# TODO: Overwrite dependencies with versions from offsets file
836+
input_data = load_cdf(dependencies[0])
837+
# TODO: use ancillary from input
838+
calibration_dataset = load_cdf(
839+
Path(__file__).parent
840+
/ "tests"
841+
/ "mag"
842+
/ "validation"
843+
/ "calibration"
844+
/ "imap_mag_l1b-calibration_20240229_v001.cdf"
845+
)
846+
# TODO: Test data missing
847+
offset_dataset = xr.Dataset()
848+
datasets = [
849+
mag_l2(calibration_dataset, offset_dataset, input_data, self.version)
850+
]
851+
833852
return datasets
834853

835854

imap_processing/mag/l1a/mag_l1a.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,6 @@ def generate_dataset(
297297
),
298298
)
299299

300-
# TODO: Epoch here refers to the start of the sample. Confirm that this is
301-
# what mag is expecting, and if it is, CATDESC needs to be updated.
302300
epoch_time = xr.DataArray(
303301
time_data,
304302
name="epoch",
@@ -338,7 +336,6 @@ def generate_dataset(
338336
),
339337
)
340338
global_attributes = attribute_manager.get_global_attributes(logical_file_id)
341-
# TODO: this method won't work because these values are not in the schema.
342339
global_attributes["is_mago"] = str(bool(single_file_l1a.is_mago))
343340
global_attributes["is_active"] = str(bool(single_file_l1a.is_active))
344341
global_attributes["vectors_per_second"] = (

imap_processing/mag/l1b/__init__.py

Whitespace-only changes.

imap_processing/mag/l2/mag_l2.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Module to run MAG L2 processing."""
2+
3+
import numpy as np
4+
import xarray as xr
5+
6+
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
7+
from imap_processing.mag.constants import DataMode
8+
from imap_processing.mag.l2.mag_l2_data import MagL2
9+
10+
11+
def mag_l2(
12+
calibration_dataset: xr.Dataset,
13+
offset_dataset: xr.Dataset,
14+
input_data: xr.Dataset,
15+
data_version: str,
16+
) -> list[xr.Dataset]:
17+
"""
18+
Complete MAG L2 processing.
19+
20+
Input data can be burst or normal mode, but MUST match the file in offset_dataset.
21+
TODO: retrieve the file from offset_dataset in cli.py.
22+
23+
Parameters
24+
----------
25+
calibration_dataset : xr.Dataset
26+
Calibration ancillary file input.
27+
offset_dataset : xr.Dataset
28+
Offset ancillary file input.
29+
input_data : xr.Dataset
30+
Input data from MAG L1C or L1B.
31+
data_version : str
32+
Version of output file.
33+
34+
Returns
35+
-------
36+
list[xr.Dataset]
37+
List of xarray datasets ready to write to CDF file. Expected to be four outputs
38+
for different frames.
39+
"""
40+
# TODO we may need to combine multiple calibration datasets into one timeline.
41+
42+
basic_test_data = MagL2(
43+
input_data["vectors"].data[:, :3], # level 2 vectors don't include range
44+
input_data["epoch"].data,
45+
input_data["vectors"].data[:, 3],
46+
{"Data_version": data_version},
47+
np.zeros(len(input_data["epoch"].data)),
48+
np.zeros(len(input_data["epoch"].data)),
49+
DataMode.NORM,
50+
)
51+
attributes = ImapCdfAttributes()
52+
attributes.add_instrument_global_attrs("mag")
53+
# temporarily point to l1c
54+
attributes.add_instrument_variable_attrs("mag", "l1c")
55+
56+
return [basic_test_data.generate_dataset(attributes)]
57+
58+
59+
def apply_calibration_matrix(
60+
calibration_dataset: xr.Dataset, vectors: np.ndarray
61+
) -> np.ndarray:
62+
"""
63+
Apply the calibration file to the vectors to rotate them in space.
64+
65+
Parameters
66+
----------
67+
calibration_dataset : xr.Dataset
68+
Ancillary file input for calibration.
69+
vectors : np.ndarray
70+
(n, 4) array of vectors to rotate and timeshift.
71+
72+
Returns
73+
-------
74+
np.ndarray
75+
Rotated and timeshifted vectors.
76+
"""
77+
raise NotImplementedError
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Data structures for MAG L2 and L1D processing."""
2+
3+
from dataclasses import dataclass, field
4+
from enum import Enum
5+
6+
import numpy as np
7+
import xarray as xr
8+
9+
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
10+
from imap_processing.mag.constants import DataMode
11+
12+
13+
class ValidFrames(Enum):
14+
"""SPICE reference frames for output."""
15+
16+
dsrf = "dsrf"
17+
srf = "srf"
18+
rtn = "rtn"
19+
gse = "gse"
20+
21+
22+
@dataclass
23+
class MagL2:
24+
"""
25+
Dataclass for MAG L2 data.
26+
27+
Since L2 and L1D should have the same structure, this can be used for either level.
28+
29+
Attributes
30+
----------
31+
vectors: np.ndarray
32+
Magnetic field vectors of size (n, 3) where n is the number of vectors.
33+
Describes (x, y, z) components of the magnetic field.
34+
epoch: np.ndarray
35+
Time of each vector in J2000 seconds. Should be of length n.
36+
range: np.ndarray
37+
Range of each vector. Should be of length n.
38+
global_attributes: dict
39+
Any global attributes we want to carry forward into the output CDF file.
40+
quality_flags: np.ndarray
41+
Quality flags for each vector. Should be of length n.
42+
quality_bitmask: np.ndarray
43+
Quality bitmask for each vector. Should be of length n. Copied from offset
44+
file in L2, marked as good always in L1D.
45+
magnitude: np.ndarray
46+
Magnitude of each vector. Should be of length n. Calculated from L2 vectors.
47+
is_l1d: bool
48+
Flag to indicate if the data is L1D. Defaults to False.
49+
"""
50+
51+
vectors: np.ndarray
52+
epoch: np.ndarray
53+
range: np.ndarray
54+
global_attributes: dict
55+
quality_flags: np.ndarray
56+
quality_bitmask: np.ndarray
57+
data_mode: DataMode
58+
magnitude: np.ndarray = field(init=False)
59+
is_l1d: bool = False
60+
61+
def __post_init__(self) -> None:
62+
"""Calculate the magnitude of the vectors after initialization."""
63+
self.magnitude = self.calculate_magnitude(self.vectors)
64+
65+
@staticmethod
66+
def calculate_magnitude(
67+
vectors: np.ndarray,
68+
) -> np.ndarray:
69+
"""
70+
Given a list of vectors (x, y, z), calculate the magnitude of each vector.
71+
72+
For an input list of vectors of size (n, 3) returns a list of magnitudes of
73+
size (n,).
74+
75+
Parameters
76+
----------
77+
vectors : np.ndarray
78+
Array of vectors to calculate the magnitude of.
79+
80+
Returns
81+
-------
82+
np.ndarray
83+
Array of magnitudes of the input vectors.
84+
"""
85+
return np.zeros(vectors.shape[0]) # type: ignore
86+
87+
def truncate_to_24h(self, timestamp: str) -> None:
88+
"""
89+
Truncate all data to a 24 hour period.
90+
91+
24 hours is given by timestamp in the format YYYYmmdd.
92+
93+
Parameters
94+
----------
95+
timestamp : str
96+
Timestamp in the format YYYYMMDD.
97+
"""
98+
pass
99+
100+
def generate_dataset(
101+
self,
102+
attribute_manager: ImapCdfAttributes,
103+
frame: ValidFrames = ValidFrames.dsrf,
104+
) -> xr.Dataset:
105+
"""
106+
Generate an xarray dataset from the dataclass.
107+
108+
This method can be used for L2 and L1D, since they have extremely similar
109+
output.
110+
111+
Parameters
112+
----------
113+
attribute_manager : ImapCdfAttributes
114+
CDF attributes object for the correct level.
115+
frame : ValidFrames
116+
SPICE reference frame to rotate the data into.
117+
118+
Returns
119+
-------
120+
xr.Dataset
121+
Complete dataset ready to write to CDF file.
122+
"""
123+
logical_source_id = f"imap_mag_l2_{self.data_mode.value.lower()}-{frame.name}"
124+
direction = xr.DataArray(
125+
np.arange(3),
126+
name="direction",
127+
dims=["direction"],
128+
attrs=attribute_manager.get_variable_attributes(
129+
"direction_attrs", check_schema=False
130+
),
131+
)
132+
133+
direction_label = xr.DataArray(
134+
direction.values.astype(str),
135+
name="direction_label",
136+
dims=["direction_label"],
137+
attrs=attribute_manager.get_variable_attributes(
138+
"direction_label", check_schema=False
139+
),
140+
)
141+
142+
epoch_time = xr.DataArray(
143+
self.epoch,
144+
name="epoch",
145+
dims=["epoch"],
146+
attrs=attribute_manager.get_variable_attributes("epoch"),
147+
)
148+
149+
vectors = xr.DataArray(
150+
self.vectors,
151+
name="vectors",
152+
dims=["epoch", "direction"],
153+
attrs=attribute_manager.get_variable_attributes("vector_attrs"),
154+
)
155+
156+
quality_flags = xr.DataArray(
157+
self.quality_flags,
158+
name="quality_flags",
159+
dims=["epoch"],
160+
attrs=attribute_manager.get_variable_attributes("compression"),
161+
)
162+
163+
quality_bitmask = xr.DataArray(
164+
self.quality_flags,
165+
name="quality_flags",
166+
dims=["epoch"],
167+
attrs=attribute_manager.get_variable_attributes("compression"),
168+
)
169+
170+
rng = xr.DataArray(
171+
self.range,
172+
name="range",
173+
dims=["epoch"],
174+
# TODO temp attrs
175+
attrs=attribute_manager.get_variable_attributes("compression_width"),
176+
)
177+
178+
magnitude = xr.DataArray(
179+
self.magnitude,
180+
name="magnitude",
181+
dims=["epoch"],
182+
attrs=attribute_manager.get_variable_attributes("compression_width"),
183+
)
184+
185+
global_attributes = (
186+
attribute_manager.get_global_attributes(logical_source_id)
187+
| self.global_attributes
188+
)
189+
190+
output = xr.Dataset(
191+
coords={
192+
"epoch": epoch_time,
193+
"direction": direction,
194+
"direction_label": direction_label,
195+
},
196+
attrs=global_attributes,
197+
)
198+
199+
output["vectors"] = vectors
200+
output["quality_flags"] = quality_flags
201+
output["quality_bitmask"] = quality_bitmask
202+
output["range"] = rng
203+
output["magnitude"] = magnitude
204+
205+
return output

imap_processing/tests/mag/conftest.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,14 @@ def mag_l1a_dataset_generator(length):
5858

5959

6060
@pytest.fixture()
61-
def mag_test_calibration_data():
61+
def mag_test_l1b_calibration_data():
6262
imap_dir = Path(__file__).parent
63-
cal_file = imap_dir / "validation" / "imap_calibration_mag_20240229_v01.cdf"
63+
cal_file = (
64+
imap_dir
65+
/ "validation"
66+
/ "calibration"
67+
/ "imap_mag_l1b-calibration_20240229_v001.cdf"
68+
)
6469
calibration_data = load_cdf(cal_file)
6570
return calibration_data
6671

imap_processing/tests/mag/test_mag_l1b.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818

1919

20-
def test_mag_processing(mag_test_calibration_data):
20+
def test_mag_processing(mag_test_l1b_calibration_data):
2121
# All specific test values come from MAG team to accommodate various cases.
2222
# Each vector is multiplied by the matrix in the calibration data for the given
2323
# range to get the calibrated vector.
@@ -31,7 +31,7 @@ def test_mag_processing(mag_test_calibration_data):
3131
mag_attributes.add_instrument_variable_attrs("mag", "l1b")
3232
mag_l1b = mag_l1b_processing(
3333
mag_l1a_dataset,
34-
mag_test_calibration_data,
34+
mag_test_l1b_calibration_data,
3535
mag_attributes,
3636
"imap_mag_l1b_norm-mago",
3737
)
@@ -50,7 +50,7 @@ def test_mag_processing(mag_test_calibration_data):
5050

5151
mag_l1b = mag_l1b_processing(
5252
mag_l1a_dataset,
53-
mag_test_calibration_data,
53+
mag_test_l1b_calibration_data,
5454
mag_attributes,
5555
"imap_mag_l1b_norm-magi",
5656
)

0 commit comments

Comments
 (0)