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:
header_records/1- every header line with its exact text and typed reading (Sidereon.GNSS.RINEX.Clock.HeaderRecord),records/1- every data record with its type, name, civil epoch, instant, declared values, surplus values and how it was read (Sidereon.GNSS.RINEX.Clock.Record),series/1- the per-satellite series ofASrecords whose epoch resolves to an instant, with every declared value (Sidereon.GNSS.RINEX.Clock.Point),skipped_records/1,diagnostics/1andnotices/1- records outside the satellite series, lines a lossy read kept without reading, and findings that do not stop a read,version/1,layout/1,satellite_system/1,time_system/1,time_system_status/1andtime_scale/1- the header facts.time_scaleisnilwhen the time system is missing, unrecognised, conflicting or has no core scale (IRN); record epochs then keep their civil fields with no instant.
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
@type civil_epoch() :: NaiveDateTime.t() | Sidereon.GNSS.RINEX.Clock.CivilEpoch.t() | {{integer(), integer(), integer()}, {integer(), integer(), number()}}
@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()} } }
@type time_system() :: :gps | :glo | :gal | :qzs | :bds | :irn | :utc | :tai
Functions
@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.
@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.
@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.
Interpolated satellite clock bias at GPS seconds. GPST and QZSST series answer; seconds outside the civil years 1 through 9999 are refused.
@spec diagnostics(t()) :: [Sidereon.GNSS.RINEX.Clock.Diagnostic.t()]
Lines a lossy read kept without reading them as records, and header time-system errors.
@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}}.
@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}}.
@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.
@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.
The header facts in one map: version, layout, satellite_system,
time_system, time_system_status, time_scale and record_count.
@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.
@spec layout(t()) :: :v300 | :v304 | nil
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.
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 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.
@spec record_count(t()) :: non_neg_integer()
Number of data records.
@spec records(t()) :: [Sidereon.GNSS.RINEX.Clock.Record.t()]
Every data record in order, including repeated records for one name and epoch.
@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}}.
@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 code of the RINEX VERSION / TYPE record, when one is written.
@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.
The GPST and QZSST samples as %{satellite => [{gps_seconds, bias_s}]}.
@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.
@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.
@spec skipped_records(t()) :: [Sidereon.GNSS.RINEX.Clock.Skip.t()]
Records read from the source that are not in the satellite series.
@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.
The core time scale record epochs are interpreted in ("GPST", "UTC", ...),
or nil when the time system does not resolve to one.
@spec time_system(t()) :: time_system() | nil
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.
@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.
Declared format version; for a product built from rows, the version it is written in.
Write the product as RINEX clock text to path.