Orbis.GNSS.Geometry (Orbis v0.9.1)

Copy Markdown View Source

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)^-1

a 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

dop_result()

@type dop_result() :: %{
  gdop: float(),
  pdop: float(),
  hdop: float(),
  vdop: float(),
  tdop: float(),
  n_satellites: non_neg_integer(),
  satellites: [String.t()]
}

receiver()

@type receiver() ::
  {number(), number(), number()}
  | %{x_m: number(), y_m: number(), z_m: number()}

visible_sat()

@type visible_sat() :: %{
  satellite_id: String.t(),
  elevation_deg: float(),
  azimuth_deg: float()
}

Functions

dop(sp3, receiver, epoch, opts \\ [])

@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 (default false, the planning value). Set true to 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.

dop_series(sp3, receiver, arg, step_seconds, opts \\ [])

@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}.

inv4(a)

@spec inv4(tuple()) :: {:ok, tuple()} | :singular

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.

passes(sp3, receiver, arg, step_seconds, opts \\ [])

@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}.

visibility_series(sp3, receiver, arg, step_seconds, opts \\ [])

@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}.

visible(sp3, receiver, epoch, opts \\ [])

@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 (default 5.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.