Powered by AppSignal & Oban Pro

GNSS Positioning

examples/gnss_positioning.livemd

GNSS Positioning

Mix.install([
  {:orbis, github: "neilberkman/orbis"},
  # Optional: only needed for fetching products over HTTPS via Orbis.GnssData.
  {:req, "~> 0.5"}
])

Fetch a precise ephemeris

Orbis.GnssData resolves a product’s canonical name and archive URL from a small catalog, downloads it (HTTPS or FTP), verifies and caches it with a provenance sidecar, and decompresses it — all behind one call. Here we pull a GFZ rapid SP3 orbit product for 2020-06-24 and load it into a handle.

product = Orbis.GnssData.mgex_sp3(:gfz, ~D[2020-06-24])
{:ok, sp3} = Orbis.GnssData.sp3(product)

A second call is served from the local cache with no network access. Pass offline: true to require the cache and never touch the network.

Query satellite positions

An SP3 product samples each satellite’s position and clock on a fixed grid; Orbis.SP3.position/3 interpolates to any epoch. Positions are ITRF/IGS ECEF meters.

{:ok, state} = Orbis.SP3.position(sp3, "G01", ~N[2020-06-24 12:00:00])

radius_km =
  :math.sqrt(state.x_m ** 2 + state.y_m ** 2 + state.z_m ** 2) / 1000.0

%{position_m: {state.x_m, state.y_m, state.z_m}, clock_s: state.clock_s, radius_km: radius_km}

The radius lands near the ~26,560 km GPS orbit. Sweep an arc to see the satellite move:

base = ~N[2020-06-24 12:00:00]

for minutes <- 0..60//15 do
  epoch = NaiveDateTime.add(base, minutes * 60, :second)
  {:ok, s} = Orbis.SP3.position(sp3, "G01", epoch)
  {epoch, Float.round(s.x_m / 1000.0, 1), Float.round(s.y_m / 1000.0, 1), Float.round(s.z_m / 1000.0, 1)}
end

Broadcast navigation and single-point positioning

Broadcast products are loaded the same way. Orbis.BroadcastEphemeris parses RINEX 3.x/4.xx navigation (GPS, Galileo, BeiDou, GLONASS), and Orbis.PointPositioning.solve/4 recovers a receiver position from one epoch of pseudoranges against either an SP3 or a broadcast source:

{:ok, eph} = Orbis.GnssData.broadcast(Orbis.GnssData.mgex_nav(:igs, ~D[2020-06-25]))

# Pseudoranges (meters) come from a receiver / RINEX observation file.
observations = [{"G07", 24_602_022.18}, {"G08", 23_676_569.52}, {"E05", 27_038_058.35}]

{:ok, sol} =
  Orbis.PointPositioning.solve(eph, observations, ~N[2020-06-25 12:00:00],
    ionosphere: true,
    troposphere: true
  )

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

A mixed-constellation observation set is solved together with one receiver clock per system; the ionosphere and troposphere corrections, the elevation mask, and satellite rejection are all configurable. See Orbis.PointPositioning for the full option list and the solution fields.