Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion imap_processing/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1799,12 +1799,28 @@ def do_processing(
datasets = list(quaternions.process_quaternions(input_files[0]))
processed_dataset.extend(datasets)
elif self.descriptor == "pointing-attitude":
if self.start_date is None:
raise ValueError(
"start_date must be provided for pointing-attitude processing."
)
spice_inputs = dependencies.get_file_paths(
data_type=SPICESource.SPICE.value
)
ah_paths = [path for path in spice_inputs if ".ah" in path.suffixes]
resolved_version = self._resolve_version(self.descriptor)
if resolved_version is None:
raise ValueError(
"No version provided for pointing-attitude processing. "
"Provide a version for the 'pointing-attitude' descriptor in "
"the dependency JSON's version block, or a fallback --version."
)
minor_version = (
resolved_version.minor
if isinstance(resolved_version, Version)
else int(resolved_version.lstrip("v"))
)
pointing_kernel_paths = pointing_frame.generate_pointing_attitude_kernel(
ah_paths
ah_paths, self.start_date, minor_version
Comment thread
tmplummer marked this conversation as resolved.
)
processed_dataset.extend(pointing_kernel_paths)
else:
Expand Down
46 changes: 36 additions & 10 deletions imap_processing/spice/pointing_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import numpy as np
import spiceypy
from imap_data_access import SPICEFilePath
from imap_data_access.file_validation import Version
from numpy.typing import NDArray

from imap_processing.spice import IMAP_SC_ID
Expand All @@ -19,6 +19,7 @@
et_to_utc,
met_to_sclkticks,
sct_to_et,
str_to_et,
)

logger = logging.getLogger(__name__)
Expand All @@ -35,7 +36,9 @@
)


def generate_pointing_attitude_kernel(imap_attitude_cks: list[Path]) -> list[Path]:
def generate_pointing_attitude_kernel(
imap_attitude_cks: list[Path], start_date: str, minor_version: int
) -> list[Path]:
"""
Generate pointing attitude kernel from input IMAP CK kernel.

Expand All @@ -44,13 +47,22 @@ def generate_pointing_attitude_kernel(imap_attitude_cks: list[Path]) -> list[Pat
imap_attitude_cks : list[Path]
List of the IMAP attitude kernels from which to generate pointing
attitude.
start_date : str
Earliest date, in YYYYMMDD format, to cover with the pointing
attitude kernel. Only pointings fully covered by the input CKs
that start on or after this date are included.
minor_version : int
Minor version, from the batch command, to use for the output
pointing attitude kernel filename.

Returns
-------
pointing_kernel_path : list[Path]
Location of the new pointing kernels.
"""
pointing_segments = calculate_pointing_attitude_segments(imap_attitude_cks)
pointing_segments = calculate_pointing_attitude_segments(
imap_attitude_cks, start_date
)
if len(pointing_segments) == 0:
raise ValueError("No Pointings covered by input dependencies.")

