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}wherejd_wholeis the*.5midnight boundary of the civil day andfractionis 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
@type leap_second_table() :: %{ source: String.t(), first_mjd: integer(), last_mjd: integer(), entries: non_neg_integer() }
@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.
A time scale, named either by atom (:gpst, :utc, :glonasst, ...) or by
its uppercase abbreviation string ("GPST", "UTC", ...).
Functions
@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.
@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.
@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}.
@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}- ayear,month,day,hourorminutethat is not an integer, or asecondthat 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 integersecondlarger in magnitude than the largest finite double,:bad_epoch- a value that is neither aNaiveDateTimenor a{{year, month, day}, {hour, minute, second}}tuple.
@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 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).
@spec leap_second_table_info() :: leap_second_table()
Provenance and coverage of the embedded leap-second table.
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.
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.
@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 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.
@spec time_scales() :: [atom()]
The supported time-scale atoms.
@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"}}
@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
@spec ut1_coverage_info() :: ut1_coverage()
Provenance and coverage of the embedded UT1/EOP table.
@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
@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.