Skip to content

Commit e2b9a69

Browse files
authored
Mag L2 rotation - propogate FILLVALS (#2903)
* Revert "A rotated NaN is still a NaN (#2901)" This reverts commit cdfd0ae. * Apply fix to propogate FILLVALs out from SPICE rotations
1 parent 00039d0 commit e2b9a69

5 files changed

Lines changed: 133 additions & 6 deletions

File tree

imap_processing/mag/l1d/mag_l1d_data.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,26 +298,40 @@ def rotate_frame(self, end_frame: ValidFrames) -> None:
298298
self.epoch_et: np.ndarray = ttj2000ns_to_et(self.epoch)
299299
self.magi_epoch_et: np.ndarray = ttj2000ns_to_et(self.magi_epoch)
300300

301-
self.vectors = frame_transform(
301+
new_vectors = frame_transform(
302302
self.epoch_et,
303303
self.vectors,
304304
from_frame=start_frame.spice_frame,
305305
to_frame=end_frame.spice_frame,
306306
allow_spice_noframeconnect=True,
307307
)
308+
if np.isnan(self.vectors).any() or (self.vectors == FILLVAL).any():
309+
new_vectors = np.where(
310+
np.isnan(self.vectors) | (self.vectors == FILLVAL),
311+
FILLVAL,
312+
new_vectors,
313+
)
314+
self.vectors = new_vectors
308315

309316
# If we were in MAGO frame, we need to rotate MAGI vectors from MAGI to
310317
# end_frame
311318
if start_frame == ValidFrames.MAGO:
312319
start_frame = ValidFrames.MAGI
313320

314-
self.magi_vectors = frame_transform(
321+
new_magi_vectors = frame_transform(
315322
self.magi_epoch_et,
316323
self.magi_vectors,
317324
from_frame=start_frame.spice_frame,
318325
to_frame=end_frame.spice_frame,
319326
allow_spice_noframeconnect=True,
320327
)
328+
if np.isnan(self.magi_vectors).any() or (self.magi_vectors == FILLVAL).any():
329+
new_magi_vectors = np.where(
330+
np.isnan(self.magi_vectors) | (self.magi_vectors == FILLVAL),
331+
FILLVAL,
332+
new_magi_vectors,
333+
)
334+
self.magi_vectors = new_magi_vectors
321335

322336
self.frame = end_frame
323337

imap_processing/mag/l2/mag_l2_data.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import xarray as xr
88

99
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
10+
from imap_processing.mag import constants
1011
from imap_processing.mag.constants import FILLVAL, DataMode
1112
from imap_processing.mag.l1b.mag_l1b import calibrate_vector
1213
from imap_processing.spice.geometry import SpiceFrame, frame_transform
@@ -417,13 +418,21 @@ def rotate_frame(self, end_frame: ValidFrames) -> None:
417418
"""
418419
if self.epoch_et is None:
419420
self.epoch_et = ttj2000ns_to_et(self.epoch)
420-
self.vectors = frame_transform(
421+
new_vectors = frame_transform(
421422
self.epoch_et,
422423
self.vectors,
423424
from_frame=self.frame.spice_frame,
424425
to_frame=end_frame.spice_frame,
425426
allow_spice_noframeconnect=True,
426427
)
428+
if np.isnan(self.vectors).any() or (self.vectors == constants.FILLVAL).any():
429+
new_vectors = np.where(
430+
np.isnan(self.vectors) | (self.vectors == constants.FILLVAL),
431+
constants.FILLVAL,
432+
new_vectors,
433+
)
434+
435+
self.vectors = new_vectors
427436
self.frame = end_frame
428437

429438

imap_processing/tests/mag/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,11 @@ def norm_dataset(mag_test_l2_data):
175175
dataset.attrs["vectors_per_second"] = vectors_per_second_attr
176176
dataset["epoch"] = epoch_vals
177177
dataset.attrs["Logical_source"] = "imap_mag_l1c_norm-mago"
178-
vectors = np.array([[i, i, i, 2] for i in range(1, 3505)])
178+
# Actual dataset is a CDF_FLOAT which is a float32.
179+
vectors = np.array(
180+
[[i, i, i, 2] for i in range(1, 3505)],
181+
dtype=np.float64,
182+
)
179183
dataset["vectors"].data = vectors
180184

181185
return dataset

imap_processing/tests/mag/test_mag_l1d.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
99
from imap_processing.cdf.utils import write_cdf
1010
from imap_processing.cli import Mag
11-
from imap_processing.mag.constants import DataMode
11+
from imap_processing.mag.constants import FILLVAL, DataMode
1212
from imap_processing.mag.l1d.mag_l1d import mag_l1d
1313
from imap_processing.mag.l1d.mag_l1d_data import MagL1d, MagL1dConfiguration
1414
from imap_processing.mag.l2.mag_l2_data import ValidFrames
@@ -571,6 +571,57 @@ def test_enhanced_gradiometry_with_quality_flags_detailed():
571571
assert np.array_equal(grad_ds["quality_flags"].data, expected_flags)
572572

573573

574+
def test_rotate_frame_preserves_fillval_and_nan(mag_l1d_test_class):
575+
"""Test that L1D rotate_frame preserves FILLVAL and NaN vectors."""
576+
mag_l1d_test_class.frame = ValidFrames.MAGO
577+
578+
vectors = mag_l1d_test_class.vectors.copy()
579+
magi_vectors = mag_l1d_test_class.magi_vectors.copy()
580+
581+
# Set some MAGO vectors to FILLVAL and NaN
582+
vectors[0] = [FILLVAL, FILLVAL, FILLVAL]
583+
vectors[2] = [np.nan, np.nan, np.nan]
584+
vectors[4] = [1.0, np.nan, 3.0]
585+
mag_l1d_test_class.vectors = vectors
586+
587+
# Set some MAGI vectors to FILLVAL and NaN
588+
magi_vectors[1] = [FILLVAL, FILLVAL, FILLVAL]
589+
magi_vectors[3] = [np.nan, np.nan, np.nan]
590+
mag_l1d_test_class.magi_vectors = magi_vectors
591+
592+
def mock_frame_transform(
593+
epoch_et,
594+
vecs,
595+
from_frame,
596+
to_frame,
597+
allow_spice_noframeconnect,
598+
):
599+
return np.full(vecs.shape, 99.0)
600+
601+
with patch(
602+
"imap_processing.mag.l1d.mag_l1d_data.frame_transform",
603+
side_effect=mock_frame_transform,
604+
):
605+
mag_l1d_test_class.rotate_frame(ValidFrames.SRF)
606+
607+
assert mag_l1d_test_class.frame == ValidFrames.SRF
608+
609+
# MAGO: FILLVAL/NaN rows preserved as FILLVAL
610+
assert np.all(mag_l1d_test_class.vectors[0] == FILLVAL)
611+
assert np.all(mag_l1d_test_class.vectors[2] == FILLVAL)
612+
assert mag_l1d_test_class.vectors[4, 1] == FILLVAL
613+
# Normal MAGO vectors get rotated value
614+
assert np.all(mag_l1d_test_class.vectors[1] == 99.0)
615+
assert np.all(mag_l1d_test_class.vectors[3] == 99.0)
616+
617+
# MAGI: FILLVAL/NaN rows preserved as FILLVAL
618+
assert np.all(mag_l1d_test_class.magi_vectors[1] == FILLVAL)
619+
assert np.all(mag_l1d_test_class.magi_vectors[3] == FILLVAL)
620+
# Normal MAGI vectors get rotated value
621+
assert np.all(mag_l1d_test_class.magi_vectors[0] == 99.0)
622+
assert np.all(mag_l1d_test_class.magi_vectors[2] == 99.0)
623+
624+
574625
def test_rotate_frames(mag_l1d_test_class):
575626
# Reset to initial MAGO frame for this test
576627
mag_l1d_test_class.frame = ValidFrames.MAGO

imap_processing/tests/mag/test_mag_l2.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import xarray as xr
66

77
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
8-
from imap_processing.mag.constants import DataMode
8+
from imap_processing.mag.constants import FILLVAL, DataMode
99
from imap_processing.mag.l2.mag_l2 import mag_l2, retrieve_matrix_from_l2_calibration
1010
from imap_processing.mag.l2.mag_l2_data import MagL2, ValidFrames
1111
from imap_processing.spice.time import (
@@ -447,6 +447,55 @@ def test_spice_returns(norm_dataset):
447447
assert np.array_equal(l2.vectors[0], [-1, -1, -1])
448448

449449

450+
def test_rotate_frame_preserves_fillval_and_nan(norm_dataset):
451+
"""Test that rotate_frame preserves FILLVAL and NaN vectors."""
452+
453+
vectors = norm_dataset["vectors"].data[:, :3].copy()
454+
n = len(vectors)
455+
456+
# Set some vectors to FILLVAL and NaN
457+
vectors[0] = [FILLVAL, FILLVAL, FILLVAL]
458+
vectors[2] = [np.nan, np.nan, np.nan]
459+
# Partial NaN in a row
460+
vectors[4] = [1.0, np.nan, 3.0]
461+
# Partial FILLVAL in a row
462+
vectors[5] = [FILLVAL, 2.0, 3.0]
463+
464+
l2 = MagL2(
465+
vectors=vectors,
466+
epoch=norm_dataset["epoch"].data,
467+
range=norm_dataset["vectors"].data[:, 3],
468+
global_attributes={},
469+
quality_flags=np.zeros(n),
470+
quality_bitmask=np.zeros(n),
471+
data_mode=DataMode.NORM,
472+
offsets=np.zeros((n, 3)),
473+
timedelta=np.zeros(n),
474+
)
475+
476+
rotated_values = np.full(vectors.shape, 99.0)
477+
with patch(
478+
"imap_processing.mag.l2.mag_l2_data.frame_transform",
479+
return_value=rotated_values,
480+
):
481+
l2.rotate_frame(ValidFrames.DSRF)
482+
483+
assert l2.frame == ValidFrames.DSRF
484+
485+
# Full FILLVAL row -> all components should be FILLVAL
486+
assert np.all(l2.vectors[0] == FILLVAL)
487+
# Full NaN row -> all components should be FILLVAL
488+
assert np.all(l2.vectors[2] == FILLVAL)
489+
# Partial NaN -> affected components should be FILLVAL
490+
assert l2.vectors[4, 1] == FILLVAL
491+
# Partial FILLVAL -> affected components should be FILLVAL
492+
assert l2.vectors[5, 0] == FILLVAL
493+
494+
# Normal vectors should get the rotated value
495+
assert np.all(l2.vectors[1] == 99.0)
496+
assert np.all(l2.vectors[3] == 99.0)
497+
498+
450499
def test_qf(norm_dataset):
451500
qf = np.zeros(len(norm_dataset["epoch"].data), dtype=int)
452501
qf[1:4] = 1

0 commit comments

Comments
 (0)