From 0bc308c8e52bb695014b9743b8cc45de5ad6e6e2 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Wed, 1 Apr 2026 16:08:45 +0100 Subject: [PATCH 1/2] fix: Rotated NaN is still a NaN Fix rotations outside of spice so they maintan FILL_VAL during rotation Before this we were introducing nearly but not quite NaN/FILL_VAL meaning very bad field data. --- imap_processing/spice/geometry.py | 6 ++++++ imap_processing/tests/spice/test_geometry.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/imap_processing/spice/geometry.py b/imap_processing/spice/geometry.py index 132560932..3654b3d49 100644 --- a/imap_processing/spice/geometry.py +++ b/imap_processing/spice/geometry.py @@ -18,6 +18,8 @@ import spiceypy from numpy.typing import NDArray +from imap_processing.mag import constants + logger = logging.getLogger(__name__) @@ -317,6 +319,10 @@ def frame_transform( # Multiple et/positions : (n, 3, 3),(n, 3, 1) -> (n, 3, 1) result = np.squeeze(rotate @ position[..., np.newaxis]) + # For every FILLVAL in the input position, ensure the output is also NaN or FILLVAL + if np.isnan(position).any() or (position == constants.FILLVAL).any(): + result = np.where(np.isnan(position) | (position == constants.FILLVAL), constants.FILLVAL, result) + return result diff --git a/imap_processing/tests/spice/test_geometry.py b/imap_processing/tests/spice/test_geometry.py index 8e0465ba0..af3a4fd70 100644 --- a/imap_processing/tests/spice/test_geometry.py +++ b/imap_processing/tests/spice/test_geometry.py @@ -2,6 +2,7 @@ from unittest import mock +from imap_processing.mag import constants import numpy as np import pytest import spiceypy @@ -159,6 +160,19 @@ def test_get_spacecraft_to_instrument_spin_phase_offset( SpiceFrame.IMAP_SPACECRAFT, SpiceFrame.IMAP_DPS, ), + # single et, single NaN/FILL_VAL vector + ( + ["2025-04-30T12:00:00.000"], + np.array( + [ + [0, 0, 0], + [constants.FILLVAL, constants.FILLVAL, constants.FILLVAL], + [np.nan, np.nan, np.nan] + ] + ), + SpiceFrame.IMAP_SPACECRAFT, + SpiceFrame.IMAP_DPS, + ), ], ) def test_frame_transform(et_strings, position, from_frame, to_frame, furnish_kernels): @@ -203,6 +217,12 @@ def test_frame_transform(et_strings, position, from_frame, to_frame, furnish_ker spice_result = spiceypy.mxv(rotation_matrix, spice_position) np.testing.assert_allclose(test_result, spice_result, atol=1e-12) + # Ensure that NaN/FILL_VAL inputs are preserved exactly as FILL_VAL outputs + # and not just really close to but not quite FILL_VAL + for input_vec, output_vec in zip(position, result): + if np.isnan(input_vec).all() or (input_vec == constants.FILLVAL).all(): + assert (output_vec == constants.FILLVAL).all() + @pytest.mark.parametrize( "spice_frame", From 276c2f038bdf13dbaa082ce239db5e0953e1f658 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Wed, 1 Apr 2026 16:16:00 +0100 Subject: [PATCH 2/2] QA fix --- imap_processing/spice/geometry.py | 6 +++++- imap_processing/tests/spice/test_geometry.py | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/imap_processing/spice/geometry.py b/imap_processing/spice/geometry.py index 3654b3d49..1d7b11988 100644 --- a/imap_processing/spice/geometry.py +++ b/imap_processing/spice/geometry.py @@ -321,7 +321,11 @@ def frame_transform( # For every FILLVAL in the input position, ensure the output is also NaN or FILLVAL if np.isnan(position).any() or (position == constants.FILLVAL).any(): - result = np.where(np.isnan(position) | (position == constants.FILLVAL), constants.FILLVAL, result) + result = np.where( + np.isnan(position) | (position == constants.FILLVAL), + constants.FILLVAL, + result, + ) return result diff --git a/imap_processing/tests/spice/test_geometry.py b/imap_processing/tests/spice/test_geometry.py index af3a4fd70..19774e198 100644 --- a/imap_processing/tests/spice/test_geometry.py +++ b/imap_processing/tests/spice/test_geometry.py @@ -2,11 +2,11 @@ from unittest import mock -from imap_processing.mag import constants import numpy as np import pytest import spiceypy +from imap_processing.mag import constants from imap_processing.spice.geometry import ( SpiceBody, SpiceFrame, @@ -167,9 +167,9 @@ def test_get_spacecraft_to_instrument_spin_phase_offset( [ [0, 0, 0], [constants.FILLVAL, constants.FILLVAL, constants.FILLVAL], - [np.nan, np.nan, np.nan] + [np.nan, np.nan, np.nan], ] - ), + ), SpiceFrame.IMAP_SPACECRAFT, SpiceFrame.IMAP_DPS, ), @@ -219,7 +219,7 @@ def test_frame_transform(et_strings, position, from_frame, to_frame, furnish_ker # Ensure that NaN/FILL_VAL inputs are preserved exactly as FILL_VAL outputs # and not just really close to but not quite FILL_VAL - for input_vec, output_vec in zip(position, result): + for input_vec, output_vec in zip(position, result, strict=False): if np.isnan(input_vec).all() or (input_vec == constants.FILLVAL).all(): assert (output_vec == constants.FILLVAL).all()