Sidereon.GNSS.PrecisePositioning (Sidereon v0.25.0)

Copy Markdown View Source

Carrier-phase precise-positioning primitives.

This is the first precise-positioning layer above the code and carrier-phase combinations in Sidereon.GNSS.IonosphereFree / Sidereon.GNSS.CarrierPhase. It solves one SP3-backed epoch from dual-frequency ionosphere-free code and phase observations:

P_IF_i = rho_i(x) + b - c * dt_sat_i + T_i
L_IF_i = rho_i(x) + b - c * dt_sat_i + T_i + N_i

where x is the receiver ECEF position, b is the receiver clock in metres, T_i is the optional a-priori slant tropospheric delay plus any estimated residual zenith delay mapped to the line of sight, and N_i is one float carrier-phase ambiguity per satellite, also in metres. The single-epoch state is linearized and iterated over [x, y, z, b, N_1, N_2, ...].

solve_float/4 solves one epoch. solve_float_epochs/3 solves a static multi-epoch arc with one receiver position, one receiver clock per epoch, and one ambiguity per satellite held constant across the arc. That multi-epoch model is the first step where carrier phase can tighten position instead of being absorbed entirely by one ambiguity per epoch. Multi-epoch and fixed solves can also estimate one residual zenith troposphere delay over the arc (estimate_ztd: true) after the a-priori Saastamoinen/Niell correction.

solve_fixed_epochs/3 starts from the same multi-epoch float model, runs LAMBDA/MLAMBDA integer least-squares on an explicit caller-supplied wavelength grid, then re-solves position and per-epoch clocks with those ambiguities held fixed.

Observation format

Observations may be maps or tuples:

%{satellite_id: "G05", code_m: 24_000_000.0, phase_m: 24_012_345.0}
{"G05", 24_000_000.0, 24_012_345.0}

code_m and phase_m should normally be ionosphere-free combinations. Use Sidereon.GNSS.IonosphereFree.iono_free/4 and Sidereon.GNSS.IonosphereFree.iono_free_phase_cycles/4 to form them from raw dual-frequency RINEX observations.

Summary

Types

A set of code/phase observations for one epoch.

A dual-frequency ionosphere-free code/phase observation.

A receiver ECEF position in metres.

Functions

Solve a static multi-epoch position with integer-fixed ambiguities.

Solve a float-ambiguity carrier-phase position for one SP3-backed epoch.

Solve a static multi-epoch float-ambiguity carrier-phase position.

Solve a static multi-epoch integer-fixed PPP arc with SPP auto-initialization, delegating the whole driver (auto-init seed, the float solve, the LAMBDA integer fix, and the ambiguity-conditioned re-solve) to the sidereon-core solve_ppp_auto_init_fixed kernel.

Solve a static multi-epoch float PPP arc with SPP auto-initialization, delegating the whole driver (the SPP code seed, the mean-position and per-epoch clock seeds, the phase-minus-code float ambiguity seeds, and the static float solve) to the sidereon-core solve_ppp_auto_init_float kernel.

Solve a static multi-epoch integer-fixed PPP arc from an existing float solution.

Solve a static multi-epoch float PPP arc from an explicit initial state.

Types

epoch_observations()

@type epoch_observations() ::
  %{epoch: NaiveDateTime.t(), observations: [observation()]}
  | {NaiveDateTime.t(), [observation()]}

A set of code/phase observations for one epoch.

observation()

@type observation() ::
  %{satellite_id: String.t(), code_m: number(), phase_m: number()}
  | {String.t(), number(), number()}

A dual-frequency ionosphere-free code/phase observation.

receiver()

@type receiver() ::
  {number(), number(), number()}
  | %{x_m: number(), y_m: number(), z_m: number()}

A receiver ECEF position in metres.

Functions

solve_fixed_epochs(source, epoch_observations, opts \\ [])

@spec solve_fixed_epochs(Sidereon.GNSS.SP3.t(), [epoch_observations()], keyword()) ::
  {:ok, Sidereon.GNSS.PrecisePositioning.FixedSolution.t()} | {:error, term()}

Solve a static multi-epoch position with integer-fixed ambiguities.

