Orbis.RinexObs (Orbis v0.8.0)

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.PointPositioning.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.GnssData.fetch/2, so this module only ever sees plain text or CRINEX text.

Example

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

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

[%{index: i, epoch: epoch} | _] = Orbis.RinexObs.epochs(obs)
{:ok, prs} = Orbis.RinexObs.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.

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.PointPositioning.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.

Extract single-frequency pseudoranges for one epoch.

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.RinexObs{handle: reference()}

Functions

approx_position(rinex_obs)

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

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(rinex_obs)

@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.PointPositioning.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.RinexObs{}} or {:error, reason}.

load!(path)

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

Like load/1 but raises on failure.

observation_codes(rinex_obs)

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

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