Sidereon.GNSS.Time (Sidereon v3.0.0)

Copy Markdown View Source

Epoch conversions shared by the GNSS correction wrappers.

These helpers turn an Elixir NaiveDateTime or a {{year, month, day}, {hour, minute, second}} tuple into the two representations the sidereon-core crate consumes:

  • a split Julian date {jd_whole, fraction} where jd_whole is the *.5 midnight boundary of the civil day and fraction is the within-day part (the same convention the SP3 reader uses);
  • integer or continuous seconds since the J2000 epoch (JD 2451545.0), used by NIF calls that consume either exact product epoch axes or fractional receive times.

No leap-second shifting is applied: the epoch stays in the time scale the caller supplied it in (typically GPS time for these models).

Summary

Types

The complete TimeModelError::InvalidInput detail returned by the detailed UTC split endpoint. family is TimeModelError, kind is TIME_MODEL_INVALID_INPUT, and field/reason preserve the core values. The family and kind fields are strings with those fixed values; they use String.t() because Elixir typespecs cannot express binary-string literals.

A time scale, named either by atom (:gpst, :utc, :glonasst, ...) or by its uppercase abbreviation string ("GPST", "UTC", ...).

Functions

Fractional day-of-year of the epoch, as {:ok, day} with the float the Niell troposphere seasonal term consumes.

Convert an epoch to integer seconds since the J2000 epoch (JD 2451545.0).

Convert an epoch to continuous floating-point seconds since J2000.

Convert an epoch to the split Julian date {:ok, {jd_whole, fraction}}.

Build an exact epoch label from a civil time value.

GPS minus UTC, in seconds, in effect at a UTC calendar date.

Provenance and coverage of the embedded leap-second table.

TAI minus UTC, in seconds, in effect at a UTC calendar date.

TAI minus UTC for a list of UTC calendar dates.

Seconds-of-day in [0, 86400), formed from the epoch's clock fields, as {:ok, seconds}.

TAI minus UTC, in seconds, in effect at a UTC calendar date.

The supported time-scale atoms.

Fixed inter-system time offset to - from, in seconds.

Leap-aware inter-system time offset to - from, in seconds, at utc_jd.

Provenance and coverage of the embedded UT1/EOP table.

Validated UTC instant for an epoch, as the split Julian date {jd_whole, fraction}.

Typed counterpart to utc_instant_split/1 that returns the full core time-model refusal detail while retaining the legacy endpoint's :invalid_instant result.

Types

leap_second_table()

@type leap_second_table() :: %{
  source: String.t(),
  first_mjd: integer(),
  last_mjd: integer(),
  entries: non_neg_integer()
}

time_model_error_detail()

@type time_model_error_detail() :: %{
  family: String.t(),
  kind: String.t(),
  message: String.t(),
  field: String.t(),
  reason: String.t(),
  debug: nil
}

The complete TimeModelError::InvalidInput detail returned by the detailed UTC split endpoint. family is TimeModelError, kind is TIME_MODEL_INVALID_INPUT, and field/reason preserve the core values. The family and kind fields are strings with those fixed values; they use String.t() because Elixir typespecs cannot express binary-string literals.

time_scale()

@type time_scale() :: atom() | String.t()

A time scale, named either by atom (:gpst, :utc, :glonasst, ...) or by its uppercase abbreviation string ("GPST", "UTC", ...).

ut1_coverage()

@type ut1_coverage() :: %{
  source: String.t(),
  first_mjd: integer(),
  last_mjd: integer(),
  first_jd_tt: float(),
  last_jd_tt: float(),
  entries: non_neg_integer()
}

Functions

day_of_year(epoch)

@spec day_of_year(NaiveDateTime.t() | tuple()) :: {:ok, float()} | {:error, term()}

Fractional day-of-year of the epoch, as {:ok, day} with the float the Niell troposphere seasonal term consumes.

January 1 00:00 is 1.0. The continuous day-of-year comes from sidereon_core::astro::time::civil::day_of_year, matching the crate's fractional SolveInputs.day_of_year convention, so the SPP troposphere and Sidereon.GNSS.Troposphere agree for the same epoch. The refusals are those of epoch_to_split_jd/1.

