A standalone regular-grid vertical TEC source.
This is the regular-grid variant sidereon-core exposes beside the IONEX
product: vertical TEC in TECU on strictly increasing epoch, latitude and
longitude axes, each with at least two nodes.
Axes and value order
The epoch axis is floating-point Unix nanoseconds, which is what the core
stores; adjacent nanoseconds are not distinguishable at large magnitudes, and
the axis is reported back exactly as stored. This is a different origin from
the IONEX product surface, whose Sidereon.GNSS.Ionosphere.Epoch counts
nanoseconds from J2000; neither is converted into the other. Latitudes and
longitudes are degrees. values is flat [epoch][latitude][longitude] with
longitude varying fastest.
A node without a value is nil; a node holding 0.0 has a value of zero.
Queries
vtec_at_pierce_point/5 takes longitude_deg before latitude_deg, matching
the core signature, and an exact integer Unix-nanosecond epoch. A query
interpolates the eight corners of the surrounding cell: bilinearly within each
of the two bracketing epochs, then linearly in time. Latitudes outside
[-87.5, 87.5] are clamped to that interval by the core.
XYZ evaluation
tec_xyz/6 and iono_delay_xyz/7 evaluate the grid along the line of sight
from an ECEF receiver position to an ECEF satellite position, both in meters:
vertical and slant TEC in TECU, or the group delay in meters on a carrier. The
core intersects the line of sight with a thin spherical shell, interpolates
the grid at the longitude and latitude of that pierce point, and maps the
vertical TEC to slant TEC with the elevation. The longitude and latitude come
from the caller: the evaluation asks a converter, a function of one argument,
for the geodetic coordinates of an ECEF position {x, y, z} in meters, and
the converter answers {lon_deg, lat_deg, alt}.
The converter is asked for the pierce point first. An answer with a NaN in any component, the altitude included, makes the core ask for the receiver position exactly as given, and the evaluation then uses the receiver's longitude and latitude. The altitude of that second answer is not read, so a NaN there asks for nothing further, and a NaN longitude or latitude there is refused rather than asked about again. An evaluation therefore calls the converter once, or twice after a NaN answer.
The core checks its inputs before the first conversion, in this order: the carrier for a delay, then the positions, the options and the geometry they give. A refusal there leaves the converter uncalled. The elevation is checked when the pierce-point answer arrives, before the core looks for a NaN in it, so a refused elevation ends the evaluation after one call even when that answer held a NaN. The grid is read only after the last conversion, so an epoch outside the grid, a pierce point outside it or a missing node is refused after the converter has been called, as are the checks on the slant mapping and, for a delay, on the delay itself.
The converter runs in the calling process as an ordinary function call, between steps of the evaluation that each return to Elixir; no native code calls it. Whatever it raises, throws or exits with reaches the caller unchanged, with its own stacktrace, and the evaluation stops there. It may itself query this grid or any other. Every step holds the grid the evaluation was prepared on, so the caller's own reference to the grid can be dropped while the evaluation runs, and no step can be continued on another grid.
Non-finite doubles
An Erlang float is always finite, while the positions the core asks about and
the answers it reads need not be: a line of sight that misses the shell gives
a pierce point whose components are NaN, and that position is still passed to
the converter, as the core passes it to its own callback. So every double of
these two functions, in either direction, is a double/0: a float where it
is finite, and otherwise {:nonfinite, bits}, bits being its IEEE 754
binary64 bit pattern as an unsigned integer, sign and NaN payload included.
nan/0, infinity/0 and neg_infinity/0 give the usual three.
The converter receives its position in this form and may answer in it, and a position, the carrier or an option may be given in it. An integer is read onto its double. The tag only carries the bits: the double reaches the core bit for bit, and the core's own NaN and finiteness checks apply to it as to any other. A latitude of either infinity is clamped to 87.5 degrees of the same sign like any latitude beyond that, while an infinite longitude is refused. The pattern under the tag must be an integer from 0 to 2^64 - 1 with every exponent bit set, which is a NaN or an infinity. A finite pattern is refused, because a finite value crosses as a float.
Summary
Types
A double as a caller may give it: a number or a double/0.
Converts an ECEF position in meters to {lon_deg, lat_deg, alt}.
A double of the XYZ evaluations: a float where it is finite, and otherwise
{:nonfinite, bits} with its IEEE 754 binary64 bit pattern.
An ECEF position in meters, {x, y, z}.
Functions
The grid's epoch axis, as the floating-point Unix nanoseconds the core stores.
Positive infinity as a double/0.
The ionospheric group delay along the line of sight from receiver_xyz to
satellite_xyz on the carrier frequency_hz, in positive meters.
The grid's latitude axis in degrees.
The grid's longitude axis in degrees.
Negative infinity as a double/0.
Builds a grid from its three axes and a flat list of TECU values in
[epoch][latitude][longitude] order.
Vertical and slant TEC along the line of sight from receiver_xyz to
satellite_xyz, in TECU.
The grid's TECU values, flat in [epoch][latitude][longitude] order with
longitude varying fastest. A node without a value is nil.
Vertical TEC interpolated at a pierce point, in TECU.
Types
A double as a caller may give it: a number or a double/0.
Converts an ECEF position in meters to {lon_deg, lat_deg, alt}.
@type double() :: float() | {:nonfinite, non_neg_integer()}
A double of the XYZ evaluations: a float where it is finite, and otherwise
{:nonfinite, bits} with its IEEE 754 binary64 bit pattern.
@type t() :: %Sidereon.GNSS.Ionosphere.TecGrid{handle: reference()}
An ECEF position in meters, {x, y, z}.
Functions
The grid's epoch axis, as the floating-point Unix nanoseconds the core stores.
Returns {:error, {:bad_tec_grid, value}} for an argument that is neither a
grid nor a handle, and {:error, {:invalid_resource, :tec_grid}} for a
reference the boundary cannot read as a standalone TEC grid. Both are the same
for the other three accessors.
@spec infinity() :: double()
Positive infinity as a double/0.
@spec iono_delay_xyz( t() | reference(), integer(), component(), xyz(), xyz(), converter(), keyword() ) :: {:ok, Sidereon.GNSS.Ionosphere.TecGrid.Evaluation.t()} | {:error, term()}
The ionospheric group delay along the line of sight from receiver_xyz to
satellite_xyz on the carrier frequency_hz, in positive meters.
frequency_hz is in hertz, a number or a double/0. The core checks the
carrier before the positions and the geometry: a carrier that is not finite
and positive is refused ahead of them, and the converter is not called. Every
other argument and option is as for tec_xyz/6. A finished evaluation
converts its slant TEC to a delay on the carrier, which the core refuses
unless finite; a carrier whose square underflows to zero is refused there,
after the converter has been called.
Returns {:ok, %Evaluation{value: delay_m}}, with degraded as for
vtec_at_pierce_point/5, or {:error, reason} with the reasons tec_xyz/6
names, and {:invalid_double, :frequency_hz, value} or
{:value_out_of_range, :frequency_hz, value} for a carrier this binding cannot
read as a double.
The grid's latitude axis in degrees.
The grid's longitude axis in degrees.
@spec nan() :: double()
NaN as a double/0: the quiet NaN without a payload.
A converter answers with a NaN component to have the evaluation fall back to the receiver position; see "XYZ evaluation" in the module documentation. Every NaN pattern is read as NaN, whatever its sign or payload.
@spec neg_infinity() :: double()
Negative infinity as a double/0.
Builds a grid from its three axes and a flat list of TECU values in
[epoch][latitude][longitude] order.
epochs_ns is floating-point Unix nanoseconds. Each axis must hold at least
two strictly increasing entries, and values must hold exactly
length(epochs_ns) * length(latitudes_deg) * length(longitudes_deg) entries.
A node without a value is nil.
Returns {:ok, grid} or {:error, reason}, where reason names the failed
invariant. The core names :axes_too_short, :axes_not_increasing,
:dimensions_overflow, {:value_count_mismatch, actual, expected},
{:invalid_field, field, reason} with the core's own field and reason text,
and {:unhandled, message} for a variant this binding predates.
This binding names two of its own, which stay apart from the core's
:invalid_field:
{:invalid_grid_field, field, value}- an axis entry or a node value that is not a number, carrying the value that is not.{:value_out_of_range, field, value}- an axis entry or a node value that is an integer larger in magnitude than the largest finite double, which has no double to be read onto, carrying that integer.
@spec tec_xyz(t() | reference(), integer(), xyz(), xyz(), converter(), keyword()) :: {:ok, Sidereon.GNSS.Ionosphere.TecGrid.Evaluation.t()} | {:error, term()}
Vertical and slant TEC along the line of sight from receiver_xyz to
satellite_xyz, in TECU.
unix_nanos is an exact integer Unix-nanosecond epoch, as for
vtec_at_pierce_point/5; an epoch that is not an integer raises
FunctionClauseError, as it does there. satellite_xyz and receiver_xyz are ECEF positions
in meters, {x, y, z}. converter is a function of one argument that takes an
ECEF position {x, y, z} in meters and returns {lon_deg, lat_deg, alt}.
"XYZ evaluation" in the module documentation describes when the converter is
called and the form every double takes.
opts is a keyword list. A key left out takes the core's default:
:missing_nodes-:strict(the default) or:renormalize, as forvtec_at_pierce_point/5.:min_elevation_rad- the floor the elevation is raised to before the slant mapping, in radians; 5 degrees by default.:earth_radius_mand:shell_height_m- the thin shell, whose radius is their sum, in meters; 6,371,000 and 450,000 by default.:nan_pierce_point_height_m- the altitude that stands in for the receiver answer's own after a NaN answer, in meters; 450,000 by default, and left as it is when:shell_height_mis given. The core requires it to be finite, and interpolates the grid in longitude and latitude only.
Returns {:ok, %Evaluation{value: {vtec_tecu, stec_tecu}}}, with degraded
as for vtec_at_pierce_point/5, or {:error, reason}. The core's reasons are
those of vtec_at_pierce_point/5 - {:invalid_field, field, reason},
{:out_of_bounds, axis, value}, {:nodes_not_available, node_gap} and
{:unhandled, message} - with the XYZ inputs among the fields it names, such
as {:invalid_field, "receiver radius_m", "not positive"} for a receiver at
the origin. This binding names its own before the core reads anything:
{:invalid_converter, value}-converteris not a function of one argument.{:invalid_position, field, value}-:satellite_xyzor:receiver_xyzis not a three-element tuple of numbers anddouble/0values, carrying the argument as given.{:invalid_double, key, value}- a double option is neither a number nor adouble/0.{:value_out_of_range, field, value}- an integer larger in magnitude than the largest finite double, which has no double to be read onto, as a position component (fieldnames the position), a double option or an answer component (:lon_deg,:lat_degor:alt); or:unix_nanospast the 64-bit range the boundary carries it in.{:invalid_conversion, answer}- the converter returned something other than a three-element tuple of numbers anddouble/0values, carrying what it returned. This is named after the call, and the evaluation stops.{:invalid_policy_value, :missing_nodes, value},{:unknown_option_key, key},{:duplicate_option_key, key}and{:invalid_options, opts}, as forvtec_at_pierce_point/5.{:bad_tec_grid, value}and{:invalid_resource, :tec_grid}, as forvtec_at_pierce_point/5.
The grid's TECU values, flat in [epoch][latitude][longitude] order with
longitude varying fastest. A node without a value is nil.
@spec vtec_at_pierce_point( t() | reference(), integer(), number(), number(), keyword() ) :: {:ok, Sidereon.GNSS.Ionosphere.TecGrid.Evaluation.t()} | {:error, term()}
Vertical TEC interpolated at a pierce point, in TECU.
unix_nanos is an exact integer Unix-nanosecond epoch. lon_deg comes before
lat_deg, matching the core signature; both are degrees.
opts is a keyword list taking one key, :missing_nodes, either :strict
(the default, which refuses a query weighting a node that holds no value) or
:renormalize (which interpolates from the weighted nodes holding values and
names the rest in the result's degraded field).
Returns {:ok, %Evaluation{}}, or {:error, reason}:
{:nodes_not_available, %Sidereon.GNSS.Ionosphere.NodeGap{}}- a weighted node holds no value and the policy is:strict.{:out_of_bounds, axis, value}- the query is past an axis endpoint;axisis the core's own name for it.{:invalid_field, field, reason}- the core refused an input, with its own field and reason text.{:unhandled, message}- a core variant this binding predates, with the core's own text.{:invalid_policy_value, :missing_nodes, value}- a choice the key does not name.{:unknown_option_key, key}- a key other than:missing_nodes. The key is returned as given and no atom is created for it.{:duplicate_option_key, key}- a key stated twice, whether the two choices agree or not; collapsing it would drop the earlier statement.{:invalid_options, opts}-optsis not a keyword list.{:value_out_of_range, field, value}-:unix_nanospast the 64-bit range the boundary carries it in, or:lon_degor:lat_deggiven as an integer larger in magnitude than the largest finite double, which has no double to be read onto.{:bad_tec_grid, value}- the first argument is neither a grid nor a handle.{:invalid_resource, :tec_grid}- the handle is not a reference to a standalone TEC grid, such as a reference to another resource kind. The boundary refuses a reference it cannot read as:badarg, which names no field and which Elixir raises asArgumentError; every other argument of this call is checked here first, so the resource the call expected is named in its place.