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

Copy Markdown View Source

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.

t()

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.

NaN as a double/0: the quiet NaN without a payload.

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

component()

@type component() :: number() | double()

A double as a caller may give it: a number or a double/0.

converter()

@type converter() :: ({double(), double(), double()} ->
                  {component(), component(), component()})

Converts an ECEF position in meters to {lon_deg, lat_deg, alt}.

double()

@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.

t()

@type t() :: %Sidereon.GNSS.Ionosphere.TecGrid{handle: reference()}

xyz()

@type xyz() :: {component(), component(), component()}

An ECEF position in meters, {x, y, z}.

Functions

epochs_ns(grid)

@spec epochs_ns(t() | reference()) :: {:ok, [float()]} | {:error, term()}

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.

infinity()

@spec infinity() :: double()

Positive infinity as a double/0.

iono_delay_xyz(grid, unix_nanos, frequency_hz, satellite_xyz, receiver_xyz, converter, opts \\ [])

@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.

latitudes_deg(grid)

@spec latitudes_deg(t() | reference()) :: {:ok, [float()]} | {:error, term()}

The grid's latitude axis in degrees.

longitudes_deg(grid)

@spec longitudes_deg(t() | reference()) :: {:ok, [float()]} | {:error, term()}

The grid's longitude axis in degrees.

nan()

@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.

neg_infinity()

@spec neg_infinity() :: double()

Negative infinity as a double/0.

new(epochs_ns, latitudes_deg, longitudes_deg, values)

@spec new([number()], [number()], [number()], [number() | nil]) ::
  {:ok, t()} | {:error, term()}

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.

tec_xyz(grid, unix_nanos, satellite_xyz, receiver_xyz, converter, opts \\ [])

@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 for vtec_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_m and :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_m is 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} - converter is not a function of one argument.
  • {:invalid_position, field, value} - :satellite_xyz or :receiver_xyz is not a three-element tuple of numbers and double/0 values, carrying the argument as given.
  • {:invalid_double, key, value} - a double option is neither a number nor a double/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 (field names the position), a double option or an answer component (:lon_deg, :lat_deg or :alt); or :unix_nanos past the 64-bit range the boundary carries it in.
  • {:invalid_conversion, answer} - the converter returned something other than a three-element tuple of numbers and double/0 values, 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 for vtec_at_pierce_point/5.
  • {:bad_tec_grid, value} and {:invalid_resource, :tec_grid}, as for vtec_at_pierce_point/5.

values(grid)

@spec values(t() | reference()) :: {:ok, [float() | nil]} | {:error, term()}

The grid's TECU values, flat in [epoch][latitude][longitude] order with longitude varying fastest. A node without a value is nil.

vtec_at_pierce_point(grid, unix_nanos, lon_deg, lat_deg, opts \\ [])

@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; axis is 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} - opts is not a keyword list.
  • {:value_out_of_range, field, value} - :unix_nanos past the 64-bit range the boundary carries it in, or :lon_deg or :lat_deg given 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 as ArgumentError; every other argument of this call is checked here first, so the resource the call expected is named in its place.