The function first solves the float multi-epoch model (solve_float_epochs/3), converts each float ambiguity from metres to cycles using the explicit :ambiguity_wavelength_m option, runs the LAMBDA/MLAMBDA integer least-squares search, and re-solves the receiver position and per-epoch clocks with the best integer ambiguities held fixed.

Required option

  • :ambiguity_wavelength_m - either a positive scalar wavelength in metres for every satellite, or a map %{"G05" => wavelength_m, ...}.

Additional options

  • :integer_ratio_threshold - minimum second-best / best weighted-score ratio for metadata.integer_status == :fixed (default 3.0).
  • :integer_search_radius_cycles / :integer_candidate_limit - retained and still validated for backward compatibility, but no longer bound the search: integer resolution uses the LAMBDA method (decorrelation + reduction + MLAMBDA search), which finds the true integer-least-squares optimum for any geometry with no search box, so it cannot return {:error, {:too_many_integer_candidates, ...}}.
  • :ambiguity_offset_m - optional scalar or %{"G05" => offset_m, ...} map subtracted from each float ambiguity before converting to cycles and added back after fixing (default 0.0). This is mainly for affine carrier-phase combinations such as wide-lane/narrow-lane fixing.

The fixed solution is returned even when the ratio test is not met; in that case metadata.integer_status is :not_fixed so callers can reject it.

solve_float(source, observations, epoch, opts \\ [])

@spec solve_float(
  Sidereon.GNSS.SP3.t(),
  [observation()],
  NaiveDateTime.t(),
  keyword()
) ::
  {:ok, Sidereon.GNSS.PrecisePositioning.Solution.t()} | {:error, term()}

Solve a float-ambiguity carrier-phase position for one SP3-backed epoch.

source is a loaded Sidereon.GNSS.SP3 product. observations is a list of ionosphere-free code/phase pairs for one epoch. epoch is interpreted in the SP3 product's time scale.

Options

  • :initial_guess - {x_m, y_m, z_m, clock_m}. If omitted, the code observations are first passed through Sidereon.GNSS.Positioning.solve/4 with ionosphere/troposphere disabled, and that code-only solution seeds the float solve.
  • :spp_initial_guess - code-only SPP seed used only when :initial_guess is omitted (default {0, 0, 0, 0}).
  • :code_sigma_m - code row standard deviation (default 1.0 m).
  • :phase_sigma_m - phase row standard deviation (default 0.01 m).
  • :elevation_weighting - when true, scale both code and phase row standard deviations by 1 / sin(elevation) so low-elevation observations contribute less to the float, fixed, and ambiguity-covariance solves (default false).
  • :max_iterations - maximum nonlinear iterations (default 8).
  • :position_tolerance_m - position-update convergence threshold (default 1.0e-4 m).
  • :clock_tolerance_m - receiver-clock update threshold (default 1.0e-4 m).
  • :troposphere - apply an a-priori Saastamoinen/Niell slant tropospheric delay to both code and phase (default false).
  • :pressure_hpa - surface pressure in hPa when :troposphere is true (default 1013.25).
  • :temperature_k - surface temperature in kelvin when :troposphere is true (default 288.15).
  • :relative_humidity - relative humidity fraction when :troposphere is true (default 0.5).
  • :tropo_mapping - the tropospheric mapping function when :troposphere is true: :niell (the climatological default) or {:vmf1, samples} where samples is a non-empty, ascending list of %{mjd:, ah:, aw:} site-wise VMF1 a-coefficient samples (the Saastamoinen zenith delays are unchanged; only the mapping differs).
  • :estimate_ztd - on multi-epoch/fixed solves only, estimate one residual zenith troposphere delay in metres over the whole static arc, mapped with the Niell wet mapping factor. Requires troposphere: true (default false).
  • :ztd_tolerance_m - residual-ZTD update convergence threshold when :estimate_ztd is true (default 1.0e-4 m).

