Hex.pm Hexdocs.pm CI

Orbis

Full-featured satellite toolkit for Elixir.

SGP4/SDP4, coordinate transforms, GNSS positioning, orbit determination, and the other precise numerics run in a Rust NIF — much of it verified to 0 ULP (bit-exact) parity against reference implementations. The orchestration layer — pass prediction, conjunction assessment, constellation management, real-time tracking, and batch analysis — is pure Elixir (and Nx for GPU workloads).

Try it in Livebook

Run in Livebook

Features

CategoryWhat it does
PropagationSGP4/SDP4 via the sgp4 Rust crate (Rust NIF)
Coordinate transformsTEME, GCRS, ITRS, geodetic, topocentric — 0 ULP Skyfield parity (Rust NIF)
Ground stationPass prediction, look angles, Doppler shift, RF link budget
Orbit determinationGibbs, Herrick-Gibbs, Gauss angles-only, Lambert/Battin (Rust NIF)
Conjunction assessmentClosest approach finder validated against the Iridium 33 / Cosmos 2251 collision
Eclipse predictionSunlit / penumbra / umbra with shadow fraction
Atmospheric densityNRLMSISE-00 model, surface to ~1000 km (Rust NIF)
JPL ephemerisSPK/BSP reader for Sun, Moon, planets (Rust NIF)
GNSS positioningSingle-point positioning from SP3 or broadcast ephemeris — GPS, Galileo, BeiDou, GLONASS — with Klobuchar/Saastamoinen–Niell corrections and DOP (Rust NIF)
GNSS ephemeris & dataSP3 precise products, RINEX 3.x/4.xx broadcast navigation, GNSS constellation catalogs, and optional SP3/CLK/NAV/IONEX fetch/cache from public archives
GNSS observationsRINEX 3 observation parsing with Hatanaka (CRINEX) decoding — raw observation values, carrier phases, pseudoranges, and station positioning from .crx/.rnx files (Rust NIF)
Reduced orbitCompact fitted mean-element model (circular or eccentric) for fast approximate position, caching, and transport — with source-backed drift reporting (Rust NIF)
GNSS measurements & QCPredicted observables (range, range-rate, Doppler, az/el), receiver velocity from Doppler, RAIM fault detection + FDE, dilution of precision & visibility, carrier-phase combinations / slip detection / Hatch smoothing, dual-frequency ionosphere-free combination, and code-differential (DGNSS) positioning
GNSS signalsGPS L1 C/A Gold-code generation, correlation, and acquisition; coherent-integration loss; LNAV navigation-message subframe synthesis and decoding
Live dataCelesTrak TLE/OMM fetching, constellation loading, name search
Real-time trackingGenServer with PubSub-compatible broadcasts
RF primitivesFSPL, EIRP, C/N₀, link margin, dish gain
Batch analysisNx-powered tensorized geometry, visibility, and RF (GPU-ready via EXLA/Torchx)
FormatsOrbis.Elements with TLE and OMM parsers/encoders

Installation

def deps do
  [{:orbis, "~> 0.9.1"}]
end

Release packages that include matching GitHub precompiled-NIF assets and checksum-Elixir.Orbis.NIF.exs download the Rust NIF for common Linux, macOS, and Windows targets. Development builds, source releases without a checksum, and ORBIS_BUILD=1 builds compile the NIF locally from Rust. GNSS product fetching (Orbis.GNSS.Data) additionally needs the optional :req dependency:

{:req, "~> 0.5"}

Quick Start

# Fetch the ISS TLE from CelesTrak
{:ok, [iss]} = Orbis.CelesTrak.fetch_tle(25544)

# Propagate to now
{:ok, teme} = Orbis.propagate(iss, DateTime.utc_now())
teme.position   # {x, y, z} km
teme.velocity   # {vx, vy, vz} km/s

# Where is it over the Earth?
{:ok, geo} = Orbis.geodetic(iss, DateTime.utc_now())
# %{latitude: 23.4, longitude: -45.6, altitude_km: 420.1}

Usage

Parse from TLE or OMM

# Two-Line Element format
{:ok, elements} = Orbis.Format.TLE.parse(line1, line2)

# OMM JSON (CelesTrak / Space-Track)
{:ok, elements} = Orbis.Format.OMM.parse(omm_json_map)

# Encode back to either format
{line1, line2} = Orbis.Format.TLE.encode(elements)
omm_map = Orbis.Format.OMM.encode(elements)

The Orbis.Elements struct is format-agnostic — parse from any source, serialize to any format.

