Orbis.GNSS.RINEX.Observations (Orbis v0.9.1)

Copy Markdown View Source

RINEX 3 observation products: parse a station's observation file, expose its header (including the surveyed APPROX POSITION XYZ), and extract the single-frequency pseudoranges that Orbis.GNSS.Positioning.solve/4 consumes.

This is the Elixir surface over the astrodynamics-gnss RINEX observation parser and its Hatanaka (CRINEX) decoder. A file is parsed once into a resource handle held by the BEAM; accessors operate on that handle and never re-read the file.

Both plain RINEX (.rnx) and Hatanaka-compressed CRINEX (.crx) text are accepted: load/1 and parse_auto/1 sniff the first line for the CRINEX VERS / TYPE marker and decode CRINEX before parsing. Gzip is handled upstream by Orbis.GNSS.Data.fetch/2, so this module only ever sees plain text or CRINEX text.

Example

{:ok, obs} = Orbis.GNSS.RINEX.Observations.load("ESBC00DNK_..._MO.crx")

Orbis.GNSS.RINEX.Observations.approx_position(obs)
# => {3_582_105.291, 532_589.7313, 5_232_754.8054}

[%{index: i, epoch: epoch} | _] = Orbis.GNSS.RINEX.Observations.epochs(obs)
{:ok, prs} = Orbis.GNSS.RINEX.Observations.pseudoranges(obs, i, codes: %{"G" => ["C1C"]})
# prs :: [{"G01", range_m}, ...] — feeds solve/4 verbatim

Default pseudorange codes

The per-system defaults are version-aware: GPS C1C, Galileo C1C then C1X, BeiDou C1I for RINEX 3.02 / C2I for 3.01 and 3.03+ (the B1I label changed between minor versions), GLONASS C1C. Override per system with the :codes option, e.g. codes: %{"G" => ["C1C"], "C" => ["C2I"]}.

Summary

Types

An epoch descriptor as returned by epochs/1.

A pseudorange observation {satellite_id, range_m}.

t()

Functions

The surveyed a-priori receiver position {x_m, y_m, z_m} (ECEF meters), or nil if the file carries no APPROX POSITION XYZ.

Carrier frequency in hertz for a system letter ("G"/"E"/"C") and a RINEX band digit (the second character of an observation code, e.g. "1" in "L1C").

Decode Hatanaka CRINEX text into the plain RINEX observation text it expands to. Returns {:ok, rinex_text} or {:error, reason}.

The epoch list as [%{index:, epoch:, flag:, sat_count:}]. The :epoch is a {{y, mo, d}, {h, mi, second_float}} tuple in the file's time scale — exactly the form Orbis.GNSS.Positioning.solve/4 accepts.

Load and parse a RINEX observation file from disk.

Like load/1 but raises on failure.

The per-constellation observation-code table as a map of system letter to the ordered code list, e.g. %{"G" => ["C1C", ...], "E" => [...]}.

Parse plain RINEX 3 observation text into a handle.

Parse text, auto-detecting plain RINEX vs CRINEX from the first line.

Decode Hatanaka CRINEX text and parse the result into a handle.

Carrier-phase observations for one epoch (the L* codes), keyed by satellite.

Extract single-frequency pseudoranges for one epoch.

Every observation value for one epoch, keyed by satellite.

Types

epoch_entry()

@type epoch_entry() :: %{
  index: non_neg_integer(),
  epoch: {{integer(), integer(), integer()}, {integer(), integer(), float()}},
  flag: 0..255,
  sat_count: non_neg_integer()
}

An epoch descriptor as returned by epochs/1.

observation()

@type observation() :: {String.t(), float()}

A pseudorange observation {satellite_id, range_m}.

t()

@type t() :: %Orbis.GNSS.RINEX.Observations{handle: reference()}

Functions

approx_position(observations)

@spec approx_position(t()) :: {float(), float(), float()} | nil

The surveyed a-priori receiver position {x_m, y_m, z_m} (ECEF meters), or nil if the file carries no APPROX POSITION XYZ.

band_frequency_hz(arg1, band)

@spec band_frequency_hz(String.t(), String.t()) :: float() | nil

Carrier frequency in hertz for a system letter ("G"/"E"/"C") and a RINEX band digit (the second character of an observation code, e.g. "1" in "L1C").

Returns nil for GLONASS (FDMA — channel-dependent) and unknown bands.

decode_crinex(text)

@spec decode_crinex(binary()) :: {:ok, String.t()} | {:error, term()}

Decode Hatanaka CRINEX text into the plain RINEX observation text it expands to. Returns {:ok, rinex_text} or {:error, reason}.

epochs(observations)

