Skip to content

Commit

Permalink
Add spice infrastructure code for spice bodies and frames
Browse files Browse the repository at this point in the history
Add geometry functions for computing IMAP spacecraft state (position, velocity)
Add IMAP SPK kernel from Nick's 366 day simulation
Add test coverage for new geometry functions
  • Loading branch information
subagonsouth committed Aug 28, 2024
1 parent 8ef0cb6 commit f3ec1af
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 2 deletions.
129 changes: 128 additions & 1 deletion imap_processing/spice/geometry.py
Original file line number Diff line number Diff line change
@@ -1 +1,128 @@
"""Functions for computing geometry using SPICE."""
"""Functions for computing geometry, many of which use SPICE."""

from collections.abc import Iterable
from enum import Enum
from typing import NamedTuple, Optional, Union

import numpy as np
import spiceypy as spice

from imap_processing.spice.kernels import ensure_spice


class SpiceId(NamedTuple):
"""Class that represents a unique identifier in the NAIF SPICE library."""

strid: str
numid: int


class SpiceBody(Enum):
"""Enum containing SPICE IDs for bodies that we use."""

# A subset of IMAP Specific bodies as defined in imap_wkcp.tf
IMAP = SpiceId("IMAP", -43)
IMAP_SPACECRAFT = SpiceId("IMAP_SPACECRAFT", -43000)
# IMAP Pointing Frame (Despun) as defined in iamp_science_0001.tf
IMAP_DPS = SpiceId("IMAP_DPS", -43901)
# Standard NAIF bodies
SSB = SpiceId("SOLAR_SYSTEM_BARYCENTER", 0)
SUN = SpiceId("SUN", 10)
EARTH = SpiceId("EARTH", 399)


class SpiceFrame(Enum):
"""Enum containing SPICE IDs for reference frames, defined in imap_wkcp.tf."""

# Standard SPICE Frames
J2000 = SpiceId("J2000", 1)
ECLIPJ2000 = SpiceId("ECLIPJ2000", 17)
# IMAP specific
IMAP_SPACECRAFT = SpiceId("IMAP_SPACECRAFT", -43000)
IMAP_LO_BASE = SpiceId("IMAP_LO_BASE", -43100)
IMAP_LO_STAR_SENSOR = SpiceId("IMAP_LO_STAR_SENSOR", -43103)
IMAP_LO = SpiceId("IMAP_LO", -43105)
IMAP_HI_45 = SpiceId("IMAP_HI_45", -43150)
IMAP_HI_90 = SpiceId("IMAP_HI_90", -43160)
IMAP_ULTRA_45 = SpiceId("IMAP_ULTRA_45", -43200)
IMAP_ULTRA_90 = SpiceId("IMAP_ULTRA_90", -43210)
IMAP_MAG = SpiceId("IMAP_MAG", -43250)
IMAP_SWE = SpiceId("IMAP_SWE", -43300)
IMAP_SWAPI = SpiceId("IMAP_SWAPI", -43350)
IMAP_CODICE = SpiceId("IMAP_CODICE", -43400)
IMAP_HIT = SpiceId("IMAP_HIT", -43500)
IMAP_IDEX = SpiceId("IMAP_IDEX", -43700)
IMAP_GLOWS = SpiceId("IMAP_GLOWS", -43750)


def imap_state(
et: Union[np.ndarray, float],
ref_frame: Optional[SpiceFrame] = None,
observer: Optional[SpiceBody] = None,
) -> Union[np.ndarray, Iterable[np.ndarray]]:
"""
Get the state (position and velocity) of the IMPA spacecraft.
By default, the state is returned in the ECLIPJ2000 frame as observed by the Sun.
Parameters
----------
et : np.ndarray or float
Epoch time(s) [J2000 seconds] to get the IMAP state for.
ref_frame : SpiceFrame, optional
Reference frame which the IMAP state is expressed in.
observer : SpiceBody, optional
Observing body.
Returns
-------
state : np.ndarray or Iterable[np.ndarray]
The Cartesian state vector representing the position and velocity of the
IMAP spacecraft.
"""
if ref_frame is None:
ref_frame = SpiceFrame.ECLIPJ2000
if observer is None:
observer = SpiceBody.SUN
state, light_time = ensured_spkezr(
SpiceBody.IMAP.name, et, ref_frame.name, "NONE", observer.name
)
return state


def ensured_spkezr(
targ: str, et: Union[np.ndarray, float], ref: str, abcorr: str, obs: str
) -> Union[tuple[np.ndarray, float], tuple[Iterable[np.ndarray], Iterable[float]]]:
"""
Wrap spice.spkezr() function with ensure_spice.
Parameters
----------
targ : str
Target body name.
et : ndarray or float
J2000 observer times.
ref : str
Reference frame name.
abcorr : str
Aberration correction method.
obs : str
Observing body name.
Returns
-------
state : np.ndarray or Iterable[np.ndarray]
State of target.
light_time : float or Iterable[float]
One way light time between observer and target.
Notes
-----
https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/spicelib/spkezr.html
"""
# No vectorization is needed b/c spiceypy already adds vectorization to the
# spkezr function. If specific time coverage functionality is added to
# @ensure_spice, parameters can be added here.
ensured = ensure_spice(spice.spkezr)
state, light_time = ensured(targ, et, ref, abcorr, obs)
return state, light_time
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
{SPICE_TEST_DATA_PATH}/imap_sclk_0000.tsc
{SPICE_TEST_DATA_PATH}/naif0012.tls
{SPICE_TEST_DATA_PATH}/naif0012.tls
{SPICE_TEST_DATA_PATH}/imap_spk_demo.bsp
34 changes: 34 additions & 0 deletions imap_processing/tests/spice/test_geometry.py
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
"""Tests coverage for imap_processing/spice/geometry.py"""

import numpy as np

from imap_processing.spice.geometry import (
SpiceBody,
SpiceFrame,
ensured_spkezr,
imap_state,
)


def test_imap_state(use_test_metakernel):
"""Test coverage for imap_state()"""
et = np.linspace(798033670, 798034670)
state = imap_state(et, observer=SpiceBody.EARTH)
assert len(state) == len(et)


def test_ensured_spkezr(use_test_metakernel):
"""Test coverage for ensured_spkezr()"""
# The imap_spk_demo.bsp kernel provides ephemeris coverage for 2025-04-15 to
# 2026-04-16. The kernel provides the IMAP ephemeris relative to Earth, so
# only the position relative to Earth can be queried without loading
# additional kernels.
# The queried ET, 798033670 is ~2025-04-16T00:00:00.0
state, lt = ensured_spkezr(
SpiceBody.IMAP.name,
798033670,
SpiceFrame.ECLIPJ2000.name,
"NONE",
SpiceBody.EARTH.name,
)
assert state.shape == (6,)
assert isinstance(lt, float)

0 comments on commit f3ec1af

Please sign in to comment.