Skip to content

Commit 85f912b

Browse files
authored
1729 add pointing frame kernel job to clipy (IMAP-Science-Operations-Center#1776)
* Add high level function for producing pointing attitude kernel * Add pointing kernel job to cli Spacecraft.do_processing Create custom Spacecraft.post_processing to handle list[Path] returned by generate_poiting_attitude_kernel() * bump imap-data-access version * add poetry.lock updates * Allow for Paths to be passed in to default post_processing method * Fix bug found in integration testing * Improve pointing kernel name generation
1 parent 35afb51 commit 85f912b

File tree

7 files changed

+163
-35
lines changed

7 files changed

+163
-35
lines changed

imap_processing/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"idex": ["l1a", "l1b", "l2a", "l2b", "l2c"],
2828
"lo": ["l1a", "l1b", "l1c", "l2"],
2929
"mag": ["l1a", "l1b", "l1c", "l1d", "l2"],
30-
"spacecraft": ["l1a"],
30+
"spacecraft": ["l1a", "spice"],
3131
"swapi": ["l1", "l2", "l3a", "l3b"],
3232
"swe": ["l1a", "l1b", "l2"],
3333
"ultra": ["l1a", "l1b", "l1c", "l2"],

imap_processing/cli.py

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from imap_processing.mag.l1c.mag_l1c import mag_l1c
7070
from imap_processing.mag.l2.mag_l2 import mag_l2
7171
from imap_processing.spacecraft import quaternions
72+
from imap_processing.spice import pointing_frame
7273
from imap_processing.swapi.l1.swapi_l1 import swapi_l1
7374
from imap_processing.swapi.l2.swapi_l2 import swapi_l2
7475
from imap_processing.swapi.swapi_utils import read_swapi_lut_table
@@ -437,6 +438,7 @@ def process(self) -> None:
437438
2. Do the data processing. The result of this step will usually be a list
438439
of new products (files).
439440
3. Post-processing actions such as uploading files to the IMAP SDC.
441+
4. Final cleanup actions.
440442
"""
441443
logger.info(f"IMAP Processing Version: {imap_processing._version.__version__}")
442444
logger.info(f"Processing {self.__class__.__name__} level {self.data_level}")
@@ -446,6 +448,7 @@ def process(self) -> None:
446448
products = self.do_processing(dependencies)
447449
logger.info("Beginning postprocessing (uploading data products)")
448450
self.post_processing(products, dependencies)
451+
self.cleanup()
449452
logger.info("Processing complete")
450453

451454
def pre_processing(self) -> ProcessingInputCollection:
@@ -497,13 +500,23 @@ def do_processing(
497500
raise NotImplementedError
498501

499502
def post_processing(
500-
self, datasets: list[xr.Dataset], dependencies: ProcessingInputCollection
503+
self,
504+
processed_data: list[xr.Dataset | Path],
505+
dependencies: ProcessingInputCollection,
501506
) -> None:
502507
"""
503508
Complete post-processing.
504509
505-
Default post-processing consists of writing the datasets to local storage
506-
and then uploading those newly generated products to the IMAP SDC.
510+
Default post-processing consists of the following:
511+
For each xarray.Dataset:
512+
1. Set `Data_version` global attribute.
513+
2. Set `Repointing` global attribute for appropriate products.
514+
3. Set `Start_date` global attribute.
515+
4. Set `Parents` global attribute.
516+
5. Write the xarray.Dataset to a local CDF file.
517+
The resulting paths to CDF files as well as any Path included in the
518+
`processed_data` input are then uploaded to the IMAP SDC.
519+
507520
Child classes can override this method to customize the
508521
post-processing actions.
509522
@@ -513,12 +526,13 @@ def post_processing(
513526
514527
Parameters
515528
----------
516-
datasets : list[xarray.Dataset]
517-
A list of datasets (products) produced by do_processing method.
529+
processed_data : list[xarray.Dataset | Path]
530+
A list of datasets (products) and paths produced by the do_processing
531+
method.
518532
dependencies : ProcessingInputCollection
519533
Object containing dependencies to process.
520534
"""
521-
if len(datasets) == 0:
535+
if len(processed_data) == 0:
522536
logger.info("No products to write to CDF file.")
523537
return
524538

@@ -541,16 +555,23 @@ def post_processing(
541555
# If it is start_date, skip repointing in the output filename.
542556

543557
products = []
544-
for ds in datasets:
545-
ds.attrs["Data_version"] = self.version
546-
if self.repointing is not None:
547-
ds.attrs["Repointing"] = self.repointing
548-
ds.attrs["Start_date"] = self.start_date
549-
ds.attrs["Parents"] = parent_files
550-
products.append(write_cdf(ds))
558+
for ds in processed_data:
559+
if isinstance(ds, xr.Dataset):
560+
ds.attrs["Data_version"] = self.version
561+
if self.repointing is not None:
562+
ds.attrs["Repointing"] = self.repointing
563+
ds.attrs["Start_date"] = self.start_date
564+
ds.attrs["Parents"] = parent_files
565+
products.append(write_cdf(ds))
566+
else:
567+
# A path to a product that was already written out
568+
products.append(ds)
551569

552570
self.upload_products(products)
553571

572+
@final
573+
def cleanup(self) -> None:
574+
"""Cleanup from processing."""
554575
logger.info("Clearing furnished SPICE kernels")
555576
spiceypy.kclear()
556577

@@ -1060,7 +1081,7 @@ class Spacecraft(ProcessInstrument):
10601081

10611082
def do_processing(
10621083
self, dependencies: ProcessingInputCollection
1063-
) -> list[xr.Dataset]:
1084+
) -> list[xr.Dataset | Path]:
10641085
"""
10651086
Perform Spacecraft specific processing.
10661087
@@ -1071,26 +1092,41 @@ def do_processing(
10711092
10721093
Returns
10731094
-------
1074-
datasets : xr.Dataset
1075-
Xr.Dataset of products.
1095+
datasets : list[xarray.Dataset | Path]
1096+
The list of processed products.
10761097
"""
10771098
print(f"Processing Spacecraft {self.data_level}")
10781099

1079-
if self.data_level != "l1a":
1100+
if self.data_level == "l1a":
1101+
# File path is expected output file path
1102+
input_files = dependencies.get_file_paths(source="spacecraft")
1103+
if len(input_files) > 1:
1104+
raise ValueError(
1105+
f"Unexpected dependencies found for Spacecraft L1A: "
1106+
f"{input_files}. Expected only one dependency."
1107+
)
1108+
datasets = list(quaternions.process_quaternions(input_files[0]))
1109+
return datasets
1110+
elif self.data_level == "spice":
1111+
spice_inputs = dependencies.get_file_paths(
1112+
data_type=SPICESource.SPICE.value
1113+
)
1114+
ah_paths = [path for path in spice_inputs if ".ah" in path.suffixes]
1115+
if len(ah_paths) != 1:
1116+
raise ValueError(
1117+
f"Unexpected spice dependencies found for Spacecraft "
1118+
f"pointing_kernel: {ah_paths}. Expected exactly one "
1119+
f"attitude history file."
1120+
)
1121+
pointing_kernel_paths = pointing_frame.generate_pointing_attitude_kernel(
1122+
ah_paths[0]
1123+
)
1124+
return pointing_kernel_paths
1125+
else:
10801126
raise NotImplementedError(
10811127
f"Spacecraft processing not implemented for level {self.data_level}"
10821128
)
10831129

1084-
# File path is expected output file path
1085-
input_files = dependencies.get_file_paths(source="spacecraft")
1086-
if len(input_files) > 1:
1087-
raise ValueError(
1088-
f"Unexpected dependencies found for Spacecraft L1A: "
1089-
f"{input_files}. Expected only one dependency."
1090-
)
1091-
datasets = list(quaternions.process_quaternions(input_files[0]))
1092-
return datasets
1093-
10941130

10951131
class Swapi(ProcessInstrument):
10961132
"""Process SWAPI."""

imap_processing/spice/pointing_frame.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import numpy as np
1111
import spiceypy
12+
from imap_data_access import SPICEFilePath
1213
from numpy.typing import NDArray
1314

1415
from imap_processing.spice.geometry import SpiceFrame
@@ -35,6 +36,39 @@
3536
)
3637

3738

39+
def generate_pointing_attitude_kernel(imap_attitude_ck: Path) -> list[Path]:
40+
"""
41+
Generate pointing attitude kernel from input IMAP CK kernel.
42+
43+
Parameters
44+
----------
45+
imap_attitude_ck : Path
46+
Location of the IMAP attitude kernel from which to generate pointing
47+
attitude.
48+
49+
Returns
50+
-------
51+
pointing_kernel_path : list[Path]
52+
Location of the new pointing kernels.
53+
"""
54+
pointing_segments = calculate_pointing_attitude_segments(imap_attitude_ck)
55+
# get the start and end yyyy_doy strings
56+
# TODO: For now just use the input CK start/end dates. It is possible that
57+
# the end date is incorrect b/c the repoint table determines the last
58+
# segment in the pointing kernel.
59+
spice_file = SPICEFilePath(imap_attitude_ck.name)
60+
pointing_kernel_path = (
61+
imap_attitude_ck.parent / f"imap_dps_"
62+
f"{spice_file.spice_metadata['start_date'].strftime('%Y_%j')}_"
63+
f"{spice_file.spice_metadata['end_date'].strftime('%Y_%j')}_"
64+
f"{spice_file.spice_metadata['version']}.ah.bc"
65+
)
66+
write_pointing_frame_ck(
67+
pointing_kernel_path, pointing_segments, imap_attitude_ck.name
68+
)
69+
return [pointing_kernel_path]
70+
71+
3872
@contextmanager
3973
def open_spice_ck_file(pointing_frame_path: Path) -> Generator[int, None, None]:
4074
"""
@@ -218,7 +252,7 @@ def calculate_pointing_attitude_segments(
218252
"repoint_id"
219253
]
220254
pointing_start_et = repoint_df.iloc[i_pointing]["repoint_end_et"]
221-
pointing_end_et = repoint_df["repoint_start_et"][i_pointing + 1]
255+
pointing_end_et = repoint_df.iloc[i_pointing + 1]["repoint_start_et"]
222256
logger.debug(
223257
f"Calculating pointing attitude for pointing "
224258
f"{pointing_segments[i_pointing]['pointing_id']} with time "

imap_processing/tests/spice/test_pointing_frame.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
"""Test coverage for imap_processing.spice.repoint.py"""
22

3+
from pathlib import Path
4+
from unittest import mock
5+
36
import numpy as np
47
import pytest
58
import spiceypy
9+
from imap_data_access import SPICEInput
610

711
from imap_processing.spice import IMAP_SC_ID
812
from imap_processing.spice.geometry import SpiceFrame
@@ -11,6 +15,7 @@
1115
_average_quaternions,
1216
_create_rotation_matrix,
1317
calculate_pointing_attitude_segments,
18+
generate_pointing_attitude_kernel,
1419
write_pointing_frame_ck,
1520
)
1621
from imap_processing.spice.time import TICK_DURATION
@@ -50,6 +55,27 @@ def et_times(pointing_frame_kernels):
5055
return et_times
5156

5257

58+
@mock.patch(
59+
"imap_processing.spice.pointing_frame.write_pointing_frame_ck", autospec=True
60+
)
61+
@mock.patch(
62+
"imap_processing.spice.pointing_frame.calculate_pointing_attitude_segments",
63+
autospec=True,
64+
return_value=None,
65+
)
66+
def test_generate_pointing_attitude_kernel(mock_gen_attitude_segments, mock_write_ck):
67+
"""Test coverage for generate_pointing_attitude_kernel function."""
68+
start_date = "2024_111"
69+
end_date = "2024_222"
70+
version = "02"
71+
ck_path = Path(f"/bogus/file/path/imap_{start_date}_{end_date}_{version}.ah.bc")
72+
pointing_ck_path = generate_pointing_attitude_kernel(ck_path)[0]
73+
assert pointing_ck_path.name == f"imap_dps_{start_date}_{end_date}_{version}.ah.bc"
74+
# Verify that file is valid pointing_attitude kernel with imap-data-access
75+
spice_input = SPICEInput(pointing_ck_path.name)
76+
assert spice_input.source[0] == "pointing_attitude"
77+
78+
5379
@pytest.mark.parametrize(
5480
"segment_start_offset, segment_end_offset, quaternion, segment_id",
5581
[

imap_processing/tests/test_cli.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,38 @@ def test_spacecraft(mock_spacecraft_l1a, mock_instrument_dependencies):
314314
assert mock_instrument_dependencies["mock_write_cdf"].call_count == 1
315315

316316

317+
@mock.patch(
318+
"imap_processing.cli.pointing_frame.generate_pointing_attitude_kernel",
319+
autospec=True,
320+
)
321+
def test_spacecraft_pointing_kernel(
322+
mock_spacecraft_pointing, mock_instrument_dependencies
323+
):
324+
"""Test coverage for cli.Spacecraft class"""
325+
326+
dependency_str = (
327+
'[{"type": "spice","files": ["naif0012.tls", '
328+
'"imap_sclk_0005.tsc", "imap_2024_100_2024_111_05.ah.bc"]}]'
329+
)
330+
input_collection = ProcessingInputCollection()
331+
input_collection.deserialize(dependency_str)
332+
mocks = mock_instrument_dependencies
333+
mocks["mock_query"].return_value = [{"file_path": "/path/to/file0"}]
334+
mocks["mock_download"].return_value = "file0"
335+
mock_spacecraft_pointing.return_value = [
336+
Path("imap_dps_2024_100_2024_111_05.ah.bc")
337+
]
338+
mocks["mock_write_cdf"].side_effect = ["/path/to/file0"]
339+
mocks["mock_pre_processing"].return_value = input_collection
340+
341+
instrument = Spacecraft(
342+
"spice", "pointing_kernel", dependency_str, "20240410", "12345", "v005", False
343+
)
344+
345+
instrument.process()
346+
assert mock_spacecraft_pointing.call_count == 1
347+
348+
317349
@mock.patch("imap_processing.cli.ultra_l1a.ultra_l1a")
318350
def test_ultra_l1a(mock_ultra_l1a, mock_instrument_dependencies):
319351
"""Test coverage for cli.Ultra class with l1a data level"""

poetry.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ exclude = ["imap_processing/tests"]
3535
[tool.poetry.dependencies]
3636
astropy-healpix = ">=1.0"
3737
cdflib = "^1.3.1"
38-
imap-data-access = ">=0.28.0"
38+
imap-data-access = ">=0.29.0"
3939
python = ">=3.10,<4"
4040
space_packet_parser = "^5.0.1"
4141
spiceypy = ">=6.0.0"

0 commit comments

Comments
 (0)