@spec epochs(t()) :: [epoch_entry()]

The epoch list as [%{index:, epoch:, flag:, sat_count:}]. The :epoch is a {{y, mo, d}, {h, mi, second_float}} tuple in the file's time scale — exactly the form Orbis.GNSS.Positioning.solve/4 accepts.

load(path)

@spec load(String.t()) :: {:ok, t()} | {:error, term()}

Load and parse a RINEX observation file from disk.

The file may be plain RINEX (.rnx) or Hatanaka CRINEX (.crx); the first line is sniffed for the CRINEX marker and decoded if present. Returns {:ok, %Orbis.GNSS.RINEX.Observations{}} or {:error, reason}.

load!(path)

@spec load!(String.t()) :: t()

Like load/1 but raises on failure.

observation_codes(observations)

@spec observation_codes(t()) :: %{required(String.t()) => [String.t()]}

The per-constellation observation-code table as a map of system letter to the ordered code list, e.g. %{"G" => ["C1C", ...], "E" => [...]}.

parse(text)

@spec parse(binary()) :: {:ok, t()} | {:error, term()}

Parse plain RINEX 3 observation text into a handle.

parse_auto(text)

@spec parse_auto(binary()) :: {:ok, t()} | {:error, term()}

Parse text, auto-detecting plain RINEX vs CRINEX from the first line.

parse_crinex(text)

@spec parse_crinex(binary()) :: {:ok, t()} | {:error, term()}

Decode Hatanaka CRINEX text and parse the result into a handle.

phases(obs, epoch, opts \\ [])

@spec phases(t(), non_neg_integer() | tuple(), keyword()) ::
  {:ok, %{required(String.t()) => [map()]}} | {:error, term()}

Carrier-phase observations for one epoch (the L* codes), keyed by satellite.

Convenience over values/3 that keeps only carrier phase and adds the wavelength and the phase expressed in metres when the carrier frequency is known for the satellite's system and band (GPS, Galileo, BeiDou). For GLONASS the FDMA wavelength depends on the satellite's frequency channel, which is not yet exposed, so wavelength_m/value_m are nil there.

Returns {:ok, %{satellite_id => [phase]}} where each phase is

%{code: "L1C", value_cycles: 1.23e8, lli: 0 | nil, ssi: 7 | nil,
  wavelength_m: 0.1903 | nil, value_m: 2.34e7 | nil}

pseudoranges(obs, epoch, opts \\ [])

@spec pseudoranges(t(), non_neg_integer() | tuple(), keyword()) ::
  {:ok, [observation()]} | {:error, term()}

Extract single-frequency pseudoranges for one epoch.

epoch is either the integer epoch index (from epochs/1) or an epoch tuple {{y, mo, d}, {h, mi, s}}, which is resolved to its index.

Without :codes, the version-aware defaults are applied across every system. When :codes is given it defines the whole policy: only the listed systems are extracted, each with its given code preference, e.g. codes: %{"G" => ["C1C"]} yields GPS-only pseudoranges and codes: %{"G" => ["C1C"], "C" => ["C2I"]} yields GPS + BeiDou.

Returns {:ok, [{"G01", range_m}, ...]} (ascending satellite id) or {:error, :epoch_out_of_range} / {:error, :unknown_epoch}.

values(obs, epoch, opts \\ [])

@spec values(t(), non_neg_integer() | tuple(), keyword()) ::
  {:ok, %{required(String.t()) => [map()]}} | {:error, term()}

Every observation value for one epoch, keyed by satellite.

epoch is the integer index (from epochs/1) or an epoch tuple {{y, mo, d}, {h, mi, s}}. Unlike pseudoranges/3 this returns the raw RINEX observations across code types — pseudorange, carrier phase, Doppler, and signal strength — so callers can build carrier-phase combinations.

Returns {:ok, %{satellite_id => [obs]}} where each obs is

%{code: "L1C", kind: :carrier_phase, value: 1.23e8, units: :cycles,
  lli: 0 | nil, ssi: 7 | nil}

kind/units follow the RINEX code's leading letter (C:pseudorange/ :meters, L:carrier_phase/:cycles, D:doppler/:hz, S:signal_strength/:db_hz). A blank observation has a nil value. Returns {:error, :epoch_out_of_range} / {:error, :unknown_epoch}.

Options

  • :codes — a per-system code filter, e.g. %{"G" => ["L1C", "L2W"]}. By default every code for every satellite is returned; a non-empty filter restricts the result (and the data crossing the NIF boundary) to the listed systems, and within each to the listed codes. A system mapped to [] keeps all of that system's codes — e.g. %{"G" => []} is GPS-only, all codes.