-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
728 spice kernel furnishing decorator (#734)
* Add preliminary decorator that handles automatically furnishing of spice kernels in correct module Add test incomplete test coverage of ensure_spice decorator in correct location * Add spice kernel furnishing fixtures Add leapsecond kernel to test data Add sclk kernel to test data Add metekrnel template to test data * Add test coverage for time kernels only use of ensure_spice Add test coverage for ensure_spice failure -> SpiceyError * Remove unused fixture Move metakernel template to other spice kernel location * remove duplicate kernel files * Add comments about why ensure_spice tests expect an exception to be raised * Use imap_data_access.config DATA_DIR instead of new tmp_path to write test metakernel Move leapsecond and spacecraft clock kernel into imap_processing package
- Loading branch information
1 parent
b3d0377
commit 4a2642b
Showing
7 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,158 @@ | ||
"""Functions for furnishing and tracking SPICE kernels.""" | ||
|
||
import functools | ||
import logging | ||
import os | ||
from typing import Any, Callable, Optional | ||
|
||
import spiceypy as spice | ||
from spiceypy.utils.exceptions import SpiceyError | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def ensure_spice( | ||
f_py: Optional[Callable] = None, time_kernels_only: bool = False | ||
) -> Callable: | ||
""" | ||
Decorator/wrapper that automatically furnishes SPICE kernels. | ||
Parameters | ||
---------- | ||
f_py : Callable | ||
The function requiring SPICE that we are going to wrap if being used | ||
explicitly, otherwise None, in which case ensure_spice is being used, | ||
not as a function wrapper (see l2a_processing.py) but as a true | ||
decorator without an explicit function argument. | ||
time_kernels_only : bool | ||
Specify that we only need to furnish time kernels (if SPICE_METAKERNEL | ||
is set, we still just furnish that metakernel and assume the time | ||
kernels are included. | ||
Returns | ||
------- | ||
Callable | ||
Decorated function, with spice error handling. | ||
Notes | ||
----- | ||
Before trying to understand this piece of code, read this: | ||
https://stackoverflow.com/questions/5929107/decorators-with-parameters/60832711#60832711 | ||
**Control flow overview:** | ||
1. Try simply calling the wrapped function naively. | ||
* SUCCESS? Great! We're done. | ||
* SpiceyError? Go to step 2. | ||
2. Furnish metakernel at SPICE_METAKERNEL | ||
* SUCCESS? Great, return the original function again (so it can be | ||
re-run). | ||
* KeyError? Seems like SPICE_METAKERNEL isn't set, no problem. Go to | ||
step 3. | ||
3. Did we get the parameter time_kernels_only=True? | ||
--> YES? We only need LSK and SCLK kernels to run this function. Go fetch | ||
those and furnish and return the original function (so it can be re-run). | ||
--> NO? Dang. This is sort of the end of the line. Re-raise the error | ||
generated from the failed spiceypy function call but add a better | ||
message to it. | ||
Examples | ||
-------- | ||
There are three ways to use this object | ||
1. A decorator with no arguments | ||
>>> @ensure_spice | ||
... def my_spicey_func(a, b): | ||
... pass | ||
2. A decorator with parameters. This is useful | ||
if we only need the latest SCLK and LSK kernels for the function involved. | ||
>>> @ensure_spice(time_kernels_only=True) | ||
... def my_spicey_time_func(a, b): | ||
... pass | ||
3. An explicit wrapper function, providing a dynamically set value for | ||
parameters, e.g. time_kernels_only | ||
>>> wrapped = ensure_spice(spicey_func, time_kernels_only=True) | ||
... result = wrapped(*args, **kwargs) | ||
""" | ||
if f_py and not callable(f_py): | ||
raise ValueError( | ||
f"Received a non-callable object {f_py} as the f_py argument to" | ||
f"ensure_spice. f_py must be a callable object." | ||
) | ||
|
||
def _decorator(func: Callable[..., Callable]) -> Callable: | ||
""" | ||
Decorate or wrap input function depending on how ensure_spice is used. | ||
Parameters | ||
---------- | ||
func : Callable | ||
The function to be decorated/wrapped. | ||
Returns | ||
------- | ||
Callable | ||
If used as a function wrapper, the decorated function is returned. | ||
""" | ||
|
||
@functools.wraps(func) | ||
def wrapper_ensure_spice(*args: Any, **kwargs: Any) -> Any: | ||
""" | ||
Wrap the function that ensure_spice is used on. | ||
Parameters | ||
---------- | ||
*args : list | ||
The positional arguments passed to the decorated function. | ||
**kwargs | ||
The keyword arguments passed to the decorated function. | ||
Returns | ||
------- | ||
Object | ||
Output from wrapped function. | ||
""" | ||
try: | ||
# Step 1. | ||
return func( | ||
*args, **kwargs | ||
) # Naive first try. Maybe SPICE is already furnished. | ||
except SpiceyError as spicey_err: | ||
try: | ||
# Step 2. | ||
metakernel_path = os.environ["SPICE_METAKERNEL"] | ||
spice.furnsh(metakernel_path) | ||
except KeyError: | ||
# TODO: An additional step that was used on EMUS was to get | ||
# a custom metakernel from the SDC API based on an input | ||
# time range. | ||
if time_kernels_only: | ||
# Step 3. | ||
# TODO: Decide if this is useful for IMAP. Possible | ||
# implementation could include downloading | ||
# the most recent leapsecond kernel from NAIF (see: | ||
# https://lasp.colorado.edu/nucleus/projects/LIBSDC/repos/libera_utils/browse/libera_utils/spice_utils.py | ||
# for LIBERA implementation of downloading and caching | ||
# kernels) and finding the most recent IMAP clock | ||
# kernel in EFS. | ||
raise NotImplementedError from spicey_err | ||
else: | ||
raise SpiceyError( | ||
"When calling a function requiring SPICE, we failed" | ||
"to load a metakernel. SPICE_METAKERNEL is not set," | ||
"and time_kernels_only is not set to True" | ||
) from spicey_err | ||
return func(*args, **kwargs) | ||
|
||
return wrapper_ensure_spice | ||
|
||
# Note: This return was originally implemented as a ternary operator, but | ||
# this caused mypy to fail due to this bug: | ||
# https://github.com/python/mypy/issues/4134 | ||
if callable(f_py): | ||
return _decorator(f_py) | ||
else: | ||
return _decorator |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
2 changes: 2 additions & 0 deletions
2
imap_processing/tests/spice/test_data/imap_test_metakernel.template
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
{SPICE_TEST_DATA_PATH}/imap_sclk_0000.tsc | ||
{SPICE_TEST_DATA_PATH}/naif0012.tls |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,69 @@ | ||
"""Tests coverage for imap_processing/spice/kernels.py""" | ||
|
||
import pytest | ||
import spiceypy as spice | ||
from spiceypy.utils.exceptions import SpiceyError | ||
|
||
from imap_processing.spice import kernels | ||
|
||
|
||
@kernels.ensure_spice | ||
def single_wrap_et2utc(et, fmt, prec): | ||
"""Directly decorate a spice function with ensure_spice for use in tests""" | ||
return spice.et2utc(et, fmt, prec) | ||
|
||
|
||
@kernels.ensure_spice | ||
def double_wrap_et2utc(et, fmt, prec): | ||
"""Decorate a spice function twice with ensure_spice for use in tests. This | ||
simulates some decorated outer functions that call lower level functions | ||
that are already decorated.""" | ||
return single_wrap_et2utc(et, fmt, prec) | ||
|
||
|
||
@kernels.ensure_spice(time_kernels_only=True) | ||
def single_wrap_et2utc_tk_only(et, fmt, prec): | ||
"""Directly wrap a spice function with optional time_kernels_only set True""" | ||
return spice.et2utc(et, fmt, prec) | ||
|
||
|
||
@kernels.ensure_spice(time_kernels_only=True) | ||
def double_wrap_et2utc_tk_only(et, fmt, prec): | ||
"""Decorate a spice function twice with ensure_spice for use in tests. This | ||
simulates some decorated outer functions that call lower level functions | ||
that are already decorated.""" | ||
return single_wrap_et2utc(et, fmt, prec) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"func", | ||
[ | ||
single_wrap_et2utc, | ||
single_wrap_et2utc_tk_only, | ||
double_wrap_et2utc, | ||
double_wrap_et2utc_tk_only, | ||
], | ||
) | ||
def test_ensure_spice_emus_mk_path(func, use_test_metakernel): | ||
"""Test functionality of ensure spice with SPICE_METAKERNEL set""" | ||
assert func(577365941.184, "ISOC", 3) == "2018-04-18T23:24:31.998" | ||
|
||
|
||
def test_ensure_spice_time_kernels(): | ||
"""Test functionality of ensure spice with timekernels set""" | ||
wrapped = kernels.ensure_spice(spice.et2utc, time_kernels_only=True) | ||
# TODO: Update/remove this test when a decision has been made about | ||
# whether IMAP will use the time_kernels_only functionality and the | ||
# ensure_spice decorator has been update. | ||
with pytest.raises(NotImplementedError): | ||
_ = wrapped(577365941.184, "ISOC", 3) == "2018-04-18T23:24:31.998" | ||
|
||
|
||
def test_ensure_spice_key_error(): | ||
"""Test functionality of ensure spice when all branches fail""" | ||
wrapped = kernels.ensure_spice(spice.et2utc) | ||
# The ensure_spice decorator should raise a SpiceyError when all attempts to | ||
# furnish a set of kernels with sufficient coverage for the spiceypy | ||
# functions that it decorates. | ||
with pytest.raises(SpiceyError): | ||
_ = wrapped(577365941.184, "ISOC", 3) == "2018-04-18T23:24:31.998" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters