Sidereon.GNSS.Ionosphere.Epoch (Sidereon v3.0.0)

Copy Markdown View Source

A scale-tagged instant at the IONEX boundary.

sidereon-core holds an instant either as a split Julian date or as exact integer nanoseconds, and the two are not interchangeable without loss. An epoch here names which one it carries, and nothing converts between them or rounds in either direction:

  • %Epoch{time_scale: "UTC", jd_whole: 2_459_024.0, jd_fraction: 0.5}
  • %Epoch{time_scale: "UTC", j2000_nanos: 646_228_800_000_000_000}

Both examples are 2020-06-24 00:00:00, in the same scale.

The nanosecond origin is J2000, not Unix

j2000_nanos counts nanoseconds from the J2000 epoch, JD 2451545.0 in the instant's own scale, which is the origin the core reads this representation against: it divides the count by 1_000_000_000 and uses the result directly as the J2000 second of the map. So j2000_nanos: 0 is JD 2451545.0, and a count before it is negative. It is not a Unix timestamp; feeding Unix nanoseconds here lands roughly thirty years late.

The standalone Sidereon.GNSS.Ionosphere.TecGrid is the other convention: its epochs_ns axis and its unix_nanos query argument are Unix nanoseconds, because that is what its core type carries. The two surfaces keep their own origins and neither is converted into the other here.

Exactly one representation is populated. The IONEX parser builds its map epochs as split Julian dates, so a parsed product reads back as split Julian dates; a product built from nanosecond epochs reads back as the same integer nanosecond count it was given.

The IONEX epoch axis is whole seconds, so a product built from a count that is not a whole number of seconds is refused with :epoch_not_representable rather than rounded onto it.

What is refused

  • {:invalid_field, "fraction", "must be within one residual day"} - a split Julian date whose fraction is outside [-1, 1], with the core's own field and reason.
  • {:value_out_of_range, :j2000_nanos, value} - a count past the signed 128-bit range the boundary carries it in.
  • {:value_out_of_range, :jd_whole, value} or {:value_out_of_range, :jd_fraction, value} - a split Julian date part that is an integer larger in magnitude than the largest finite double, which has no double to be read onto.
  • {:invalid_epoch_field, field, value} - from from_civil/2, a year, month, day, hour or minute that is not an integer, or a second that is not a number. The shared calendar helper takes the first five as 32-bit integers, and an hour arrived at by division is a float, since Elixir's / always gives one.
  • {:value_out_of_range, field, value} - from from_civil/2, a year, month, day, hour or minute that is an integer outside the 32-bit range, or an integer second larger in magnitude than the largest finite double. A NaiveDateTime is checked the same way: its year can be past the 32-bit range.
  • :ambiguous_epoch_representation - both representations are populated, or neither is.
  • :bad_epoch - the value is not an Epoch.

Time scales

time_scale is the core's own abbreviation. The boundary reads all eleven scales the core names: "UTC", "TAI", "TT", "TCG", "TDB", "TCB", "GPST", "GST", "BDT", "GLONASST" and "QZSST". Any other string is reported as an unknown scale rather than replaced with a default. IONEX products are UTC throughout, so a parsed product's epochs are "UTC"; the scale is carried because the core instant carries it, and a product built from samples is tagged with whatever the caller states.

There is no default epoch and no default scale: from_civil/2 takes the scale it tags the result with.

Summary

Functions

An epoch from a civil NaiveDateTime or {{y, m, d}, {h, min, s}} tuple, as the split Julian date of that calendar instant, tagged with time_scale.

An epoch held as exact integer nanoseconds since J2000 (JD 2451545.0) in time_scale.

An epoch held as the split Julian date jd_whole + jd_fraction in time_scale.

Types

t()

@type t() :: %Sidereon.GNSS.Ionosphere.Epoch{
  j2000_nanos: integer() | nil,
  jd_fraction: number() | nil,
  jd_whole: number() | nil,
  time_scale: String.t()
}

Functions

from_civil(time_scale, epoch)

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

An epoch from a civil NaiveDateTime or {{y, m, d}, {h, min, s}} tuple, as the split Julian date of that calendar instant, tagged with time_scale.

The scale is the caller's statement about the calendar fields; this function does not shift the instant between scales. A fractional second is carried into the Julian date fraction as given.

Returns {:ok, epoch}, or {:error, reason} when a field has no value of the kind the shared calendar helper takes, as listed under "What is refused" in this module's documentation. Unlike the other constructors here, this one converts when it is called, because the calendar arithmetic runs in the core, so its refusals are returned here rather than when the epoch is converted.

Examples

{:ok, epoch} = Epoch.from_civil("UTC", {{2020, 6, 24}, {0, 0, 0}})

{:error, {:invalid_epoch_field, :hour, 2.0}} =
  Epoch.from_civil("UTC", {{2020, 6, 24}, {7200 / 3600, 0, 0}})

j2000_nanos(time_scale, nanos)

@spec j2000_nanos(String.t(), integer()) :: t()

An epoch held as exact integer nanoseconds since J2000 (JD 2451545.0) in time_scale.

The count is carried to the core as the integer it is, with no scaling and no rounding. Zero is JD 2451545.0 itself and a count before it is negative.

Examples

# 2000-01-01 12:00:00 UTC, the J2000 epoch itself
Epoch.j2000_nanos("UTC", 0)

# one hour before it
Epoch.j2000_nanos("UTC", -3_600_000_000_000)

julian_date(time_scale, jd_whole, jd_fraction)

@spec julian_date(String.t(), number(), number()) :: t()

An epoch held as the split Julian date jd_whole + jd_fraction in time_scale.

The two parts are kept as they were given, integer or float, and are read onto the doubles the boundary takes when the epoch is converted, so an integer no double holds is a named refusal of the build rather than an exception out of this constructor.