epoch_to_j2000_seconds(ndt)

@spec epoch_to_j2000_seconds(NaiveDateTime.t() | tuple()) ::
  {:ok, integer()} | {:error, term()}

Convert an epoch to integer seconds since the J2000 epoch (JD 2451545.0).

A whole-second epoch yields an exact integer (the core returns the exact whole-second value, which is converted back to an integer here). Returns {:ok, seconds} or {:error, :non_integer_second_epoch} if the epoch carries a sub-second part, with the field refusals of epoch_to_split_jd/1. The continuous seconds come from sidereon_core::astro::time::civil::j2000_seconds.

epoch_to_j2000_seconds_fractional(epoch)

@spec epoch_to_j2000_seconds_fractional(NaiveDateTime.t() | tuple()) ::
  {:ok, float()} | {:error, term()}

Convert an epoch to continuous floating-point seconds since J2000.

Unlike epoch_to_j2000_seconds/1, this accepts sub-second NaiveDateTime values and tuple epochs with a floating-point seconds field. Delegates to sidereon_core::astro::time::civil::j2000_seconds. Returns {:ok, seconds} or the refusals of epoch_to_split_jd/1; a value of another shape is {:error, :non_integer_second_epoch}.

epoch_to_split_jd(epoch)

@spec epoch_to_split_jd(NaiveDateTime.t() | tuple()) ::
  {:ok, {float(), float()}} | {:error, term()}

Convert an epoch to the split Julian date {:ok, {jd_whole, fraction}}.

The calendar arithmetic lives in sidereon-core (sidereon_core::astro::time::civil::split_julian_date); this module only marshals the epoch into civil (year, month, day, hour, minute, second) fields. A field the core cannot take is named rather than raised; see "Epoch refusals" below.

Epoch refusals

Every epoch helper here reads the fields it passes through the one check the IONEX epochs use, so a NaiveDateTime and a tuple holding the same fields are refused the same way:

  • {:invalid_epoch_field, field, value} - a year, month, day, hour or minute that is not an integer, or a second that is not a number,
  • {:value_out_of_range, field, value} - one of the first five outside the signed 32-bit range the core takes, or an integer second larger in magnitude than the largest finite double,
  • :bad_epoch - a value that is neither a NaiveDateTime nor a {{year, month, day}, {hour, minute, second}} tuple.

exact_epoch(epoch)

@spec exact_epoch(NaiveDateTime.t() | tuple()) ::
  {:ok, Sidereon.GNSS.Time.ExactEpoch.t()} | {:error, term()}

Build an exact epoch label from a civil time value.

gps_utc_offset_s(year, month, day)

@spec gps_utc_offset_s(integer(), integer(), integer()) :: float()

GPS minus UTC, in seconds, in effect at a UTC calendar date.

This is the IS-GPS-200 quantity broadcast in the navigation message (the leap seconds a GPS receiver applies): 18 s from 2017. It is leap_seconds/3 minus the constant 19 s TAI - GPST, so it is the value to use whenever you mean "GPS - UTC", not leap_seconds/3 (which is TAI - UTC).

leap_second_table_info()

@spec leap_second_table_info() :: leap_second_table()

Provenance and coverage of the embedded leap-second table.

leap_seconds(year, month, day)

@spec leap_seconds(integer(), integer(), integer()) :: float()

TAI minus UTC, in seconds, in effect at a UTC calendar date.

Delegates to sidereon_core::astro::time::scales::julian_day_number and sidereon_core::astro::time::scales::find_leap_seconds.

leap_seconds_batch(dates)

@spec leap_seconds_batch([{integer(), integer(), integer()}]) :: [float()]

TAI minus UTC for a list of UTC calendar dates.

Each date is {year, month, day} and each result delegates to the same core functions as leap_seconds/3.

second_of_day(epoch)

@spec second_of_day(NaiveDateTime.t() | tuple()) :: {:ok, float()} | {:error, term()}

