Sidereon.GNSS.RINEX.Clock (Sidereon v3.0.0)

Copy Markdown View Source

RINEX clock (.CLK) products: lossless reading, typed views, editing, writing and satellite clock-bias interpolation.

Precise clock products are distributed as RINEX clock files alongside the SP3 orbit. The SP3 orbit carries satellite clocks too, but only at the SP3 epoch spacing (15 minutes for IGS final), whereas the companion .CLK file carries the same clocks at a much finer cadence (30 seconds for IGS final). Linearly interpolating a 15-minute clock across the gap is a metre-level error on the faster satellite oscillators; the 30s clock removes almost all of it.

The product keeps its text

A product read from text keeps the text as its authority: every header line with its exact label and payload, and every body line in order, including blank lines, AR, AS, CR, DR and MS records, continuation lines and, after parse_lossy/1, the lines that do not read as a record. to_rinex_string/1 restates an unedited product byte for byte. The views are derived from those lines:

TIME SYSTEM ID reads GPS, GLO, GAL, QZS, BDS (and BDT), IRN, UTC and TAI. GLO is UTC, so a 23:59:60 label on a leap-second day is an epoch. A file without the record takes the 3.00 default with a notice.

The series field of the struct holds the GPST and QZSST samples as %{satellite => [{gps_seconds, bias_s}]}, the shape the precise-positioning satellite-clock option reads; series/1 returns every sample with its scale-tagged epoch.

Editing

Edits never change a product: set_time_system/2, set_record_values/3, insert_record/3, remove_record/2, retain_records/2 and edit_records/2 return a new product, after the core has validated the whole change. An edit the writer would refuse, including one that would drop a record's surplus values, is refused and changes nothing.

Writing

to_rinex_string/1 writes every value only when it reads back to the same bits and an epoch only as a seconds field that states it exactly, and refuses by name what it cannot state. to_rinex_string_with_policy/2 with a Sidereon.GNSS.RINEX.Clock.WritePolicy allowing nearest_microsecond_epochs writes such an epoch at the nearest microsecond and reports each departure. A product built from rows is written with a header stating its version, satellite system, TIME SYSTEM ID and data types.

Refusals

Errors are {:error, {tag, fields}} with every field the core's error carries: {:malformed_as_record, %{line, reason, record}}, {:missing_continuation, %{line, record_type}}, {:malformed_continuation, %{line, reason, record}}, {:bad_field, %{line, field, value}}, {:invalid_input, %{field, reason}} and {:unsupported_time_scale, %{scale}}. An argument the boundary cannot carry is named before the call as {:invalid_epoch_field, field, value}, {:value_out_of_range, field, value} or {:invalid_argument, field, value}.

Summary

Functions

A civil GPS-time tag as GPS seconds, reading the second as clock_s/3 does.

A civil clock tag in time_scale as a scale-tagged instant, reading the second as clock_s/3 does: {:ok, epoch} or {:error, :invalid_epoch}. A 23:59:60 label is accepted for UTC on a day that ends with a positive leap second; every other scale refuses it.

Interpolated satellite clock bias in seconds at epoch.

Interpolated satellite clock bias at GPS seconds. GPST and QZSST series answer; seconds outside the civil years 1 through 9999 are refused.

Lines a lossy read kept without reading them as records, and header time-system errors.

Replace the declared values of every record for which edit returns a list of values (bias first), in one pass; nil leaves a record unchanged. The whole batch is checked before anything changes: if one edit is refused, none is applied. Returns {:ok, {clock, edited_count}}.

Build a product in time_scale (a core scale abbreviation such as "GPST", "UTC" or "BDT") from %{satellite => [%Point{}]} (or a list of {satellite, points}), keeping every declared value of each point.

Build a GPST product from %{satellite => [{gps_seconds, bias_s}]} (or a list of {satellite, rows}). Each satellite's seconds must be strictly increasing; seconds outside the civil years 1 through 9999 are refused.

Every header line in order with its typed reading. A product built from rows has none.

The header facts in one map: version, layout, satellite_system, time_system, time_system_status, time_scale and record_count.

Insert a record before the record at index, or after the last when index is record_count/1.

Column layout records are read and written in: :v300, :v304 or nil.

Load a RINEX clock file strictly. See parse/1.

Load a RINEX clock file, raising on error.

Load a RINEX clock file keeping unreadable lines with diagnostics. See parse_lossy/1.

