Packages

Pure Elixir ephemeris computation: time scales, JPL kernel evaluation, earth orientation, and sky snapshots from an observer location.

Current section

Files

Jump to
eph_core README.md
Raw

README.md

# EphCore
Pure Elixir ephemeris computation library. Provides high-precision astronomical calculations for celestial body positions, time conversions, and coordinate transformations.
## Installation
Add `eph_core` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:eph_core, "~> 0.1.0"}
# Or from GitHub:
# {:eph_core, github: "jakedjohnson/eph_core"}
]
end
```
## Data setup
EphCore does **not** commit or package third-party ephemeris kernels, Earth
orientation tables, or star catalogs. The application **will not start** until
the baseline files below are present. Nothing under `priv/ephemeris/` or
`priv/stars/` is included in the Hex package.
From a fresh clone or a consuming Mix app, at the project root:
```bash
mix deps.get
mix eph.download_kernels
mix test
```
`mix eph.download_kernels` writes under `$PWD/priv`. Runtime uses that
directory when the baseline files are there; otherwise it uses the compiled
`:eph_core` priv directory. Set `:kernel_base_dir` when the process working
directory is not the app root. Hipparcos stars are optional and are not
required to boot or to run the core tests.
### Baseline ephemeris data
```bash
mix eph.download_kernels
```
This downloads into `$PWD/priv/ephemeris/` by default (~32 MB total):
| File | Relative path | Source | Approx. size |
|------|---------------|--------|--------------|
| `de440s.bsp` | `ephemeris/spk/de440s.bsp` | NASA JPL / NAIF | ~31 MB |
| `finals2000A.all` | `ephemeris/time/finals2000A.all` | IERS | ~3 MB |
| `naif0012.tls` | `ephemeris/time/naif0012.tls` | NASA JPL / NAIF | ~5 KB |
| `tab5.3a.txt` | `ephemeris/nutation/tab5.3a.txt` | IERS Conventions | ~200 KB |
| `tab5.3b.txt` | `ephemeris/nutation/tab5.3b.txt` | IERS Conventions | ~200 KB |
### Asteroid SPK kernels
Generate a small-body SPK via the JPL Horizons API:
```bash
mix eph.generate_kernel 2000001
```
Output defaults to `priv/ephemeris/spk/asteroids/2000001.bsp`. See
`mix help eph.generate_kernel` for date range and output options.
### Optional: Hipparcos fixed stars
Fixed-star features (`EphCore.Stars.*`) require the Hipparcos main catalog:
```bash
mix eph.setup_stars
```
This places `hip_main.dat` (~51 MB) at `priv/stars/hip_main.dat`. If the
download fails, fetch the file manually from the
[VizieR I/239 catalog](https://cdsarc.cds.unistra.fr/viz-bin/Cat/I/239) (ESA
Hipparcos) and copy it to that path. Core ephemeris features work without it.
### Custom data directory
`:kernel_base_dir` is **not** required for a Mix app started from its project
root after `mix eph.download_kernels`. Set it for releases, containers, Livebook
sessions whose working directory is not the repo, or any process whose cwd is
not the app root:
```elixir
config :eph_core,
kernel_base_dir: "/var/eph_core/data"
```
Both `ephemeris/` and `stars/` are resolved under this base (for example
`/var/eph_core/data/ephemeris/spk/de440s.bsp` and
`/var/eph_core/data/stars/hip_main.dat`). The Mix download tasks honor the
same config when it is already set.
### Third-party attribution
- **NASA JPL / NAIF** — planetary SPK kernels (`de440s.bsp`), leap-second kernel
(`naif0012.tls`). See [NAIF](https://naif.jpl.nasa.gov/naif/).
- **IERS** — Earth orientation parameters (`finals2000A.all`) and IAU 2000A
nutation tables. See [IERS](https://www.iers.org/) and
[IERS Conventions](https://iers-conventions.obspm.fr/).
- **ESA Hipparcos** — star catalog (`hip_main.dat`, VizieR catalog I/239). See
[Hipparcos at ESA](https://www.cosmos.esa.int/web/hipparcos/catalogues).
## Usage
```elixir
# Compute sky positions for celestial bodies
datetime = ~U[2026-02-02 12:00:00Z]
location = %{lat: 44.9778, lon: -93.2650, height: 250}
bodies = [:sun, :moon, :mars]
{:ok, observation} = EphCore.observe(datetime, location, bodies)
# Access results
observation.bodies[:sun]
# => %EphCore.SnapshotPipeline.SkyPosition{
# altitude_deg: ...,
# azimuth_deg: ...,
# topocentric_range_km: ...,
# ...
# }
```
## Supported Targets
`EphCore.observe/4` accepts the solar-system targets that are present in the
loaded SPK kernels:
- Sun, Moon, and planets: `:sun`, `:moon`, `:mercury`, `:venus`, `:mars`,
`:jupiter`, `:saturn`, `:uranus`, `:neptune`, `:pluto`
- Named asteroid kernels generated by `mix eph.generate_kernel`: `:ceres`,
`:pallas`, `:juno`, `:vesta`, `:chiron`
```elixir
# Requires priv/ephemeris/spk/asteroids/2000001.bsp
{:ok, observation} = EphCore.observe(datetime, location, [:ceres])
observation.bodies[:ceres].altitude_deg
```
Fixed stars use the Hipparcos catalog API rather than `observe/4`:
```elixir
{:ok, regulus} = EphCore.Stars.Catalog.lookup(49_669)
position =
EphCore.Stars.Position.compute(regulus, jd_tt,
observer: %{lat_deg: 44.9778, lon_deg: -93.2650, height_m: 250},
lst_deg: 120.0
)
position.altitude_deg
```
Future releases should consolidate these target types behind `EphCore.observe/4`
so callers can pass one mixed target list for the observed celestial sphere. See
[`#1`](https://github.com/jakedjohnson/eph_core/issues/1) for the planned API
direction.
## `observe/4` — Options Reference
`EphCore.SnapshotPipeline.observe(datetime, location, bodies, opts)` accepts four
top-level option keys: `:models`, `:corrections`, `:motion`, and `:geometry`. Each is a
map; any omitted sub-key falls back to the default. Invalid values fail fast with an
`{:error, changeset}` from `EphCore.SnapshotPipeline.Intent`.
| Key | Sub-key | Type | Default | Validation | What it controls |
|-----|---------|------|---------|------------|------------------|
| `models` | `delta_t` | `:iers \| :approximate` | `:iers` | enum | ΔT source for UTC→TT. Use `:approximate` only for rough work. |
| `models` | `earth_orientation` | `:gmst \| :gast` | `:gmst` | enum | Sidereal-time model; `:gast` adds the equation of equinoxes (~1″). |
| `models` | `earth` | `:wgs84` | `:wgs84` | enum | Reference ellipsoid (only WGS84 today). |
| `models` | `ecliptic_frame` | `:j2000 \| :mean_of_date \| :true_of_date` | `:true_of_date` | enum | Ecliptic reference plane; `:true_of_date` includes precession + nutation. |
| `corrections` | `precession_nutation` | `boolean` | `false` | boolean | Legacy flag; nutation is always applied in `:true_of_date`/`:mean_of_date`. Kept for back-compat. |
| `corrections` | `aberration` | `boolean` | `false` | boolean | Annual aberration (~20.5″). Shifts ecliptic longitude ±5–25″ per body. |
| `corrections` | `light_time` | `boolean` | `false` | boolean | Light-time retardation (body seen where it was τ ago). Dominant term in station-time error (~3h for Neptune). |
| `motion` | `enabled` | `boolean` | `true` | boolean | Compute longitude rate (deg/day) via central difference. |
| `motion` | `dt_minutes` | `1..1440` | `30` | integer in range | Half-window for the finite-difference rate; smaller = noisier near a station. |
| `geometry` | `ring_samples` | `0` or `8..72` | `24` | integer (0, or clamped to 8..72) | 3D ring-arc samples for frontend geometry; `0` disables. |
> **Apparent geocentric requires both flags.** `aberration: true` *and* `light_time: true`
> together enable the apparent-geocentric path (`Intent.apparent_geocentric?/1`). When set,
> each body gains `apparent_geocentric_ecliptic_longitude` / `_latitude`, and (with motion
> enabled) `apparent_geocentric_ecliptic_lon_rate_deg_per_day`. The default geocentric
> `ecliptic_longitude` and `topocentric_ecliptic_longitude` fields are always retained.
```elixir
# Default (geometric) call
EphCore.SnapshotPipeline.observe(datetime, location, bodies)
# Full apparent-geocentric call
EphCore.SnapshotPipeline.observe(datetime, location, bodies,
corrections: %{aberration: true, light_time: true},
motion: %{enabled: true, dt_minutes: 30}
)
```
## Frame Semantics — When to Use What
There are two independent axes. Callers combine them to select a frame:
- **Geocentric vs topocentric** — is the body projected from Earth's center or from the
observer's surface location? Topocentric adds diurnal parallax (largest for the Moon,
~57′; sub-arcsecond for outer planets).
- **Geometric vs apparent** — geometric is the instantaneous true position; apparent applies
**light-time retardation** (body at retarded epoch `jd_tt − τ`) and **annual aberration**
(~20.5″).
Practical rules:
- **Topocentric geometric** (default; no `corrections`): projected from the observer's
location, no light-time or aberration. Use when the observer's vantage point matters
and almanac-style timing is not required.
- **Apparent geocentric** (`corrections: %{aberration: true, light_time: true}`, no observer
subtraction): matches published almanac positions and event instants at the geocenter. Use
for timing **longitude-rate zero-crossings** (retrograde stations) or **longitude-threshold
crossings**, where a near-zero rate amplifies a small position difference into a multi-hour
timing error.
- **Alt/az and horizon events** (rise/set/transit): use the
`EphCore.Events.Almanac` / `EphCore.Corrections.ApparentPlace` path, which is separate from
this snapshot pipeline.
All positions are true-of-date (precession + IAU 2000A nutation) by default. Nutation is
always applied for `:true_of_date`; the `precession_nutation` correction flag is legacy.
## Compute-Cost Guidance
Relative to the default geometric snapshot (1 SPK evaluation per body per timestamp):
- `light_time: true` — +2 SPK re-evaluations per body (2 light-time iterations; range barely
changes after the first). For a 10-body snapshot: ~+20 SPK calls, negligible wall time.
- `aberration: true` — +2 SPK evaluations for Earth velocity (central difference, shared
across all bodies and cached per frame), plus one vector op per body. Negligible.
- `motion: true` (default) — +2 SPK evaluations per body at `t ± dt_minutes`. With apparent
enabled, each of those `±t` evaluations also runs the apparent-place pass, so motion +
apparent ≈ 3× the SPK calls vs geometric motion alone. Station bisection calls the probe
~50–60 times, so benchmark before shipping a hot path.
`EphCore.Corrections.ApparentPlace.frame/2` caches per-timestamp shared work and accepts a
reused `:nutation` / `:earth_velocity` — reuse it across bodies at the same instant.
## Worked Example — Neptune Retrograde Station (2026-07-07)
Near a station the longitude rate ≈ 0, so a fixed position offset of ε degrees shifts the
zero-crossing time by ε / (dω/dt) — which is *hours* for an outer planet. That is why the
choice of frame moves the station time by ~4.9h even though the positions differ by arcseconds.
| Frame | Swiss Ephemeris | Skyfield (DE440s) | eph_core |
|-------|--------------------|-------------------|----------|
| **apparent geocentric** (almanac standard) | **10:54:37Z** | **10:54:57Z** | ~10:54Z (`corrections: apparent`) |
| geometric geocentric | 07:33:39Z | 06:45:25Z | 08:06:05Z |
| geometric topocentric | 05:56:25Z | 05:46:29Z | 06:01:45Z (default) |
```elixir
# Default (geometric topocentric) — ~4h53m early vs almanacs
{:ok, snap} = EphCore.SnapshotPipeline.observe(dt, minneapolis, [:neptune],
motion: %{enabled: true, dt_minutes: 30})
snap.solar_system_positions[:neptune].ecliptic_longitude
# geometric longitude; drives the ~06:01:45Z station in the default path
# Apparent geocentric — matches published almanacs (~10:54Z)
{:ok, snap} = EphCore.SnapshotPipeline.observe(dt, minneapolis, [:neptune],
corrections: %{aberration: true, light_time: true},
motion: %{enabled: true, dt_minutes: 30})
snap.solar_system_positions[:neptune].apparent_geocentric_ecliptic_longitude
snap.motion[:neptune].apparent_geocentric_ecliptic_lon_rate_deg_per_day
```
Rule of thumb: event-timing detectors (stations, ingresses) should consume the
apparent-geocentric rate, while "where is it in the sky from here" answers stay
topocentric geometric.
## Documentation
### Livebook tours
Open any `.livemd` in [Livebook](https://livebook.dev/) (Desktop is fine). Each notebook
puts `Mix.install` in Livebook’s **setup cell** (top of the file, before sections),
uses the local `eph_core` path as a normal runtime dependency, and loads `de440s.bsp` only. Run `mix eph.download_kernels` in the
repo before evaluating a setup cell — EphCore will not start without baseline files.
After editing a notebook on disk, re-evaluate changed cells in Livebook (or close and
re-open the session) to pick up updates.
| Notebook | What it teaches |
|----------|-----------------|
| [Getting Started](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/getting_started.livemd) | `observe/4` lab: sky table, frame choices, and city fan-out |
| [Snapshot Pipeline](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/01_observe_pipeline/01_snapshot_pipeline.livemd) | Stage-by-stage walk of `observe/4``%Observation{}` |
| [Time & Sidereal](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/01_observe_pipeline/02_time_sidereal.livemd) | UTC → TAI → TT → UT1 → GMST/GAST → LMST/LAST |
| [Kernels & Chebyshev](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/01_observe_pipeline/03_kernels_chebyshev.livemd) | DAF → ETS → Type 2 → Clenshaw; NAIF IDs; raw SPK queries |
| [Celestial Geometry](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/01_observe_pipeline/04_celestial_geometry.livemd) | Geodetic/ECEF, ecliptic frames, projection, the celestial sphere |
| [Apparent Place](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/01_observe_pipeline/05_apparent_place.livemd) | Light-time + annual aberration; geometric vs apparent; parallax |
| [Time Series](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/01_observe_pipeline/06_time_series.livemd) | Grid compute + horizon geometry (charts / event searches) |
| [Almanac](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/02_applications/01_almanac.livemd) | Rise/transit/set fan-out (`EphCore.Events.Almanac`) |
| [Skyfield Snapshot](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/02_applications/02_skyfield_snapshot.livemd) | One-shot EphCore vs Skyfield (Pythonx): timing + like-for-like diffs |
| [Fixed Stars](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/02_applications/03_fixed_stars.livemd) | Hipparcos catalog, proper motion, alt/az sky board (`EphCore.Stars`) |
| [Retrograde & Stations](https://github.com/jakedjohnson/eph_core/blob/main/notebooks/02_applications/04_retrograde_stations.livemd) | Retrograde loops, station finding, topocentric vs geocentric rate |
## Contributing
Sanity-check tools for validating against external sources (Skyfield, IERS) live in `dev/tasks/`. They are compiled only in the `:dev` and `:test` environments and are not part of the published package.
When changing public APIs or the modules behind them, update the relevant
Livebook tours in `notebooks/` in the same change. The tours should call
EphCore's public functions wherever possible rather than reimplementing the
library logic inside notebook cells; this keeps notebook breakage visible when
the dependency API changes.
To run sanity checks:
```bash
# Requires python3 + skyfield installed
mix eph.sky_position_sanity_check
mix eph.time_conversion_sanity_check
```
## License
MIT