Expand All @@ -61,15 +73,13 @@ def generate_pointing_attitude_kernel(imap_attitude_cks: list[Path]) -> list[Pat
end_datetime = spiceypy.et2datetime(
sct_to_et(pointing_segments[-1]["end_sclk_ticks"])
)
# Use the last ck from sorted list to get the version number. I
# don't think this will be anything but 1.
sorted_ck_paths = list(sorted(imap_attitude_cks, key=lambda x: x.name))
spice_file = SPICEFilePath(sorted_ck_paths[-1].name)
version_str = str(Version(None, minor_version)).lstrip("v")
pointing_kernel_path = (
sorted_ck_paths[-1].parent / f"imap_dps_"
f"{start_datetime.strftime('%Y_%j')}_"
f"{end_datetime.strftime('%Y_%j')}_"
f"{spice_file.spice_metadata['version']}.ah.bc"
f"{version_str}.ah.bc"
)
write_pointing_frame_ck(
pointing_kernel_path, pointing_segments, [p.name for p in imap_attitude_cks]
Expand Down Expand Up @@ -176,6 +186,7 @@ def write_pointing_frame_ck(

def calculate_pointing_attitude_segments(
ck_paths: list[Path],
start_date: str,
) -> NDArray:
"""
Calculate the data for each segment of the DPS_FRAME attitude kernel.
Expand All @@ -191,6 +202,10 @@ def calculate_pointing_attitude_segments(
----------
ck_paths : list[pathlib.Path]
List of CK kernels to use to generate the pointing attitude kernel.
start_date : str
Earliest date, in YYYYMMDD format, to cover with the pointing
attitude kernel. Used as the lower bound when selecting pointings
that are fully covered by the input CKs.

Returns
-------
Expand Down Expand Up @@ -221,7 +236,7 @@ def calculate_pointing_attitude_segments(
# to cover the time range of the new repoint table.
# Get the coverage of the CK files storing the earliest start time and
# latest end time.
et_start = np.inf
ck_start = np.inf
et_end = -np.inf
for ck_path in ck_paths:
ck_cover = spiceypy.ckcov(
Expand All @@ -234,12 +249,23 @@ def calculate_pointing_attitude_segments(
f"{ck_path.name} covers time range: ({et_to_utc(individual_ck_start)}, "
f"{et_to_utc(individual_ck_end)}) in {num_intervals} intervals."
)
et_start = min(et_start, individual_ck_start)
ck_start = min(ck_start, individual_ck_start)
et_end = max(et_end, individual_ck_end)

logger.info(
f"CK kernels combined coverage range: "
f"{(et_to_utc(et_start), et_to_utc(et_end))}, "
f"{(et_to_utc(ck_start), et_to_utc(et_end))}, "
)

# The batch command's start_date limits (narrows) the coverage of the
# produced pointing attitude kernel. It is floored at the CK files'
# own coverage start so that a pointing is only ever selected if it is
# actually, fully covered by the input CKs.
requested_start_et = str_to_et(datetime.strptime(start_date, "%Y%m%d").isoformat())
et_start = max(requested_start_et, ck_start)
logger.info(
f"Using coverage start date: {et_to_utc(et_start)} "
f"(requested start_date: {et_to_utc(requested_start_et)})"
)

# Get data from the repoint table and convert to Pointings
Expand Down
42 changes: 38 additions & 4 deletions imap_processing/tests/spice/test_pointing_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,14 @@ def test_generate_pointing_attitude_kernel(
"""Test coverage for generate_pointing_attitude_kernel function."""
start_date = "2024_111"
end_date = "2024_222"
version = "02"
version = "002"
mock_et2datetime.side_effect = [
datetime.strptime(date_str, "%Y_%j") for date_str in [start_date, end_date]
]
ck_path = Path(f"/bogus/file/path/imap_{start_date}_{end_date}_{version}.ah.bc")
pointing_ck_path = generate_pointing_attitude_kernel([ck_path])[0]
ck_path = Path(f"/bogus/file/path/imap_{start_date}_{end_date}_001.ah.bc")
pointing_ck_path = generate_pointing_attitude_kernel([ck_path], "20240420", 2)[0]
assert pointing_ck_path.name == f"imap_dps_{start_date}_{end_date}_{version}.ah.bc"
mock_gen_attitude_segments.assert_called_once_with([ck_path], "20240420")
# Verify that file is valid pointing_attitude kernel with imap-data-access
spice_input = SPICEInput(pointing_ck_path.name)
assert spice_input.source[0] == "pointing_attitude"
Expand All @@ -111,7 +112,7 @@ def test_generate_pointing_attitude_kernel_no_pointings(mock_gen_attitude_segmen
"""Test when no pointings are covered by the input CK."""
ck_path = Path("/bogus/file/path/imap_2025_100_2025_101_001.ah.bc")
with pytest.raises(ValueError, match="No Pointings covered"):
_ = generate_pointing_attitude_kernel([ck_path])[0]
_ = generate_pointing_attitude_kernel([ck_path], "20250101", 1)[0]


@pytest.mark.parametrize(
Expand Down Expand Up @@ -252,6 +253,7 @@ def test_calculate_pointing_attitude_segments(

segment_data = calculate_pointing_attitude_segments(
[spice_test_data_path / "imap_sim_ck_2hr_2secsampling_with_nutation.bc"],
"20000101",
)

# Nick Dutton's MATLAB code result
Expand Down Expand Up @@ -306,6 +308,7 @@ def test_multiple_pointings(

segment_data = calculate_pointing_attitude_segments(
[spice_test_data_path / "imap_sim_ck_2hr_2secsampling_with_nutation.bc"],
"20000101",
)

# The way we defined the repoints, we expect two pointing segments
Expand All @@ -317,3 +320,34 @@ def test_multiple_pointings(
np.testing.assert_allclose(
segment_data["end_sclk_ticks"], repoint_start_met[2:4] / TICK_DURATION
)


def test_calculate_pointing_attitude_segments_start_date_narrows_coverage(
spice_test_data_path,
furnish_pointing_frame_kernels,
use_fake_repoint_data_for_time,
):
"""Tests that a start_date after the CK coverage excludes an otherwise
fully-covered pointing, confirming start_date can narrow coverage beyond
what the CK files themselves provide."""
# Same single-pointing setup as test_calculate_pointing_attitude_segments,
# which is fully covered by the CK.
ck_met_start, ck_met_end = get_ck_met_coverage(furnish_pointing_frame_kernels[-1])
use_fake_repoint_data_for_time(
np.array([ck_met_start - 10, ck_met_end - 1]),
np.array([ck_met_start + 1, ck_met_end + 10]),
)

# start_date is the calendar day after the CK's own coverage ends, so it
# should exclude the pointing even though the CK fully covers it.
ck_end_et = sct_to_et(ck_met_end / TICK_DURATION)
day_after_ck_end = spiceypy.et2utc(ck_end_et + 86400, "ISOC", 0)[:10].replace(
"-", ""
)

segment_data = calculate_pointing_attitude_segments(
[spice_test_data_path / "imap_sim_ck_2hr_2secsampling_with_nutation.bc"],
day_after_ck_end,
)

assert len(segment_data) == 0
111 changes: 111 additions & 0 deletions imap_processing/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,117 @@ def test_spacecraft_pointing_kernel(

instrument.process()
assert mock_spacecraft_pointing.call_count == 1
call_args = mock_spacecraft_pointing.call_args[0]
assert call_args[1] == "20240410"
assert call_args[2] == 5
Comment thread
tmplummer marked this conversation as resolved.


@mock.patch(
"imap_processing.cli.pointing_frame.generate_pointing_attitude_kernel",
autospec=True,
)
def test_spacecraft_pointing_kernel_version_from_dependency(
mock_spacecraft_pointing, mock_instrument_dependencies
):
"""Test that a pointing-attitude entry in the dependency version map is
used, taking its minor version over the fallback --version."""

dependency_files_str = (
'[{"type": "spice","files": ["naif0012.tls", '
'"imap_sclk_0005.tsc", "imap_2024_100_2024_111_05.ah.bc"]}]'
)
dependency_str = json.dumps(
{
"dependency": json.loads(dependency_files_str),
"version": {
"pointing-attitude": {"major_version": None, "minor_version": 7}
},
}
)
input_collection = ProcessingInputCollection()
input_collection.deserialize(dependency_files_str)
mocks = mock_instrument_dependencies
mocks["mock_query"].return_value = [{"file_path": "/path/to/file0"}]
mocks["mock_download"].return_value = "file0"
mock_spacecraft_pointing.return_value = [
Path("imap_dps_2024_100_2024_111_007.ah.bc")
]
mocks["mock_write_cdf"].side_effect = ["/path/to/file0"]
mocks["mock_pre_processing"].return_value = input_collection

# Fallback --version ("v005") differs from the dependency version map's
# minor_version (7), so this confirms the batch-provided Version wins.
instrument = Spacecraft(
"l1a", "pointing-attitude", dependency_str, "20240410", "12345", "v005", False
)

instrument.process()
assert mock_spacecraft_pointing.call_count == 1
call_args = mock_spacecraft_pointing.call_args[0]
assert call_args[1] == "20240410"
assert call_args[2] == 7


@mock.patch(
"imap_processing.cli.pointing_frame.generate_pointing_attitude_kernel",
autospec=True,
)
def test_spacecraft_pointing_kernel_no_start_date(
mock_spacecraft_pointing, mock_instrument_dependencies
):
"""Test coverage for cli.Spacecraft class when only repointing is provided"""

dependency_str = (
'[{"type": "spice","files": ["naif0012.tls", '
'"imap_sclk_0005.tsc", "imap_2024_100_2024_111_05.ah.bc"]}]'
)
input_collection = ProcessingInputCollection()
input_collection.deserialize(dependency_str)
mocks = mock_instrument_dependencies
mocks["mock_query"].return_value = [{"file_path": "/path/to/file0"}]
mocks["mock_download"].return_value = "file0"
mocks["mock_pre_processing"].return_value = input_collection

# start_date is a valid CLI-only alternative to repointing, so it can be
# None here.
instrument = Spacecraft(
"l1a", "pointing-attitude", dependency_str, None, "12345", "v005", False
)

with pytest.raises(ValueError, match="start_date must be provided"):
instrument.process()
assert mock_spacecraft_pointing.call_count == 0


@mock.patch(
"imap_processing.cli.pointing_frame.generate_pointing_attitude_kernel",
autospec=True,
)
def test_spacecraft_pointing_kernel_no_version(
mock_spacecraft_pointing, mock_instrument_dependencies
):
"""Test coverage for cli.Spacecraft class when no version can be resolved"""

dependency_str = (
'[{"type": "spice","files": ["naif0012.tls", '
'"imap_sclk_0005.tsc", "imap_2024_100_2024_111_05.ah.bc"]}]'
)
input_collection = ProcessingInputCollection()
input_collection.deserialize(dependency_str)
mocks = mock_instrument_dependencies
mocks["mock_query"].return_value = [{"file_path": "/path/to/file0"}]
mocks["mock_download"].return_value = "file0"
mocks["mock_pre_processing"].return_value = input_collection

# No version block for "pointing-attitude" in the dependency JSON, and no
# fallback --version, matches argparse's default of None.
instrument = Spacecraft(
"l1a", "pointing-attitude", dependency_str, "20240410", None, None, False
)

with pytest.raises(ValueError, match="No version provided"):
instrument.process()
assert mock_spacecraft_pointing.call_count == 0


@mock.patch("imap_processing.cli.ultra_l1a.ultra_l1a")
Expand Down
Loading