"""SPICE kernel loading (ported from rspk_loadfiles.f and rspk_loadsc.f)."""
from __future__ import annotations
import logging
from pathlib import Path
import cspyce
from ephemeris_tools.config import get_spice_path
from ephemeris_tools.constants import EARTH_ID
from ephemeris_tools.spice.common import MAXSHIFTS, get_state
logger = logging.getLogger(__name__)
[docs]
def load_spice_files(
planet: int, version: int = 0, *, force: bool = False
) -> tuple[bool, str | None]:
"""Load SPICE kernels for the given planet (port of RSPK_LoadFiles).
Initializes the library for geometry calculations. Must be called before
other SPICE/RSPK calls (except set_observer_*). Subsequent calls for the
same planet do nothing. Observer is reset to Earth center.
Parameters:
planet: Planet index: 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune,
9=Pluto.
version: Ephemeris version number, or 0 for latest.
force: If True, clear kernel pool and internal state and reload (for
tests that need a specific planet after another was loaded).
Returns:
(True, None) if loaded successfully; (False, error_message) on failure.
"""
state = get_state()
if force:
try:
cspyce.kclear()
except Exception as e:
return (False, f'Cannot clear SPICE pool for force reload: {e}')
state.pool_loaded = False
state.planet_num = 0
state.planet_id = 0
state.obs_id = EARTH_ID
state.obs_is_set = False
state.nshifts = 0
state.shift_id = [0] * MAXSHIFTS
state.shift_dt = [0.0] * MAXSHIFTS
elif state.planet_num != 0 and state.planet_num != planet:
return (False, 'SPICE already loaded for a different planet')
base = Path(get_spice_path())
if not base.exists():
return (False, f'SPICE_PATH directory does not exist: {base}')
if not base.is_dir():
return (False, f'SPICE_PATH is not a directory: {base}')
if not state.pool_loaded:
for ker in ('leapseconds.ker', 'p_constants.ker'):
ker_path = base / ker
if ker_path.exists():
try:
cspyce.furnsh(str(ker_path))
except Exception as e:
logger.warning('Failed to load %s: %s', ker_path, e)
state.pool_loaded = True
config_path = base / 'SPICE_planets.txt'
if not config_path.exists():
logger.warning('Config not found: %s', config_path)
return (
False,
f'SPICE_planets.txt not found under {base}. '
'Ensure SPICE_PATH points to a SPICE kernel tree that includes SPICE_planets.txt.',
)
load_version = version
loaded = False
with config_path.open() as f:
for line_no, line in enumerate(f, start=1):
line = line.strip()
if not line or line.startswith('!'):
continue
parts = line.split(',')
if len(parts) < 3:
logger.error(
'SPICE_planets.txt line %d: expected at least 3 fields '
'(planet version filename), got %d: %r',
line_no,
len(parts),
line,
)
continue
try:
p = int(parts[0])
v = int(parts[1])
filename = parts[2].strip().strip('"')
except ValueError as e:
logger.error(
'SPICE_planets.txt line %d: bad value '
'(planet/version must be integer): %r - %s',
line_no,
line,
e,
)
continue
if load_version == 0 and p == planet:
load_version = v
if p == planet and v == load_version:
kpath = base / filename
if kpath.exists():
try:
cspyce.furnsh(str(kpath))
loaded = True
except Exception as e:
logger.warning('Failed to load %s: %s', kpath, e)
if not loaded:
return (
False,
f'No kernel files for planet {planet} (version {load_version}) found under {base}. '
'Check SPICE_planets.txt for planet/version and that the listed kernel files exist.',
)
state.planet_num = planet
state.planet_id = planet * 100 + 99
# Match FORTRAN RSPK_LoadFiles behavior: reset observer to Earth's center.
state.obs_id = EARTH_ID
state.obs_is_set = False
state.nshifts = 0
state.shift_id = [0] * MAXSHIFTS
state.shift_dt = [0.0] * MAXSHIFTS
return (True, None)
[docs]
def load_spacecraft(
sc_id: str,
planet: int,
version: int = 0,
set_obs: bool = True,
) -> bool:
"""Load SPICE kernels for a spacecraft at a planet (port of RSPK_LoadSC).
Loads kernels needed for geometry with that spacecraft. Optionally sets
the observer to the spacecraft. Call after load_spice_files or in place
of it for the same planet.
Parameters:
sc_id: Spacecraft identifier (e.g. 'CAS', 'VG1', 'NH', 'GLL').
planet: Planet index: 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune.
version: Ephemeris version, or 0 for latest.
set_obs: If True, set observer to this spacecraft.
Returns:
True if kernels were loaded, False otherwise.
"""
state = get_state()
if state.planet_num != 0 and state.planet_num != planet:
return False
base = Path(get_spice_path())
if not state.pool_loaded:
for ker in ('leapseconds.ker', 'p_constants.ker'):
ker_path = base / ker
if ker_path.exists():
try:
cspyce.furnsh(str(ker_path))
except Exception as e:
logger.warning('Failed to load %s: %s', ker_path, e)
state.pool_loaded = True
config_path = base / 'SPICE_spacecraft.txt'
if not config_path.exists():
logger.warning('Config not found: %s', config_path)
return False
sc_upper = sc_id.strip().upper()
load_version = version
loaded = False
with config_path.open() as f:
for line_no, line in enumerate(f, start=1):
line = line.strip()
if not line or line.startswith('!'):
continue
parts = line.split(',')
if len(parts) < 5:
logger.error(
'SPICE_spacecraft.txt line %d: expected at least 5 fields '
'(name planet version naif_id filename), got %d: %r',
line_no,
len(parts),
line,
)
continue
try:
name = parts[0].strip().strip('"').upper()
p = int(parts[1])
v = int(parts[2])
naif_id = int(parts[3])
filename = parts[4].strip().strip('"')
except (ValueError, IndexError) as e:
logger.error(
'SPICE_spacecraft.txt line %d: bad value '
'(planet/version/naif_id must be integer): %r - %s',
line_no,
line,
e,
)
continue
if name == sc_upper and p == planet and load_version == 0:
load_version = v
if name == sc_upper and p == planet and v == load_version:
kpath = base / filename
if kpath.exists():
try:
cspyce.furnsh(str(kpath))
loaded = True
if set_obs:
state.obs_id = naif_id
# Match FORTRAN RSPK_SetObsId semantics: this flag is only
# for geodetic Earth observatories, not body observers.
state.obs_is_set = False
except Exception as e:
logger.warning('Failed to load %s: %s', kpath, e)
if not loaded:
return False
state.planet_num = planet
state.planet_id = planet * 100 + 99
return True