Single-frequency ionospheric group-delay corrections.
Four models are exposed over the sidereon-core crate: the GPS broadcast
Klobuchar model (eight alpha/beta coefficients, IS-GPS-200), the Galileo
NeQuick-G model in both its compact single-layer and full three-dimensional
forms, and an IONEX vertical-TEC-grid slant delay (single-layer model). Each
returns the group delay in positive meters.
The IONEX surface also reads and writes products: parse_ionex/1 and
parse_ionex_with_warnings/1 read one, ionex_to_string/1 writes one back,
from_samples/1 and from_node_samples/5 build one from samples with no text
in the loop, and tec_grid_samples/1 and tec_samples/1 read one back out.
ionex_header/1 reads the descriptive records and skipped_records/1 reads
what a forgiving parse passed over. Sidereon.GNSS.Ionosphere.TecGrid is the
standalone regular-grid variant, queried at a pierce point or along the line
of sight between ECEF receiver and satellite positions, as vertical and slant
TEC or as a group delay.
This is the ionosphere correction for a GNSS signal. It is not
Sidereon.Atmosphere, which is NRLMSISE-00 neutral-atmosphere mass density for
drag, a different quantity entirely.
Sign convention
The returned delay is a group delay and is positive: it increases the
measured pseudorange (the signal arrives later than vacuum geometry would
predict). The carrier-phase advance is the negation of this value. The
ionosphere is dispersive, so the delay reported on a carrier other than the
model's native L1 is the L1 delay scaled by (f_L1 / f)^2; pass the carrier
via frequency_hz.
Units at the boundary
Public inputs are in degrees (_deg) and meters/hertz, per the Sidereon naming
convention. Latitude is positive north, longitude positive east, azimuth
clockwise from north.
Handles
Every function taking a product handle takes a reference from parse_ionex/1,
load_ionex/1, from_samples/1 or from_node_samples/5. A reference the
boundary cannot read as an IONEX product - a
Sidereon.GNSS.Ionosphere.TecGrid handle, or a reference to any other
resource kind - is returned as {:error, {:invalid_resource, :ionex}} by every
one of them, rather than raising ArgumentError out of a call whose contract
is {:ok, _} | {:error, _}. The boundary refuses such a reference as
:badarg, which names no field; every other argument of these calls is
checked here first, each under its own name, so the resource the call expected
is what is named in its place.
Summary
Functions
Build an IONEX product from one sample per TEC grid node.
Build an IONEX product directly from whole-grid TEC samples.
Galileo NeQuick-G single-frequency ionospheric group delay, scaled to
frequency_hz.
The descriptive header records a parsed or sample-built product carries.
Batch IONEX slant delays, one result per request in request order.
IONEX vertical-TEC-grid slant ionospheric group delay, scaled to frequency_hz.
IONEX slant delay under explicit coverage, missing-node and mapping policies.
Serialize a parsed IONEX product back to standard IONEX text.
GPS broadcast Klobuchar L1 ionospheric group delay, scaled to frequency_hz.
Load and parse an IONEX file into a product handle.
Galileo NeQuick-G full slant ionospheric group delay (positive meters).
Galileo NeQuick-G full three-dimensional slant total electron content (TECU).
Parse an in-memory IONEX byte buffer into a product handle.
Parse an IONEX byte buffer, keeping what the reader reported about the file.
The number of records a forgiving parse passed over.
Extract a product handle as whole-grid TEC samples.
Extract a product handle as one TEC sample per grid node.
Functions
@spec from_node_samples( [Sidereon.GNSS.Ionosphere.TecSample.t()], number(), number(), integer(), Sidereon.GNSS.Ionosphere.Header.t() ) :: {:ok, reference()} | {:error, term()}
Build an IONEX product from one sample per TEC grid node.
Every node of the grid the samples span must appear exactly once. The product
has RMS maps when any sample carries an RMS value and height maps when any
carries a height offset; a node without one is nil there.
shell_height_km and base_radius_km are kilometers and exponent is the
IONEX EXPONENT field. header carries the descriptive records the product
keeps: node samples say nothing about the mapping function or the program that
produced them, so they are stated here.
Flat node samples carry no map-presence field, so this entry cannot state RMS
or height maps that are declared yet hold no value at any node: samples whose
rms_tecu is nil everywhere give a product with no RMS map.
from_samples/1 states each stack explicitly and is the entry for that.
{:error, {:value_out_of_range, field, value}} names a radius that is an
integer larger in magnitude than the largest finite double, which has no
double to be read onto, or an exponent outside the signed 32-bit range the
core holds it in, alongside the refusals
Sidereon.GNSS.Ionosphere.TecSample,
Sidereon.GNSS.Ionosphere.Header and the core name.
@spec from_samples(Sidereon.GNSS.Ionosphere.TecGridSamples.t()) :: {:ok, reference()} | {:error, term()}
Build an IONEX product directly from whole-grid TEC samples.
Axes are degrees, shell and base radii are kilometers, TEC and RMS grids are
TECU and height grids are kilometers. A node without a value is nil; absent
RMS or height maps are nil, which is not the same as maps present with every
node nil.
The returned handle is accepted anywhere a parsed IONEX handle is, including
ionex_slant_delay/7 and ionex_to_string/1.
@spec galileo_nequick_g_delay( map(), number(), number(), number(), number(), NaiveDateTime.t() | tuple(), number() ) :: {:ok, float()} | {:error, term()}
Galileo NeQuick-G single-frequency ionospheric group delay, scaled to
frequency_hz.
coeffs carries the three Galileo broadcast effective-ionisation coefficients
as %{ai0: a0, ai1: a1, ai2: a2}. The receiver geodetic latitude/longitude and
the satellite azimuth/elevation are in degrees; epoch is a NaiveDateTime or
{{y, m, d}, {h, min, s}} tuple in Galileo system time. The NeQuick-G arm maps
the slant delay by elevation only, so azimuth_deg is accepted (to match the
shared ionosphere-model boundary) but does not change the result. Returns
{:ok, delay_m} (positive meters) or {:error, reason}, with the argument
refusals klobuchar_delay/7 names (a coefficient under its own name, :ai0,
:ai1 or :ai2) and the epoch refusals of
Sidereon.GNSS.Time.epoch_to_split_jd/1.
@spec ionex_header(reference()) :: {:ok, Sidereon.GNSS.Ionosphere.Header.t()} | {:error, term()}
The descriptive header records a parsed or sample-built product carries.
Returns {:ok, %Header{}}. mapping_function is nil where the product
declares no MAPPING FUNCTION record, one of :none, :cosz or :qfac for
the codes IONEX 1 names, and the code's own text for any other.
@spec ionex_slant_batch( reference(), [Sidereon.GNSS.Ionosphere.SlantRequest.t()], Sidereon.GNSS.Ionosphere.SlantPolicy.t() | keyword() | map() ) :: {:ok, ok: Sidereon.GNSS.Ionosphere.SlantEvaluation.t(), error: term()} | {:error, term()}
Batch IONEX slant delays, one result per request in request order.
requests is a list of Sidereon.GNSS.Ionosphere.SlantRequest. policy is as
in ionex_slant_evaluation/8.
Returns {:ok, results} where results has exactly one entry per request, in
request order, each {:ok, %SlantEvaluation{}} or {:error, reason} with that
row's own reason. A row that fails does not remove itself from the list, take
any other row with it, or come back as a zero delay.
A row fails on its own for a request that cannot be put into the form the
boundary takes as much as for one the product refuses: a sub-second epoch, a
calendar field that is not an integer, a field that is not a number, a value
past the range the boundary carries it in and a row that is not a
SlantRequest are each that row's failure, with the reasons
Sidereon.GNSS.Ionosphere.SlantRequest documents. A product refusal is one of
those ionex_slant_evaluation/8 documents.
The call itself fails only on something that is not a property of one row: a
policy the binding cannot read, a request list of the wrong shape, a handle
that is not an IONEX product ({:invalid_resource, :ionex}), or a failure of
the boundary itself, which is returned as it was reported rather than turned
into row values. Nothing that belongs to one row is allowed to become the
whole call's failure and take the other rows' results with it.
@spec ionex_slant_delay( reference(), number(), number(), number(), number(), NaiveDateTime.t() | tuple(), number() ) :: {:ok, float()} | {:error, term()}
IONEX vertical-TEC-grid slant ionospheric group delay, scaled to frequency_hz.
handle is a product handle from parse_ionex/1, load_ionex/1,
from_samples/1 or from_node_samples/5. The receiver geodetic
latitude/longitude and the satellite azimuth/elevation are in degrees; epoch
is a UTC NaiveDateTime or {{y, m, d}, {h, min, s}} tuple on a whole second,
UTC being the scale of IONEX map epochs (the pierce point rides on the IONEX
shell, so the receiver height is not used).
This is the strict convenience: it refuses a query outside the product's
coverage and one whose interpolation weights a node the product gives as
non-available, and it maps vertical TEC to the line of sight with the
single-layer 1/cos(z'). Use ionex_slant_evaluation/8 for other policies or
for the status of the value.
Returns {:ok, delay_m} (positive meters) or {:error, reason}, where
reason is one of the refusals ionex_slant_evaluation/8 documents.
@spec ionex_slant_evaluation( reference(), number(), number(), number(), number(), NaiveDateTime.t() | tuple(), number(), Sidereon.GNSS.Ionosphere.SlantPolicy.t() | keyword() | map() ) :: {:ok, Sidereon.GNSS.Ionosphere.SlantEvaluation.t()} | {:error, term()}
IONEX slant delay under explicit coverage, missing-node and mapping policies.
Arguments are as ionex_slant_delay/7, plus policy: a
Sidereon.GNSS.Ionosphere.SlantPolicy, or a keyword list or map of its
:coverage, :missing_nodes and :mapping choices. An unknown key or choice
is an error; nothing falls back to a default for a name it does not recognize.
Returns {:ok, %SlantEvaluation{}} carrying the delay and its independent
held, degraded and assumed_mapping status fields, which can all be set on
one value, or {:error, reason}.
Refusals
Where the product gives no value:
{:out_of_coverage, coverage_error}- the query is outside the product's epochs, latitudes or longitudes under:strictcoverage.coverage_erroris:epoch_before_first_map,:epoch_after_last_map,:latitude_out_of_rangeor:longitude_out_of_range.{:nodes_not_available, %Sidereon.GNSS.Ionosphere.NodeGap{}}- the interpolation weights a node the product gives as non-available, under:strictmissing nodes. The gap names every such node on each bracketing map.{:height_not_available, %Sidereon.GNSS.Ionosphere.HeightNode{}}- the product's height maps give no value at the named node, so the maps state no single-layer height to ride on.{:varying_heights, %Sidereon.GNSS.Ionosphere.HeightNode{}}- the height maps give two different heights, the second at the named node.{:mapping_function, declaration}-:declaredmapping against a product whoseMAPPING FUNCTIONdefines no factor.declarationis:none,:cosz,:qfac, the declared code's own text, or:absentwhere the product carries no such record.
Where an input is refused before or by the core:
{:invalid_field, field, reason}- the receiver position is outside the geodetic frame's range, with the core's own field and reason text. The frame takes latitude in[-90, 90]and longitude in[-180, 180]degrees, and a value outside either is reported as"lat_rad"or"lon_rad"because the frame names its radian fields.{:invalid_input, message}- the core refused elevation (which it takes in[0, 90]degrees), a non-positive or non-finite frequency, or another input, with the core's own message. The core carries no field/reason pair for these, so none is invented here.{:unhandled, message}- a core refusal variant this binding predates, with the core's own text. No other tag stands in for it.:non_integer_second_epochor{:value_out_of_range, field, value}- the epoch is not on a whole second, or a calendar field, a J2000 second or one of the numeric arguments is past the range the boundary carries it in. A numeric argument is past it when it is an integer larger in magnitude than the largest finite double, which has nof64to be read onto.{:invalid_epoch_field, field, value}- a date or clock field of a tuple epoch that is not an integer. The shared calendar helper takes those five fields as 32-bit integers, andhourarrived at by division is a float, since Elixir's/always gives one.{:invalid_request_field, field, value}- an argument that is not a number, named with the value that was given.{:invalid_resource, :ionex}- the handle is not a reference to an IONEX product, as described under "Handles" in this module's documentation.- the policy refusals of
Sidereon.GNSS.Ionosphere.SlantPolicy.
Serialize a parsed IONEX product back to standard IONEX text.
handle is a product handle from parse_ionex/1, load_ionex/1,
from_samples/1 or from_node_samples/5. This is the inverse of
parse_ionex/1: re-parsing the output reproduces the same TEC grids. The
serialization is deterministic and performs no I/O.
The writer is fallible: it refuses a value it cannot write exactly rather than
rounding it, and that refusal is returned as {:error, reason}.
Examples
{:ok, handle} = Sidereon.GNSS.Ionosphere.parse_ionex(ionex_bytes)
{:ok, text} = Sidereon.GNSS.Ionosphere.ionex_to_string(handle)
{:ok, _reparsed} = Sidereon.GNSS.Ionosphere.parse_ionex(text)
@spec klobuchar_delay( map(), number(), number(), number(), number(), NaiveDateTime.t() | tuple(), number() ) :: {:ok, float()} | {:error, term()}
GPS broadcast Klobuchar L1 ionospheric group delay, scaled to frequency_hz.
params carries the eight broadcast coefficients as
%{alpha: {a0, a1, a2, a3}, beta: {b0, b1, b2, b3}} (or lists). The receiver
geodetic latitude/longitude and the satellite azimuth/elevation are in
degrees; epoch is a NaiveDateTime or {{y, m, d}, {h, min, s}} tuple in
GPS time. Returns {:ok, delay_m} (positive meters) or {:error, reason}.
An argument the boundary cannot carry is named before the call:
{:invalid_double, field, value} for a value that is not a number (a
coefficient is named :alpha or :beta), {:value_out_of_range, field, value}
for an integer no double holds, and the epoch refusals of
Sidereon.GNSS.Time.second_of_day/1. A value the model refuses is
{:error, :invalid_input}.
Load and parse an IONEX file into a product handle.
Returns {:ok, reference()} or {:error, reason}.
Galileo NeQuick-G full slant ionospheric group delay (positive meters).
The full 3D slant TEC from nequick_g_stec/2 mapped to a dispersive group
delay on frequency_hz. coeffs and ray are as in nequick_g_stec/2, with
the same refusals, and frequency_hz is named as a double field.
Returns {:ok, delay_m} (positive meters) or {:error, reason}.
Galileo NeQuick-G full three-dimensional slant total electron content (TECU).
This is the reference-grade full 3D NeQuick-G integration, distinct from the
compact single-layer galileo_nequick_g_delay/7. The coeffs map carries the
broadcast effective-ionisation coefficients %{ai0:, ai1:, ai2:}. The ray
map carries both endpoints of the receiver-to-satellite line of sight, in the
reference algorithm's native units:
:month- month of the year,1..12:utc_hours- UTC time of day in hours,[0, 24]:station_lon_deg,:station_lat_deg,:station_height_m:satellite_lon_deg,:satellite_lat_deg,:satellite_height_m
Returns {:ok, stec_tecu} (slant TEC in TECU) or {:error, reason}. A
coefficient or ray field the boundary cannot carry is named before the call:
{:invalid_double, field, value} or {:value_out_of_range, field, value} for
a double field, {:invalid_integer, :month, value} for a month that is not
an integer and {:value_out_of_range, :month, value} for one outside 0..255;
a map missing a key is :bad_nequick_params or :bad_nequick_ray.
Parse an in-memory IONEX byte buffer into a product handle.
Returns {:ok, reference()} or {:error, reason}. The buffer is parsed
exactly once; the parsed grid is held as a resource handle.
Findings the reader reports without refusing the file are not returned here.
Use parse_ionex_with_warnings/1 to keep them.
@spec parse_ionex_with_warnings(binary()) :: {:ok, Sidereon.GNSS.Ionosphere.ParseResult.t()} | {:error, term()}
Parse an IONEX byte buffer, keeping what the reader reported about the file.
Returns {:ok, %ParseResult{}} or {:error, reason}. The warnings are in
reader order, and each carries every field its finding has alongside the
core's formatted message. An empty list means the reader found nothing to
report.
skipped_records is the count of records the parse passed over, which is a
separate thing from a warning: a skipped record raises no finding, so a parse
can report no warnings and still have skipped something. See
skipped_records/1.
@spec skipped_records(reference()) :: {:ok, non_neg_integer()} | {:error, term()}
The number of records a forgiving parse passed over.
The reader skips a block it does not support, such as START OF AUX DATA, and
a header record whose field it cannot read, counting each rather than dropping
it silently. A skip is not a warning and is not reported as one, so this is
the only way to ask whether anything in the file was left out. A product built
from samples has skipped nothing.
@spec tec_grid_samples(reference()) :: {:ok, Sidereon.GNSS.Ionosphere.TecGridSamples.t()} | {:error, term()}
Extract a product handle as whole-grid TEC samples.
This is the inverse of from_samples/1: rebuilding from the returned samples
reproduces every stored float and epoch. Axes are degrees, shell and base
radii are kilometers, TEC and RMS grids are TECU and height grids are
kilometers.
@spec tec_samples(reference()) :: {:ok, [Sidereon.GNSS.Ionosphere.TecSample.t()]} | {:error, term()}
Extract a product handle as one TEC sample per grid node.
Samples come back in [map][latitude][longitude] order. Each carries its own
epoch, latitude and longitude in degrees, vertical TEC and RMS in TECU, and a
height offset in kilometers, with nil for any value the node does not have.