Load a RINEX clock file keeping unreadable lines with diagnostics, raising on file errors.

Findings about how the product was read that do not stop it being read, such as {:time_system_defaulted, %{system}}, :time_system_missing, {:surplus_values, %{records, first_line}} or {:whitespace_records, %{records, first_line}} and {:trailing_text_records, %{records, first_line}}.

Parse RINEX clock text, failing on the first line that does not read.

Parse RINEX clock text keeping every line that does not read verbatim with a diagnostic (diagnostics/1). Nothing is dropped: to_rinex_string/1 on the result restates the input exactly.

Number of data records.

Every data record in order, including repeated records for one name and epoch.

Remove the record at index with every line it spans, returning {:ok, {clock, removed_record}}.

Keep the records keep accepts (a truthy result) and remove every other, with every line it spans, in one pass; returns {:ok, {clock, removed_count}}. Blank and unread lines stay.

Satellite system code of the RINEX VERSION / TYPE record, when one is written.

The per-satellite series of AS records whose epoch resolves to an instant, each strictly time-ordered, with scale-tagged epochs and every declared value. Where records repeat one satellite and instant, the last in file order is the sample; every such record remains in records/1.

The GPST and QZSST samples as %{satellite => [{gps_seconds, bias_s}]}.

Replace the declared values of the record at index (in records/1 order), bias first. The record keeps its type, name and epoch, including the exact text of its seconds field. An edit that would drop the record's surplus values, or that the writer could not state, is refused.

Declare the product's time system (:gps, :glo, :gal, :qzs, :bds, :irn, :utc or :tai), replacing every TIME SYSTEM ID record or inserting one. Every record epoch is checked in the new system first; if one does not convert, nothing changes. A product with no header section, or built from rows, is refused.

Records read from the source that are not in the satellite series.

One line of the source text by one-based line number, without its terminator, or nil.

The core time scale record epochs are interpreted in ("GPST", "UTC", ...), or nil when the time system does not resolve to one.

The product's time system, when one is declared, defaulted or built in.

How the time system was established: :declared, :defaulted, {:unrecognized, label}, {:conflicting, labels} or :constructed.

Write the product as RINEX clock text, refusing by name a value or epoch it cannot state exactly.

Write the product under a Sidereon.GNSS.RINEX.Clock.WritePolicy, returning {:ok, %{text: text, departures: departures}} with every departure the policy allowed and the writer emitted, as {:epoch_at_nearest_microsecond, %{record, name, epoch, written}}: the record's index in records/1, its name, the epoch the product holds (nil without an instant) and the epoch fields as written.

Declared format version; for a product built from rows, the version it is written in.

Write the product as RINEX clock text to path.

Types

civil_epoch()

@type civil_epoch() ::
  NaiveDateTime.t()
  | Sidereon.GNSS.RINEX.Clock.CivilEpoch.t()
  | {{integer(), integer(), integer()}, {integer(), integer(), number()}}

t()

@type t() :: %Sidereon.GNSS.RINEX.Clock{
  handle: reference() | nil,
  series: %{required(String.t()) => [{float(), float()}]},
  trailing_suffixes: %{
    required(non_neg_integer()) => {binary(), non_neg_integer()}
  }
}

time_system()

@type time_system() :: :gps | :glo | :gal | :qzs | :bds | :irn | :utc | :tai

Functions

civil_to_gps_seconds(epoch)

@spec civil_to_gps_seconds(civil_epoch()) :: {:ok, float()} | {:error, term()}

A civil GPS-time tag as GPS seconds, reading the second as clock_s/3 does.

civil_to_instant(time_scale, epoch)

@spec civil_to_instant(String.t(), civil_epoch()) ::
  {:ok, Sidereon.GNSS.Ionosphere.Epoch.t()} | {:error, term()}

A civil clock tag in time_scale as a scale-tagged instant, reading the second as clock_s/3 does: {:ok, epoch} or {:error, :invalid_epoch}. A 23:59:60 label is accepted for UTC on a day that ends with a positive leap second; every other scale refuses it.

clock_s(clock, satellite_id, epoch)

@spec clock_s(t(), String.t(), civil_epoch() | Sidereon.GNSS.Ionosphere.Epoch.t()) ::
  {:ok, float()} | {:error, term()}

Interpolated satellite clock bias in seconds at epoch.