Seconds-of-day in [0, 86400), formed from the epoch's clock fields, as {:ok, seconds}.

Used by the Klobuchar diurnal term, which takes the GPS second-of-day directly. The arithmetic delegates to sidereon_core::astro::time::civil::second_of_day. Only the clock fields are read, so only hour, minute and second are checked; the refusals are those of epoch_to_split_jd/1.

tai_utc_offset_s(year, month, day)

@spec tai_utc_offset_s(integer(), integer(), integer()) :: float()

TAI minus UTC, in seconds, in effect at a UTC calendar date.

The unambiguously named alias of leap_seconds/3 (the IERS Bulletin C quantity, 37 s from 2017); it returns the identical value. Use gps_utc_offset_s/3 for the GNSS "GPS - UTC" offset instead.

time_scales()

@spec time_scales() :: [atom()]

The supported time-scale atoms.

timescale_offset(from, to)

@spec timescale_offset(time_scale(), time_scale()) ::
  {:ok, float()} | {:error, term()}

Fixed inter-system time offset to - from, in seconds.

Returns the value that, added to a reading in the from scale, yields the to-scale reading of the same instant. Defined only for the atomic scales (TAI/TT/GPST/GST/QZSST/BDT) whose mutual offset is a constant.

Returns {:error, {:epoch_required, scale}} for the UTC-based scales (UTC and GLONASST) whose offset carries the leap-second count (use timescale_offset_at/3 with an epoch), and {:error, {:unsupported, "TDB"}} for TDB (its offset from TT is an epoch-dependent periodic term).

iex> Sidereon.GNSS.Time.timescale_offset(:gpst, :tai)
{:ok, 19.0}

iex> Sidereon.GNSS.Time.timescale_offset(:gpst, :utc)
{:error, {:epoch_required, "UTC"}}

timescale_offset_at(from, to, utc_jd)

@spec timescale_offset_at(time_scale(), time_scale(), number()) ::
  {:ok, float()} | {:error, term()}

Leap-aware inter-system time offset to - from, in seconds, at utc_jd.

utc_jd is the UTC Julian date of the instant; it only affects the result when from or to is UTC-based (UTC/GLONASST), resolving the leap-second count. For purely atomic pairs it is ignored and the result matches timescale_offset/2.

iex> {:ok, off} = Sidereon.GNSS.Time.timescale_offset_at(:glonasst, :utc, 2_451_545.0)
iex> Float.round(off, 1)
-10800.0

ut1_coverage_info()

@spec ut1_coverage_info() :: ut1_coverage()

Provenance and coverage of the embedded UT1/EOP table.

utc_instant_split(epoch)

@spec utc_instant_split(NaiveDateTime.t() | tuple()) ::
  {:ok, {float(), float()}} | {:error, term()}

Validated UTC instant for an epoch, as the split Julian date {jd_whole, fraction}.

Delegates to sidereon_core::astro::time::model::Instant::from_utc_civil, the entry the ionosphere/troposphere delay dispatchers build their epoch argument from. Unlike epoch_to_split_jd/1, this runs the core's JulianDateSplit guard, so an out-of-day clock field is rejected as {:error, :invalid_instant} rather than producing an out-of-range fraction. A field the core cannot take is refused as in epoch_to_split_jd/1.

iex> {:ok, {jd_whole, _fraction}} =
...>   Sidereon.GNSS.Time.utc_instant_split({{2020, 6, 25}, {12, 0, 0}})
iex> jd_whole
2_459_025.5

utc_instant_split_detailed(epoch)

@spec utc_instant_split_detailed(NaiveDateTime.t() | tuple()) ::
  {:ok, {float(), float()}}
  | {:error, {:invalid_input, time_model_error_detail()}}
  | {:error, {:invalid_epoch_field, atom(), term()}}
  | {:error, {:value_out_of_range, atom(), integer() | float()}}
  | {:error, :bad_epoch}

Typed counterpart to utc_instant_split/1 that returns the full core time-model refusal detail while retaining the legacy endpoint's :invalid_instant result.