Satellite-geometry and mission-planning layer above the GNSS observables: from a static receiver position and a precise (SP3) ephemeris, answer the three planning questions — which satellites are visible, how good is the geometry (dilution of precision), and when does each satellite rise and set.
This module solves no positioning problem; it reads satellite states through
Orbis.GNSS.Observables and applies standard textbook GNSS geometry.
Visibility
A satellite is visible when its topocentric elevation is at or above an
elevation mask. Azimuth and elevation come from Orbis.GNSS.Observables, which
rotates the receiver-to-satellite line of sight into the local east-north-up
(ENU) frame at the receiver's geodetic latitude/longitude.
Dilution of precision (DOP)
Dilution of precision summarises how the receiver-to-satellite geometry maps
range-measurement noise into solution uncertainty. From a design (geometry)
matrix G whose rows are the line-of-sight unit vectors plus a receiver-clock
column, and an optional diagonal weight matrix W, the cofactor matrix is
Q = (G^T W G)^-1a 4x4 symmetric matrix ordered [x, y, z, clock]. The position block is in
ECEF metres and the clock state is in the same length unit as the ranges.
Sign and column convention
Each row is [-e_x, -e_y, -e_z, 1], where e is the ECEF receiver-to-
satellite unit line of sight (the partial derivative of the predicted range
with respect to the receiver position is -e; the clock column is +1). The
geometry matrix is therefore built in ECEF, exactly as
Orbis.GNSS.Positioning builds it — the horizontal/vertical split is taken
after inverting, by rotating the 3x3 position block into the local ENU frame
at the receiver's geodetic latitude/longitude:
R = [[-sin l, cos l, 0 ],
[-sin p cos l, -sin p sin l, cos p],
[ cos p cos l, cos p sin l, sin p]]with p the geodetic latitude and l the longitude (radians); the rotated
block is Q_enu = R Q_pos R^T. The DOP scalars are then
pdop = sqrt(qE + qN + qU)(the ENU position block),hdop = sqrt(qE + qN),vdop = sqrt(qU),tdop = sqrt(Q[3][3])(the clock variance),gdop = sqrt(Q[0][0] + Q[1][1] + Q[2][2] + Q[3][3])(the cofactor trace, which is rotation invariant, so it equals the ENU-frame trace).
Weights
The default is the unweighted geometric DOP (W = I), the standard textbook
cofactor (G^T G)^-1. An elevation weighting (weights: :elevation, with
w = sin^2(elevation)) is also available; it reproduces the weighting that a
least-squares positioning solve applies, and is what lets the DOP here be
cross-checked component-for-component against Orbis.GNSS.Positioning's
reported DOP for the same satellite set and epoch.
Limitation
This is a single-receiver-clock (single-system) DOP. A mixed-constellation
geometry with one receiver clock per system (extra clock columns) is out of
scope; restrict the visible set to one system (e.g. systems: ["G"]) for a
well-posed DOP.
Passes
A pass is a contiguous interval over which a satellite stays above the mask.
Rise and set are detected by threshold-crossing on the sampled elevation, so
they are resolved only to the sampling step step_seconds: a finer step gives
finer rise/set epochs.
Summary
Functions
Dilution of precision for the visible satellites at epoch.
Per-epoch dilution of precision over a time window.
Explicit 4x4 cofactor (adjugate / determinant) inverse.
Rise / peak / set passes for each satellite over a time window.
Per-epoch count of visible satellites over a time window.
List the satellites visible from receiver at epoch, above the elevation
mask, sorted by elevation descending.
Types
Functions
@spec dop(Orbis.GNSS.SP3.t(), receiver(), NaiveDateTime.t(), keyword()) :: dop_result() | {:error, atom()}
Dilution of precision for the visible satellites at epoch.
Builds the geometry matrix from the visible satellites' ECEF line-of-sight
unit vectors (rows [-e_x, -e_y, -e_z, 1]), forms Q = (G^T W G)^-1, rotates
the position block into ENU, and returns all five DOP scalars plus the
satellite count and ids.
Options
In addition to the visible/4 options (:elevation_mask_deg, :systems):
:weights-:unit(default,W = I, the standard geometric DOP) or:elevation(w = sin^2(elevation), the least-squares weighting).:light_time- apply the light-time / Sagnac line-of-sight corrections when forming the geometry (defaultfalse, the planning value). Settrueto match a converged positioning geometry exactly.:satellites- an explicit list of satellite ids to use instead of the visibility scan (still subject to predicting successfully); useful to pin the geometry to a known set.
Returns %{gdop, pdop, hdop, vdop, tdop, n_satellites, satellites} or a
tagged error: {:error, :invalid_receiver}, {:error, :too_few_satellites}
(fewer than four usable directions), or {:error, :singular_geometry}. Never
raises.
@spec dop_series( Orbis.GNSS.SP3.t(), receiver(), {NaiveDateTime.t(), NaiveDateTime.t()}, pos_integer(), keyword() ) :: [map()] | {:error, :invalid_receiver}
Per-epoch dilution of precision over a time window.
Samples {t0, t1} (inclusive of t0, up to and including t1) every
step_seconds and computes dop/4 at each sample. Returns a list of
%{epoch, gdop, pdop, hdop, vdop, tdop, n_satellites, satellites} for the
epochs whose geometry yields a finite DOP; epochs with too few satellites or a
singular geometry are skipped. An empty or inverted window returns [].
opts are the dop/4 options. Errors from a malformed receiver propagate as
{:error, :invalid_receiver}.
Explicit 4x4 cofactor (adjugate / determinant) inverse.
a is a 4x4 matrix as a tuple of four 4-tuples. Returns {:ok, inverse} (same
shape) or :singular when the determinant is exactly zero. The (i, j)
cofactor is (-1)^(i+j) times the (i, j) minor; the inverse is the transpose
of the cofactor matrix over the determinant, so inv[j][i] = cofactor(i, j) / det.
@spec passes( Orbis.GNSS.SP3.t(), receiver(), {NaiveDateTime.t(), NaiveDateTime.t()}, pos_integer(), keyword() ) :: [map()] | {:error, :invalid_receiver}
Rise / peak / set passes for each satellite over a time window.
Samples {t0, t1} every step_seconds and, for each satellite, splits the
samples into contiguous runs above the elevation mask. Each run is one pass:
%{
satellite_id: String.t(),
rise_epoch: NaiveDateTime.t(),
set_epoch: NaiveDateTime.t(),
peak_elevation_deg: float(),
peak_epoch: NaiveDateTime.t()
}rise_epoch is the first sample above the mask and set_epoch the last; both
are resolved only to the sampling step (the true crossing lies within one
step_seconds of the reported epoch). A satellite already above the mask at
t0, or still above it at t1, yields a pass clamped to the window. opts
are the visible/4 options. A malformed receiver yields
{:error, :invalid_receiver}.
@spec visibility_series( Orbis.GNSS.SP3.t(), receiver(), {NaiveDateTime.t(), NaiveDateTime.t()}, pos_integer(), keyword() ) :: [%{epoch: NaiveDateTime.t(), n_visible: non_neg_integer()}] | {:error, :invalid_receiver}
Per-epoch count of visible satellites over a time window.
Samples {t0, t1} every step_seconds and returns a list of
%{epoch, n_visible}. An empty or inverted window returns []. opts are the
visible/4 options. A malformed receiver yields {:error, :invalid_receiver}.
@spec visible(Orbis.GNSS.SP3.t(), receiver(), NaiveDateTime.t(), keyword()) :: [visible_sat()] | {:error, :invalid_receiver}
List the satellites visible from receiver at epoch, above the elevation
mask, sorted by elevation descending.
Options
:elevation_mask_deg- minimum elevation in degrees (default5.0); a satellite is included iff its elevation is at or above this value.:systems- keep only these constellations, given as leading-letter strings (e.g.["G"]for GPS,["G", "E"]for GPS + Galileo). Default: keep all systems.
Returns a list of %{satellite_id, elevation_deg, azimuth_deg} or
{:error, :invalid_receiver} for a malformed receiver. Never raises.