"""Empyrean: high-fidelity orbital mechanics for Python."""
import pathlib
from empyrean import io
# ── Type re-exports (organized by subpackage) ────────────────
from empyrean.coordinates.coordinates import (
CartesianCoordinates,
CometaryCoordinates,
KeplerianCoordinates,
SphericalCoordinates,
)
from empyrean.coordinates.covariance import (
CartesianCovariance,
CometaryCovariance,
KeplerianCovariance,
SphericalCovariance,
)
from empyrean.coordinates.enums import Frame, Origin
from empyrean.coordinates.epoch import Epochs, TimeScale
# ── Function re-exports (organized by subpackage) ────────────
from empyrean.coordinates.transform import transform_coordinates
from empyrean.ephemeris.generate import generate_ephemeris
from empyrean.ephemeris.result import Ephemeris, EphemerisConfig, EphemerisResult
from empyrean.ephemeris.sensitivity import (
ObservationSensitivities,
StateSensitivities,
)
from empyrean.impact import (
BPlanes,
ImpactProbabilities,
compute_b_planes,
compute_impact_probabilities,
)
from empyrean.io.horizons import query_horizons, query_horizons_vectors
from empyrean.io.observations import query_observations, query_radar
from empyrean.io.sbdb import query_sbdb
from empyrean.math import MixtureComponent, eigenvector_max_6x6, split_gaussian
from empyrean.observers.observers import Observers
from empyrean.observers.state import get_observer_states
from empyrean.od.ades_observations import ADESObservations
from empyrean.od.determine import determine, evaluate, read_ades, refine
from empyrean.od.radar_observations import ADESRadarObservations
from empyrean.od.residuals import (
AcceptabilityReport,
ObservationResults,
ResidualSummary,
StationBiases,
)
from empyrean.od.result import (
AcceptabilityThresholds,
AutoEscalationPolicy,
CovarianceRepresentation,
DebiasingConfig,
DebiasingResolution,
DetermineResult,
EvaluateResult,
IODConfig,
ODConfig,
OriginPolicy,
OriginPolicyMode,
OutputEpoch,
OutputEpochMode,
RejectionConfig,
RejectionKind,
SigmaPolicy,
SolveForParams,
StationRaDecConfig,
WeightingConfig,
WeightingLayer,
WeightingLayerKind,
WeightingPreset,
)
from empyrean.od.session import Session, SessionDiff
from empyrean.orbits.nongrav import NonGravParams
from empyrean.orbits.orbits import (
CartesianOrbits,
CometaryOrbits,
KeplerianOrbits,
SphericalOrbits,
)
from empyrean.orbits.photometry import PhotometricParams
from empyrean.propagation.config import (
AdvancedIntegratorConfig,
DiagnosticsConfig,
ForceModelTier,
IntegratorChoice,
MonteCarlo,
OriginSwitchingConfig,
PropagationConfig,
SigmaPoint,
UncertaintyMethod,
)
from empyrean.propagation.events import (
AtmosphericEntries,
AtmosphericExits,
CaptureEnds,
CaptureStarts,
CloseApproachEnds,
CloseApproachStarts,
CovarianceRegimeChanges,
EventConfig,
Events,
EventSummary,
Impacts,
Periapses,
PossibleImpacts,
ShadowEntries,
ShadowExits,
)
from empyrean.propagation.propagate import propagate
from empyrean.propagation.result import PropagationResult
from empyrean.propagation.tagged_covariance import (
CovarianceKind,
CovarianceQuality,
TaggedCovariance,
TaggedCovariances,
TargetFunctional,
)
from empyrean.states import get_states
def version_string() -> str:
"""Return the multi-line version report for the empyrean stack.
Format::
empyrean-core <ver>
villeneuve <ver>
scott <ver>
nolan <ver>
Where each upstream version is the git-populated ``<tag>+<sha>``
string baked into the cdylib at build time. Use this for build-
provenance reporting in logs / crash dumps / `--version`-style
output.
Returns
-------
str
Multi-line version report.
"""
from empyrean._empyrean_rs import _version_string
result: str = _version_string()
return result
def versions() -> dict[str, str]:
"""Return per-crate versions of the empyrean stack.
Returns
-------
dict[str, str]
Mapping of crate name (``empyrean_core`` / ``villeneuve`` /
``scott`` / ``nolan``) to its version string. ``empyrean_core``
is its semver from ``Cargo.toml``; the upstream physics crates
carry git-populated ``<tag>+<sha>`` strings.
"""
from empyrean._empyrean_rs import _versions
core, villeneuve, scott, nolan = _versions()
return {
"empyrean_core": core,
"villeneuve": villeneuve,
"scott": scott,
"nolan": nolan,
}
[docs]
def default_data_dir() -> pathlib.Path:
"""Return the OS-appropriate XDG data directory empyrean uses by default.
Resolution order:
1. ``EMPYREAN_DATA_DIR`` environment variable, if set.
2. The OS XDG data location:
- Linux: ``$XDG_DATA_HOME/empyrean/data/`` (default
``~/.local/share/empyrean/data/``)
- macOS: ``~/Library/Application Support/empyrean/data/``
- Windows: ``%APPDATA%\\empyrean\\data\\``
Cheap to call — does not touch the filesystem.
Returns
-------
pathlib.Path
Path to the data directory.
"""
from pathlib import Path
from empyrean._empyrean_rs import _default_data_dir
return Path(_default_data_dir())
def _bundled_gm_path() -> str:
"""Return the path to the gm_de440.tpc bundled inside the wheel."""
from importlib.resources import files
# `joinpath` on `Traversable` only accepts a single child segment per
# call (despite the `MultiplexedPath` overload accepting varargs); chain
# to compose the relative path portably.
return str(files("empyrean").joinpath("_data").joinpath("gm_de440.tpc"))
def _discover_b612_data() -> dict[str, str]:
"""Detect B612 Foundation SPICE kernel pip packages and return paths.
Returns a dict mapping a stable kernel name to the file path of
every detected package. Empty dict if none are installed.
"""
paths: dict[str, str] = {}
try:
import naif_de440
paths["de440"] = naif_de440.de440
except ImportError:
pass
try:
import jpl_small_bodies_de441_n16
paths["sb441_n16"] = jpl_small_bodies_de441_n16.de441_n16
except ImportError:
pass
try:
import naif_eop_high_prec
paths["earth_high_prec"] = naif_eop_high_prec.eop_high_prec
except ImportError:
pass
try:
import naif_eop_historical
paths["earth_historical"] = naif_eop_historical.eop_historical
except ImportError:
pass
try:
import naif_eop_predict
paths["earth_predict"] = naif_eop_predict.eop_predict
except ImportError:
pass
try:
import mpc_obscodes
paths["mpc_obscodes"] = mpc_obscodes.mpc_obscodes
except ImportError:
pass
return paths
# Maps B612 kernel name → filename expected by villeneuve's DataManager.
# See villeneuve/src/data.rs for the authoritative filename list.
_B612_TO_VILLENEUVE_FILENAME = {
"de440": "de440.bsp",
"sb441_n16": "sb441-n16.bsp",
"earth_high_prec": "earth_latest_high_prec.bpc",
"earth_historical": "earth_620120_250826.bpc",
"earth_predict": "earth_2025_250826_2125_predict.bpc",
"mpc_obscodes": "obscodes_extended.json",
}
def _stage_b612_cache(b612: dict[str, str]) -> pathlib.Path:
"""Stage B612-provided kernel symlinks inside the platform data directory.
Links each B612-provided kernel into villeneuve's XDG-compliant
data directory (``~/Library/Application Support/empyrean/data/`` on
macOS, ``~/.local/share/empyrean/data/`` on Linux, ``%APPDATA%\\empyrean\\data\\``
on Windows) under the filename villeneuve expects, so the SPICE /
asteroid / Earth-orientation kernels shipped by the B612 PyPI
packages are reused without redownload.
Linking *into* the canonical data directory (not a sibling
``b612-cache/``) keeps villeneuve and scott in agreement: villeneuve
downloads anything missing — ``bias.dat`` is the practical case —
next to the symlinks, and scott's catalog-debiasing loader
(``DataManager::new().data_dir()``) finds the bias table at the same
XDG default. Honors ``EMPYREAN_DATA_DIR`` via the same logic
:func:`Context.from_data_dir(None) <Context.from_data_dir>` uses.
Existing real files at a target path take precedence — only stale
symlinks are replaced, so a user who already downloaded a fresh
kernel does not have it silently swapped for the (possibly older)
version that ships with a B612 release.
Returns the data directory path.
"""
from pathlib import Path
from empyrean._empyrean_rs import _default_data_dir
cache = Path(_default_data_dir())
cache.mkdir(parents=True, exist_ok=True)
def _link_if_safe(target: Path, link: Path) -> None:
# Replace stale symlinks (e.g. when a B612 package updated and
# the previous version was unlinked from site-packages) but
# never overwrite a real file the user fetched themselves.
if link.is_symlink():
link.unlink()
elif link.exists():
return
link.symlink_to(target)
for key, filename in _B612_TO_VILLENEUVE_FILENAME.items():
if key not in b612:
continue
_link_if_safe(Path(b612[key]), cache / filename)
# Bundled gm_de440.tpc (not available from B612)
gm_src = Path(_bundled_gm_path())
if gm_src.exists():
_link_if_safe(gm_src, cache / "gm_de440.tpc")
return cache
[docs]
def initialize(
*,
data_dir: str | pathlib.Path | None = None,
de440_path: str | pathlib.Path | None = None,
gm_path: str | pathlib.Path | None = None,
) -> None:
"""Initialize empyrean with SPICE kernel data.
On first call, loads ephemeris data into a global context. Subsequent
calls are no-ops.
If the B612 Foundation data packages (``naif-de440``,
``jpl-small-bodies-de441-n16``, ``naif-eop-high-prec``,
``naif-eop-historical``, ``naif-eop-predict``, ``mpc-obscodes``) are
installed and no explicit paths are provided, empyrean stages a
symlinked cache under the platform XDG data directory
(``$XDG_DATA_HOME/empyrean/b612-cache/`` on Linux,
``~/Library/Application Support/empyrean/b612-cache/`` on macOS,
``%APPDATA%\\empyrean\\b612-cache\\`` on Windows; honors
``EMPYREAN_DATA_DIR``) and uses that as the data directory — zero
network access required. Falls back to ``data_dir`` (default: same
XDG location, ``data/`` instead of ``b612-cache/``) plus
:func:`download_data` otherwise.
Parameters
----------
data_dir : str, optional
Directory containing kernel files. Overrides B612 detection.
de440_path : str, optional
Explicit path to ``de440.bsp``. Overrides B612 detection.
gm_path : str, optional
Explicit path to ``gm_de440.tpc``.
"""
from empyrean._empyrean_rs import _initialize
if data_dir is None and de440_path is None:
b612 = _discover_b612_data()
if b612:
data_dir = str(_stage_b612_cache(b612))
_initialize(data_dir=data_dir, de440_path=de440_path, gm_path=gm_path)
[docs]
def download_data(*, data_dir: str | pathlib.Path | None = None) -> str:
"""Download all required SPICE kernel files.
Downloads to the OS-appropriate XDG data directory by default
(see :func:`default_data_dir`). Skips files that are already
present and up-to-date.
Parameters
----------
data_dir : str, optional
Target directory. Defaults to the value returned by
:func:`default_data_dir` (honors ``EMPYREAN_DATA_DIR``).
Returns
-------
str
Path to the data directory.
"""
from empyrean._empyrean_rs import _download_data
result: str = _download_data(data_dir=data_dir)
return result