Coordinate Transforms

gcrs = Orbis.teme_to_gcrs(teme, datetime)

station = %{latitude: 40.7128, longitude: -74.006, altitude_m: 10.0}
{:ok, geo} = Orbis.geodetic(elements, datetime)
{:ok, look} = Orbis.look_angle(elements, datetime, station)
# %{azimuth: 359.6, elevation: -41.9, range_km: 9130.5}

The GCRS transform includes IAU2000A nutation (1365 terms), IAU2006 precession, frame bias, and time scale conversions (UTC→TAI→TT→TDB→UT1).

Constellation Management

{:ok, constellation} = Orbis.Constellation.load("globalstar")
constellation.count  #=> 85

# Propagate all satellites in parallel
positions = Orbis.Constellation.propagate_all(constellation, DateTime.utc_now())

# Find visible satellites from a ground station
visible = Orbis.Constellation.visible_from(constellation, station, datetime)

Real-Time Tracking

{:ok, tracker} = Orbis.Tracker.start_link(elements, interval_ms: 1000)
Orbis.Tracker.subscribe(tracker)

receive do
  {:orbis_tracker, _pid, state} ->
    IO.puts("#{state.geodetic.latitude}, #{state.geodetic.longitude}")
end

Pass Prediction

passes = Orbis.Passes.predict(elements, station,
  ~U[2024-01-01 00:00:00Z], ~U[2024-01-02 00:00:00Z])

for pass <- passes do
  IO.puts("Rise: #{pass.rise} | Max el: #{pass.max_elevation}° | Set: #{pass.set}")
end

Conjunction Assessment

approaches = Orbis.Conjunction.find(elements1, elements2,
  end_min: 2880.0, step_min: 1.0, threshold_km: 50.0)

for {tca_min, distance_km} <- approaches do
  IO.puts("TCA: +#{Float.round(tca_min / 60, 1)}h, miss: #{Float.round(distance_km, 2)} km")
end
# Path loss from slant range
fspl = Orbis.RF.fspl(look.range_km, 1616.0)  # MHz

# Full link margin
margin = Orbis.RF.link_margin(%{
  eirp_dbw: Orbis.RF.eirp(27.0, 3.0),
  fspl_db: fspl,
  receiver_gt_dbk: -12.0,
  other_losses_db: 3.0,
  required_cn0_dbhz: 35.0
})

Orbit Determination

# Gibbs: 3 position vectors → velocity
{v2, theta12, theta23, copa} = Orbis.IOD.gibbs(r1, r2, r3)

# Gauss: 3 angular observations → full orbit
{r2, v2} = Orbis.IOD.gauss(
  decl1, decl2, decl3, rtasc1, rtasc2, rtasc3,
  jd1, jdf1, jd2, jdf2, jd3, jdf3,
  site1, site2, site3)

# Lambert: transfer orbit between two positions
{v1t, v2t} = Orbis.Lambert.solve(r1, r2, v1, dm, de, nrev, dtsec)

Eclipse & Ephemeris

eph = Orbis.Ephemeris.load("/path/to/de421.bsp")

{:ok, status} = Orbis.Eclipse.check(elements, datetime, eph)
# :sunlit | :penumbra | :umbra

mars = Orbis.Ephemeris.position(eph, :mars, :earth, datetime)

GNSS Positioning

GNSS-specific APIs are grouped under Orbis.GNSS.*.

# Precise ephemeris (SP3): interpolate a satellite's position/clock at any epoch
sp3 = Orbis.GNSS.SP3.load!("GBM0MGXRAP_20201760000_01D_05M_ORB.SP3")
{:ok, state} = Orbis.GNSS.SP3.position(sp3, "G01", ~N[2020-06-24 00:00:00])
# %Orbis.GNSS.SP3.State{x_m: ..., y_m: ..., z_m: ..., clock_s: ...}

# Or broadcast navigation — GPS, Galileo, BeiDou, GLONASS (RINEX 3.x/4.xx)
eph = Orbis.GNSS.Broadcast.load!("BRDC00IGS_20201770000_01D_MN.rnx")

# Single-point position from one epoch of pseudoranges
observations = [{"G07", 24_602_022.18}, {"G08", 23_676_569.52}, {"E05", 27_038_058.35}]

{:ok, sol} =
  Orbis.GNSS.Positioning.solve(eph, observations, ~N[2020-06-25 12:00:00],
    ionosphere: true,
    troposphere: true,
    klobuchar_alpha: {1.0e-8, 0.0, 0.0, 0.0},
    klobuchar_beta: {9.0e4, 0.0, 0.0, 0.0}
  )

