Source code for ephemeris_tools.spice.geometry

"""Body geometry: RA/Dec, phase, ranges, lat/lon (ported from rspk_* body routines)."""

from __future__ import annotations

import math

import cspyce

from ephemeris_tools.constants import EARTH_ID, SUN_ID
from ephemeris_tools.spice.bodmat import bodmat
from ephemeris_tools.spice.common import get_state
from ephemeris_tools.spice.observer import observer_state
from ephemeris_tools.spice.orbits import moon_distances
from ephemeris_tools.spice.shifts import spkapp_shifted

TWOPI = 2.0 * math.pi


[docs] def body_radec(et: float, body_id: int) -> tuple[float, float]: """Return observed J2000 right ascension and declination of a body at a time. Port of RSPK_BodyRaDec. Returns values for Earth center or an observatory depending on set_observer_location/set_observer_id. Coordinates are in the barycenter frame and are not corrected for stellar aberration (correct relative to cataloged star positions). Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). body_id: SPICE body ID. Returns: Tuple of (ra, dec) in radians. """ obs_pv = observer_state(et) body_dpv, _ = spkapp_shifted(body_id, et, 'J2000', obs_pv, 'LT') _, ra, dec = cspyce.recrad(body_dpv[:3]) return (ra, dec)
[docs] def body_phase(et: float, body_id: int) -> float: """Return solar phase angle of a body as seen from the observer (radians). Port of RSPK_BodyPhase. Uses standard SPKAPP with 'LT' (no time shift). Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). body_id: SPICE body ID. Returns: Phase angle in radians. """ obs_pv = observer_state(et) body_dpv, dt = cspyce.spkapp(body_id, et, 'J2000', obs_pv[:6], 'LT') body_time = et - dt body_pv = cspyce.spkssb(body_id, body_time, 'J2000') sun_dpv, _ = cspyce.spkapp(SUN_ID, body_time, 'J2000', body_pv[:6], 'LT') obs_dp = cspyce.vminus(body_dpv[:3]) return cspyce.vsep(sun_dpv[:3], obs_dp)
[docs] def body_ranges(et: float, body_id: int) -> tuple[float, float]: """Return Sun-body and observer-body distances (km). Port of RSPK_BodyRanges. Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). body_id: SPICE body ID. Returns: Tuple of (sundist, observer_dist) in km. """ obs_pv = observer_state(et) body_dpv, dt = spkapp_shifted(body_id, et, 'J2000', obs_pv, 'LT') body_time = et - dt body_pv = cspyce.spkssb(body_id, body_time, 'J2000') sun_dpv, _ = cspyce.spkapp(SUN_ID, body_time, 'J2000', body_pv[:6], 'LT+S') sun_dist = cspyce.vnorm(sun_dpv[:3]) obs_dist = cspyce.vnorm(body_dpv[:3]) return (sun_dist, obs_dist)
[docs] def planet_phase(et: float) -> float: """Return solar phase angle of the planet as seen from the observer (radians). Port of RSPK_Phase. Works for arbitrary observers (Earth or spacecraft). Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). Returns: Phase angle in radians. """ state = get_state() obs_pv = observer_state(et) planet_dpv, dt = cspyce.spkapp(state.planet_id, et, 'J2000', obs_pv[:6].tolist(), 'LT') planet_time = et - dt planet_pv = cspyce.spkssb(state.planet_id, planet_time, 'J2000') sun_dpv, _ = cspyce.spkapp(SUN_ID, planet_time, 'J2000', planet_pv[:6], 'LT') obs_dp = cspyce.vminus(planet_dpv[:3]) return cspyce.vsep(sun_dpv[:3], obs_dp)
[docs] def planet_ranges(et: float, *, planet_id: int | None = None) -> tuple[float, float]: """Return Sun-planet and observer-planet distances (km). Port of RSPK_Ranges. Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). planet_id: Optional SPICE body ID for the planet. When omitted, uses get_state().planet_id so existing callers are unchanged. Returns: Tuple of (sundist, observer_dist) in km. """ state = get_state() body_id = state.planet_id if planet_id is None else planet_id obs_pv = observer_state(et) planet_dpv, dt = cspyce.spkapp(body_id, et, 'J2000', obs_pv[:6].tolist(), 'LT') planet_time = et - dt planet_pv = cspyce.spkssb(body_id, planet_time, 'J2000') sun_dpv, _ = cspyce.spkapp(SUN_ID, planet_time, 'J2000', planet_pv[:6], 'LT+S') return (cspyce.vnorm(sun_dpv[:3]), cspyce.vnorm(planet_dpv[:3]))
[docs] def limb_radius(et: float) -> tuple[float, float]: """Return planet radius (km) and projected angular radius (radians). Port of RSPK_LimbRad. Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). Returns: Tuple of (rkm, rradians). """ state = get_state() obs_pv = observer_state(et) planet_dpv, _ = cspyce.spkapp(state.planet_id, et, 'J2000', obs_pv[:6].tolist(), 'LT') radii = cspyce.bodvrd(str(state.planet_id), 'RADII') rkm = radii[0] rradians = rkm / cspyce.vnorm(planet_dpv[:3]) return (rkm, rradians)
[docs] def conjunction_angle(et: float, body1_id: int, body2_id: int) -> float: """Return angular distance between two bodies as seen from the observer (radians). Port of RSPK_Conjunc. Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). body1_id: SPICE body ID of first body. body2_id: SPICE body ID of second body. Returns: Angular separation in radians. """ obs_pv = observer_state(et) b1_dpv, _ = spkapp_shifted(body1_id, et, 'J2000', obs_pv, 'LT+S') b2_dpv, _ = spkapp_shifted(body2_id, et, 'J2000', obs_pv, 'LT+S') return cspyce.vsep(b1_dpv[:3], b2_dpv[:3])
[docs] def anti_sun(et: float, body_id: int) -> tuple[float, float]: """Return anti-Sun right ascension and declination (radians). Port of RSPK_AntiSun. Returns the direction away from the Sun as seen from the body's frame, in J2000 RA/Dec. Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). body_id: SPICE body ID. Returns: Tuple of (ra, dec) in radians. """ obs_pv = observer_state(et) _planet_dpv, dt = cspyce.spkapp(body_id, et, 'J2000', obs_pv[:6].tolist(), 'LT') planet_time = et - dt planet_pv = cspyce.spkssb(body_id, planet_time, 'J2000') sun_dpv, _ = cspyce.spkapp(SUN_ID, planet_time, 'J2000', planet_pv[:6], 'LT+S') anti = [-sun_dpv[0], -sun_dpv[1], -sun_dpv[2]] _, ra, dec = cspyce.recrad(anti) return (ra, dec)
[docs] def body_latlon(et: float, body_id: int) -> tuple[float, float, float, float]: """Return sub-observer and sub-solar latitude and longitude (radians). Port of RSPK_BodyLatLon. Longitudes are from the body's prime meridian; the longitude beneath a fixed observer increases with time. Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). body_id: SPICE body ID. Returns: Tuple of (subobs_lat, subsol_lat, subobs_long, subsol_long) in radians. Raises: ValueError: If observer or Sun direction in body frame has zero length. """ state = get_state() obs_pv = observer_state(et) radii = cspyce.bodvrd(str(body_id), 'RADII') r_eq = radii[0] _body_dpv, dt = spkapp_shifted(body_id, et, 'J2000', obs_pv, 'CN') body_time = et - dt + r_eq / cspyce.clight() body_pv = cspyce.spkssb(body_id, body_time, 'J2000') sun_dpv, _ = cspyce.spkapp(SUN_ID, body_time, 'J2000', body_pv, 'CN+S') obs_id = state.obs_id if state.obs_id != 0 else EARTH_ID obs_dpv, _ = cspyce.spkapp(obs_id, body_time, 'J2000', body_pv, 'XCN+S') rotmat = bodmat(body_id, body_time) obs_dp_in_body = cspyce.mxv(rotmat, obs_dpv[:3]) n = cspyce.vnorm(obs_dp_in_body) if n < 1e-12: raise ValueError( 'Observer direction in body frame has zero length; cannot compute sub-observer lat/lon' ) subobs_lat = math.asin(max(-1.0, min(1.0, obs_dp_in_body[2] / n))) subobs_long = math.atan2(obs_dp_in_body[1], obs_dp_in_body[0]) if subobs_long < 0: subobs_long += TWOPI subobs_long = TWOPI - subobs_long sun_dp_in_body = cspyce.mxv(rotmat, sun_dpv[:3]) n = cspyce.vnorm(sun_dp_in_body) if n < 1e-12: raise ValueError( 'Sun direction in body frame has zero length; cannot compute sub-solar lat/lon' ) subsol_lat = math.asin(max(-1.0, min(1.0, sun_dp_in_body[2] / n))) subsol_long = math.atan2(sun_dp_in_body[1], sun_dp_in_body[0]) if subsol_long < 0: subsol_long += TWOPI subsol_long = TWOPI - subsol_long return (subobs_lat, subsol_lat, subobs_long, subsol_long)
[docs] def body_lonlat(et: float, body_id: int) -> tuple[float, float, float, float]: """Return sub-observer and sub-solar longitude and latitude (radians). Port of RSPK_BodyLonLat. Same as body_latlon but returns (lon, lat) order. Longitude increases with time (east for Uranus, west for others). Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). body_id: SPICE body ID. Returns: Tuple of (subobs_lon, subobs_lat, subsol_lon, subsol_lat) in radians. """ subobs_lat, subsol_lat, subobs_long, subsol_long = body_latlon(et, body_id) return (subobs_long, subobs_lat, subsol_long, subsol_lat)
[docs] def moon_tracker_offsets(et: float, moon_ids: list[int]) -> tuple[list[float], float]: """Return projected angular offsets of moons from planet axis (radians). Port of RSPK_MoonDist. Positive offsets are on the morning ansa (higher RA); negative on the evening ansa. Values apply to Earth center or observatory depending on observer setup. Parameters: et: Ephemeris time of the observation (e.g. from cspyce.utc2et). moon_ids: List of SPICE body IDs for the moons. Returns: Tuple of (list of offset angles in radians per moon, limb radius in radians). """ offsets, limb = moon_distances(et, moon_ids) return (list(offsets), float(limb))