Source code for ephemeris_tools.spice.load

"""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