epoch is a civil epoch in the product's time scale (a NaiveDateTime, a Sidereon.GNSS.RINEX.Clock.CivilEpoch or a {{y, m, d}, {h, min, s}} tuple whose s may carry a fraction and, on a UTC product, be 60.x on a leap-second day) or a scale-tagged Sidereon.GNSS.Ionosphere.Epoch. The query second is read as the shortest decimal of the double given, every digit kept, so a query at a record's stated epoch lands on that record.

Returns {:ok, bias_s} when the satellite has records bracketing the epoch (or an exact-match record), {:error, :no_clock} when the satellite is unknown or the epoch lies outside its record span, or {:error, reason} when the product's time system resolves to no scale or the epoch is not valid in it. Linear interpolation between the two nearest records, across a UTC leap second by elapsed time; no extrapolation.

clock_s_at_gps_seconds(clock, satellite_id, gps_seconds)

@spec clock_s_at_gps_seconds(t(), String.t(), number()) ::
  {:ok, float()} | {:error, term()}

Interpolated satellite clock bias at GPS seconds. GPST and QZSST series answer; seconds outside the civil years 1 through 9999 are refused.

diagnostics(clock)

@spec diagnostics(t()) :: [Sidereon.GNSS.RINEX.Clock.Diagnostic.t()]

Lines a lossy read kept without reading them as records, and header time-system errors.

edit_records(clock, edit)

@spec edit_records(t(), (Sidereon.GNSS.RINEX.Clock.Record.t() -> [number()] | nil)) ::
  {:ok, {t(), non_neg_integer()}} | {:error, term()}

Replace the declared values of every record for which edit returns a list of values (bias first), in one pass; nil leaves a record unchanged. The whole batch is checked before anything changes: if one edit is refused, none is applied. Returns {:ok, {clock, edited_count}}.

from_clock_points(time_scale, rows)

@spec from_clock_points(
  String.t(),
  map() | [{String.t(), [Sidereon.GNSS.RINEX.Clock.Point.t()]}]
) ::
  {:ok, t()} | {:error, term()}

Build a product in time_scale (a core scale abbreviation such as "GPST", "UTC" or "BDT") from %{satellite => [%Point{}]} (or a list of {satellite, points}), keeping every declared value of each point.

The writer writes QZSST and BDT products as 3.04 and the others as 3.00, and refuses a scale no RINEX clock time system names (GLONASS system time among them, since GLO names UTC hours) as {:unsupported_time_scale, %{scale}}.

from_series_rows(rows)

@spec from_series_rows(map() | [{String.t(), [{number(), number()}]}]) ::
  {:ok, t()} | {:error, term()}

Build a GPST product from %{satellite => [{gps_seconds, bias_s}]} (or a list of {satellite, rows}). Each satellite's seconds must be strictly increasing; seconds outside the civil years 1 through 9999 are refused.

header_records(clock)

@spec header_records(t()) :: [Sidereon.GNSS.RINEX.Clock.HeaderRecord.t()]

Every header line in order with its typed reading. A product built from rows has none.

info(clock)

@spec info(t()) :: map()

The header facts in one map: version, layout, satellite_system, time_system, time_system_status, time_scale and record_count.

insert_record(clock, index, record)

@spec insert_record(t(), non_neg_integer(), map()) :: {:ok, t()} | {:error, term()}

Insert a record before the record at index, or after the last when index is record_count/1.

record is a map with record_type (:ar, :as, :cr, :dr or :ms), name (a satellite identifier for :as), epoch (a civil epoch as clock_s/3 takes one) and values (the bias followed by up to five further values). The record must be writable in the product's layout and its epoch valid in the product's time scale.

layout(clock)

@spec layout(t()) :: :v300 | :v304 | nil

Column layout records are read and written in: :v300, :v304 or nil.

load(path)

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

Load a RINEX clock file strictly. See parse/1.

load!(path)

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

Load a RINEX clock file, raising on error.

load_lossy(path)

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

Load a RINEX clock file keeping unreadable lines with diagnostics. See parse_lossy/1.

load_lossy!(path)

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

Load a RINEX clock file keeping unreadable lines with diagnostics, raising on file errors.

notices(clock)

@spec notices(t()) :: [term()]

Findings about how the product was read that do not stop it being read, such as {:time_system_defaulted, %{system}}, :time_system_missing, {:surplus_values, %{records, first_line}} or {:whitespace_records, %{records, first_line}} and {:trailing_text_records, %{records, first_line}}.

parse(contents)

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

Parse RINEX clock text, failing on the first line that does not read.

