# ssapy_toolkit/Orbital_Mechanics/all_orbit_quantities.py
import numpy as np
from astropy.time import Time
from ssapy import Orbit
from ssapy_toolkit.constants import EARTH_MU
from ssapy_toolkit.Orbital_Mechanics.keplerian import true_anomaly # mean/eccentric -> true anomaly [3]
[docs]
def all_orbital_quantities(
r=None,
v=None,
a=None,
e=None,
i=None,
pa=None,
raan=None,
ta=None,
ma=None,
periapsis=None,
apoapsis=None,
t=None,
mu=EARTH_MU,
):
"""
Compute a consistent set of orbital quantities, aligned with SSAPy conventions.
SSAPy Keplerian initialization order:
Orbit.fromKeplerianElements(a, e, i, pa, raan, trueAnomaly, t, mu) [6]
Priority order if conflicting inputs are provided:
1) (r, v)
2) (periapsis, apoapsis)
3) (a, e)
Notes:
- Angles default to 0 if not supplied (i, raan, pa, ta).
- If ma is provided without ta, we convert ma->ta using ssapy_toolkit.keplerian.true_anomaly [3].
- We prefer SSAPy-computed attributes (period, meanMotion, anomalies, etc.) when available [6].
"""
if t is None:
t = Time.now().gps
t = float(t)
mu = float(mu)
# ----------------------------
# Case 1: Cartesian state
# ----------------------------
if r is not None and v is not None:
r = np.asarray(r, dtype=float).reshape(3)
v = np.asarray(v, dtype=float).reshape(3)
orbit = Orbit(r=r, v=v, t=t, mu=mu)
return _extract_all_elements_ssapy(orbit)
# Defaults for angles
i = 0.0 if i is None else float(i)
raan = 0.0 if raan is None else float(raan)
pa = 0.0 if pa is None else float(pa)
# ----------------------------
# Case 2: periapsis/apoapsis
# ----------------------------
if periapsis is not None and apoapsis is not None:
periapsis = float(periapsis)
apoapsis = float(apoapsis)
a = 0.5 * (periapsis + apoapsis)
e = (apoapsis - periapsis) / (apoapsis + periapsis)
ta = _resolve_true_anomaly(ta=ta, ma=ma, e=e)
orbit = Orbit.fromKeplerianElements(a, e, i, pa, raan, ta, t, mu) # [6]
return _extract_all_elements_ssapy(orbit)
# ----------------------------
# Case 3: Keplerian a/e
# ----------------------------
if a is not None and e is not None:
a = float(a)
e = float(e)
ta = _resolve_true_anomaly(ta=ta, ma=ma, e=e)
orbit = Orbit.fromKeplerianElements(a, e, i, pa, raan, ta, t, mu) # [6]
return _extract_all_elements_ssapy(orbit)
raise ValueError(
"Insufficient parameters provided. Need either:\n"
" 1) Position (r) and velocity (v) vectors, or\n"
" 2) Periapsis and apoapsis distances, or\n"
" 3) Semi-major axis (a) and eccentricity (e)\n"
"Angular elements (i, raan, pa, ta) default to 0 if not provided."
)
def _resolve_true_anomaly(*, ta, ma, e):
"""Pick/compute true anomaly; default to 0. Uses yu keplerian.true_anomaly for ma->ta [3]."""
if ta is not None:
return float(ta)
if ma is None:
return 0.0
# yu keplerian approximation supports eccentricity + mean anomaly [3]
ta_val = true_anomaly(eccentricity=float(e), mean_anomaly=float(ma))
if ta_val is None:
raise ValueError("Could not compute true anomaly from mean anomaly with provided inputs.")
return float(ta_val)
def _extract_all_elements_ssapy(orbit: Orbit):
"""
Return a dictionary that is as consistent with SSAPy as possible:
- Use SSAPy naming (pa, raan, trueAnomaly/meanAnomaly/eccentricAnomaly, meanMotion, period) [6]
- Keep r,v,t,mu and add common derived scalars (rp, ra).
"""
a_val, e_val, i_val, pa_val, raan_val, ta_val = orbit.keplerianElements # [6]
# Prefer SSAPy properties [6]
out = {
# epoch/state
"t": float(orbit.t),
"mu": float(orbit.mu),
"r": np.asarray(orbit.r, dtype=float),
"v": np.asarray(orbit.v, dtype=float),
# classical Keplerian elements (SSAPy order/names) [6]
"a": float(a_val),
"e": float(e_val),
"i": float(i_val),
"pa": float(pa_val),
"raan": float(raan_val),
"trueAnomaly": float(ta_val),
# SSAPy anomaly fields (when defined) [6]
"eccentricAnomaly": _safe_float(getattr(orbit, "eccentricAnomaly", np.nan)),
"meanAnomaly": _safe_float(getattr(orbit, "meanAnomaly", np.nan)),
# SSAPy derived fields (when defined) [6]
"period": _safe_float(getattr(orbit, "period", np.inf)),
"meanMotion": _safe_float(getattr(orbit, "meanMotion", 0.0)),
"p": _safe_float(getattr(orbit, "p", np.nan)),
"angularMomentum": np.asarray(getattr(orbit, "angularMomentum", np.full(3, np.nan)), dtype=float),
"energy": _safe_float(getattr(orbit, "energy", np.nan)),
"LRL": np.asarray(getattr(orbit, "LRL", np.full(3, np.nan)), dtype=float),
# SSAPy periapsis/apoapsis coordinates (vectors) [6]
"periapsis": np.asarray(getattr(orbit, "periapsis", np.full(3, np.nan)), dtype=float),
"apoapsis": np.asarray(getattr(orbit, "apoapsis", np.full(3, np.nan)), dtype=float),
}
# Common scalar distances (rp/ra) derived from a,e when meaningful
if np.isfinite(out["a"]) and out["e"] < 1.0:
out["rp"] = out["a"] * (1.0 - out["e"])
out["ra"] = out["a"] * (1.0 + out["e"])
else:
out["rp"] = np.nan
out["ra"] = np.inf
return out
def _safe_float(x):
try:
return float(x)
except Exception:
return np.nan