sol.position          # %{x_m: ..., y_m: ..., z_m: ...} — ITRF ECEF meters
sol.dop.pdop          # position dilution of precision
sol.system_clocks_s   # %{"G" => ..., "E" => ...} — one receiver clock per GNSS

Products can be fetched and cached (needs the optional :req dependency):

product = Orbis.GNSS.Data.mgex_sp3(:gfz, ~D[2020-06-24])
{:ok, sp3} = Orbis.GNSS.Data.sp3(product)   # downloads, verifies, caches, loads

Parse a station's RINEX observation file (Hatanaka .crx or plain .rnx), extract pseudoranges, and recover its position:

{:ok, obs} = Orbis.GNSS.RINEX.Observations.load("STAT00DNK_R_..._MO.crx")
[%{index: i, epoch: epoch} | _] = Orbis.GNSS.RINEX.Observations.epochs(obs)
{:ok, prs} = Orbis.GNSS.RINEX.Observations.pseudoranges(obs, i, codes: %{"G" => ["C1C"]})
{:ok, sol} = Orbis.GNSS.Positioning.solve(eph, prs, epoch)

Inspect carrier-phase observables and build the standard precise-positioning combinations:

{:ok, phases} =
  Orbis.GNSS.RINEX.Observations.phases(obs, i, codes: %{"G" => ["L1C", "L2W"]})

g03 = phases["G03"]
l1 = Enum.find(g03, &(&1.code == "L1C"))
l2 = Enum.find(g03, &(&1.code == "L2W"))

geometry_free_m = Orbis.GNSS.CarrierPhase.geometry_free(l1.value_m, l2.value_m)
{:ok, mw_m} =
  Orbis.GNSS.CarrierPhase.melbourne_wubbena(
    l1.value,
    l2.value,
    24_000_000.0,
    24_000_005.0,
    l1.frequency_hz,
    l2.frequency_hz
  )

Fit a compact reduced-orbit model and check its drift against the source:

{:ok, model} =
  Orbis.GNSS.ReducedOrbit.fit(sp3,
    satellite_id: "G05",
    window: {~N[2020-06-24 00:00:00], ~N[2020-06-24 06:00:00]},
    model: :eccentric_secular
  )

{:ok, pos} = Orbis.GNSS.ReducedOrbit.position(model, ~N[2020-06-24 12:00:00])  # ECEF m
map = Orbis.GNSS.ReducedOrbit.to_map(model)   # versioned, transportable

A runnable walkthrough is in examples/gnss_positioning.livemd.

A GPS constellation catalog (PRN ↔ SVN ↔ NORAD ↔ SP3 id, active/usable flags) is built from CelesTrak and an optional NAVCEN overlay:

{:ok, records} = Orbis.GNSS.Constellation.fetch_gps()
Orbis.GNSS.Constellation.to_csv(records)         # prn,norad_cat_id,active,sp3_id

# Cross-check a catalog against the satellites a precise product actually carries
report = Orbis.GNSS.Constellation.validate_sp3(records, sp3)
Orbis.GNSS.Constellation.valid?(report)

Coordinate Frames

FrameDescription
TEMETrue Equator Mean Equinox — SGP4 output frame
GCRSGeocentric Celestial Reference System — inertial
ITRSInternational Terrestrial Reference System — Earth-fixed
GeodeticWGS84 latitude, longitude, altitude
TopocentricAzimuth, elevation, range from a ground station

Accuracy

ComponentReferenceAccuracy
TEME→GCRS→ITRSSkyfield0 ULP (bit-identical)
SGP4 propagationSkyfield< 1 mm (via sgp4 crate)
Gibbs / Herrick-GibbsVallado Python0 ULP
Gauss IODVallado Python1e-12 relative
Lambert (Battin)Vallado Python1e-12 relative
ConjunctionIridium/Cosmos 2251< 2 km miss, < 1 min timing
RF (FSPL)Analytical (inverse square law)Exact
SP3 interpolationgnssanalysis0 ULP
Broadcast orbit/clockpinned IS-GPS-200 recipe0 ULP (recipe); ~m vs SP3
Ionosphere/troposphereKlobuchar / Saastamoinen–Niell0 ULP
GNSS DOPcofactor inverse0 ULP
Single-point positionscipy least squaressub-micron agreement
RINEX / CRINEX decodeRNXCMP crx2rnxbyte-exact
Reduced orbitSP3 (drift-checked)approximate, ~km/day (see docs)

License

MIT