Records are read at the columns of the file's declared version (the 80-column layout before 3.04, the 85-column layout from 3.04), then at the other version's columns, then as whitespace-separated values; each record reports how it was read.

parse_lossy(contents)

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

Parse RINEX clock text keeping every line that does not read verbatim with a diagnostic (diagnostics/1). Nothing is dropped: to_rinex_string/1 on the result restates the input exactly.

record_count(clock)

@spec record_count(t()) :: non_neg_integer()

Number of data records.

records(clock)

@spec records(t()) :: [Sidereon.GNSS.RINEX.Clock.Record.t()]

Every data record in order, including repeated records for one name and epoch.

remove_record(clock, index)

@spec remove_record(t(), non_neg_integer()) ::
  {:ok, {t(), Sidereon.GNSS.RINEX.Clock.Record.t()}} | {:error, term()}

Remove the record at index with every line it spans, returning {:ok, {clock, removed_record}}.

retain_records(clock, keep)

@spec retain_records(t(), (Sidereon.GNSS.RINEX.Clock.Record.t() -> as_boolean(term()))) ::
  {:ok, {t(), non_neg_integer()}} | {:error, term()}

Keep the records keep accepts (a truthy result) and remove every other, with every line it spans, in one pass; returns {:ok, {clock, removed_count}}. Blank and unread lines stay.

satellite_system(clock)

@spec satellite_system(t()) :: String.t() | nil

Satellite system code of the RINEX VERSION / TYPE record, when one is written.

series(clock)

@spec series(t()) :: %{required(String.t()) => [Sidereon.GNSS.RINEX.Clock.Point.t()]}

The per-satellite series of AS records whose epoch resolves to an instant, each strictly time-ordered, with scale-tagged epochs and every declared value. Where records repeat one satellite and instant, the last in file order is the sample; every such record remains in records/1.

series_rows(clock)

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

The GPST and QZSST samples as %{satellite => [{gps_seconds, bias_s}]}.

set_record_values(clock, index, values)

@spec set_record_values(t(), non_neg_integer(), [number()]) ::
  {:ok, t()} | {:error, term()}

Replace the declared values of the record at index (in records/1 order), bias first. The record keeps its type, name and epoch, including the exact text of its seconds field. An edit that would drop the record's surplus values, or that the writer could not state, is refused.

set_time_system(clock, system)

@spec set_time_system(t(), time_system()) :: {:ok, t()} | {:error, term()}

Declare the product's time system (:gps, :glo, :gal, :qzs, :bds, :irn, :utc or :tai), replacing every TIME SYSTEM ID record or inserting one. Every record epoch is checked in the new system first; if one does not convert, nothing changes. A product with no header section, or built from rows, is refused.

skipped_records(clock)

@spec skipped_records(t()) :: [Sidereon.GNSS.RINEX.Clock.Skip.t()]

Records read from the source that are not in the satellite series.

source_line(clock, line)

@spec source_line(t(), pos_integer()) :: String.t() | nil

One line of the source text by one-based line number, without its terminator, or nil.

time_scale(clock)

@spec time_scale(t()) :: String.t() | nil

The core time scale record epochs are interpreted in ("GPST", "UTC", ...), or nil when the time system does not resolve to one.

time_system(clock)

@spec time_system(t()) :: time_system() | nil

The product's time system, when one is declared, defaulted or built in.

time_system_status(clock)

@spec time_system_status(t()) :: term()

How the time system was established: :declared, :defaulted, {:unrecognized, label}, {:conflicting, labels} or :constructed.

to_rinex_string(clock)

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

Write the product as RINEX clock text, refusing by name a value or epoch it cannot state exactly.

to_rinex_string_with_policy(clock, policy)

@spec to_rinex_string_with_policy(t(), Sidereon.GNSS.RINEX.Clock.WritePolicy.t()) ::
  {:ok, %{text: String.t(), departures: [term()]}} | {:error, term()}

Write the product under a Sidereon.GNSS.RINEX.Clock.WritePolicy, returning {:ok, %{text: text, departures: departures}} with every departure the policy allowed and the writer emitted, as {:epoch_at_nearest_microsecond, %{record, name, epoch, written}}: the record's index in records/1, its name, the epoch the product holds (nil without an instant) and the epoch fields as written.

version(clock)

@spec version(t()) :: float() | nil

Declared format version; for a product built from rows, the version it is written in.

write(clock, path)

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

Write the product as RINEX clock text to path.