Returns {:ok, %Solution{}} or {:error, reason}. Reasons include :no_observations, {:too_few_satellites, used, 4}, {:duplicate_observation, sat}, {:invalid_observation, entry}, :invalid_initial_guess, {:invalid_sigma, key}, {:invalid_option, key}, {:code_seed_failed, reason}, {:no_ephemeris, sat, reason}, {:troposphere_failed, sat, reason}, and :singular_geometry. If the iteration limit is reached after a valid solve step, the function returns a solution with metadata.converged == false and metadata.status == :max_iterations so callers can inspect the residuals and decide whether to reject it.

solve_float_epochs(source, epoch_observations, opts \\ [])

@spec solve_float_epochs(Sidereon.GNSS.SP3.t(), [epoch_observations()], keyword()) ::
  {:ok, Sidereon.GNSS.PrecisePositioning.MultiEpochSolution.t()}
  | {:error, term()}

Solve a static multi-epoch float-ambiguity carrier-phase position.

epoch_observations is a list of %{epoch: epoch, observations: obs} maps or {epoch, obs} tuples. The receiver position is static across the whole arc, each epoch gets its own receiver clock, and each satellite gets one ambiguity held constant across every epoch where that satellite appears.

This model is still float ambiguity only. It does not fix integer ambiguities or estimate a stochastic PPP process, but it lets changing geometry across the arc separate position from carrier ambiguities.

Options are the same as solve_float/4, plus:

  • :ambiguity_tolerance_m - maximum ambiguity-update convergence threshold (default 1.0e-4 m).

Returns {:ok, %MultiEpochSolution{}} or {:error, reason}. Reasons include :no_epochs, {:too_few_epochs, used, 2}, {:duplicate_epoch, epoch}, {:too_few_epoch_observations, epoch, used, 4}, {:too_few_equations, equations, unknowns}, and the same observation, option, ephemeris, seeding, and geometry errors as solve_float/4.

solve_ppp_auto_init_fixed(source, epoch_observations, opts \\ [])

@spec solve_ppp_auto_init_fixed(Sidereon.GNSS.SP3.t(), list(), keyword()) ::
  {:ok, map()} | {:error, term()}

Solve a static multi-epoch integer-fixed PPP arc with SPP auto-initialization, delegating the whole driver (auto-init seed, the float solve, the LAMBDA integer fix, and the ambiguity-conditioned re-solve) to the sidereon-core solve_ppp_auto_init_fixed kernel.

Options match solve_fixed_epochs/3 plus the auto-init options documented on solve_ppp_auto_init_float/3. Returns {:ok, solution} or {:error, reason}.

solve_ppp_auto_init_float(source, epoch_observations, opts \\ [])

@spec solve_ppp_auto_init_float(Sidereon.GNSS.SP3.t(), list(), keyword()) ::
  {:ok, map()} | {:error, term()}

Solve a static multi-epoch float PPP arc with SPP auto-initialization, delegating the whole driver (the SPP code seed, the mean-position and per-epoch clock seeds, the phase-minus-code float ambiguity seeds, and the static float solve) to the sidereon-core solve_ppp_auto_init_float kernel.

This is a thin delegation to the core auto-init driver: the seed the existing solve_float_epochs/3 builds in Elixir is now formed inside the kernel. The epoch_observations and the measurement/tropo/correction options are the same as solve_float_epochs/3. Auto-init specific options:

  • :initial_guess - %{position: {x, y, z}, clock_m: c} to bypass the SPP seed entirely, or absent to run the per-epoch SPP auto-init
  • :spp_initial_guess - {x, y, z, b} SPP cold-start (default all-zero)
  • :spp_troposphere - apply the troposphere in the SPP seed (default false)
  • :spp_met - %{pressure_hpa:, temperature_k:, relative_humidity:} for the SPP seed troposphere (default standard atmosphere)

Returns {:ok, solution} or {:error, reason}.

solve_ppp_fixed(source, epoch_observations, float_solution, opts \\ [])

Solve a static multi-epoch integer-fixed PPP arc from an existing float solution.

solve_ppp_float(source, epoch_observations, initial_state, opts \\ [])

@spec solve_ppp_float(Sidereon.GNSS.SP3.t(), list(), map(), keyword()) ::
  {:ok, Sidereon.GNSS.PrecisePositioning.MultiEpochSolution.t()}
  | {:error, term()}

Solve a static multi-epoch float PPP arc from an explicit initial state.