use astrodynamics::math::least_squares::{
self, solve_trf, LeastSquaresProblem, SolveOptions, Status,
};
use nalgebra::DVector;
use crate::dop::{dop, dop_multi, Dop, LineOfSight};
use crate::frame::{ItrfPositionM, Wgs84Geodetic};
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::ionex::{klobuchar_native, KlobucharParams};
use crate::sp3::Sp3;
use crate::tropo::slant_components;
pub const C_M_S: f64 = 299_792_458.0;
pub const F_L1_HZ: f64 = 1_575.42e6;
pub const F_B1I_HZ: f64 = 1_561.098e6;
pub(crate) const fn carrier_frequency_hz(system: GnssSystem) -> Option<f64> {
match system {
GnssSystem::Gps | GnssSystem::Galileo => Some(F_L1_HZ),
GnssSystem::BeiDou => Some(F_B1I_HZ),
_ => None,
}
}
pub const OMEGA_E_DOT_RAD_S: f64 = 7.2921151467e-5;
const AU_KM: f64 = 149_597_870.700;
const WGS84_A_KM: f64 = 6378.137;
const WGS84_F: f64 = 1.0 / 298.257223563;
const WGS84_E2: f64 = 2.0 * WGS84_F - WGS84_F * WGS84_F;
const PI: f64 = std::f64::consts::PI;
const TAU: f64 = std::f64::consts::TAU;
pub const TRANSMIT_TIME_ITERATIONS: usize = 2;
pub const ELEVATION_MASK_RAD: f64 = 10.0 * PI / 180.0;
pub const SIGMA0_M: f64 = 1.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Observation {
pub satellite_id: GnssSatelliteId,
pub pseudorange_m: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RejectionReason {
NoEphemeris,
LowElevation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RejectedSat {
pub satellite_id: GnssSatelliteId,
pub reason: RejectionReason,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SolutionMetadata {
pub iterations: usize,
pub converged: bool,
pub status: Status,
pub ionosphere_applied: bool,
pub troposphere_applied: bool,
}
#[derive(Debug, Clone)]
pub struct ReceiverSolution {
pub position: ItrfPositionM,
pub geodetic: Option<Wgs84Geodetic>,
pub rx_clock_s: f64,
pub system_clocks_s: Vec<(GnssSystem, f64)>,
pub dop: Option<Dop>,
pub residuals_m: Vec<f64>,
pub used_sats: Vec<GnssSatelliteId>,
pub rejected_sats: Vec<RejectedSat>,
pub metadata: SolutionMetadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Corrections {
pub ionosphere: bool,
pub troposphere: bool,
}
impl Corrections {
pub const NONE: Self = Self {
ionosphere: false,
troposphere: false,
};
pub const IONO: Self = Self {
ionosphere: true,
troposphere: false,
};
pub const IONO_TROPO: Self = Self {
ionosphere: true,
troposphere: true,
};
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KlobucharCoeffs {
pub alpha: [f64; 4],
pub beta: [f64; 4],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SurfaceMet {
pub pressure_hpa: f64,
pub temperature_k: f64,
pub relative_humidity: f64,
}
#[derive(Debug, Clone)]
pub struct SolveInputs {
pub observations: Vec<Observation>,
pub t_rx_j2000_s: f64,
pub t_rx_second_of_day_s: f64,
pub day_of_year: f64,
pub initial_guess: [f64; 4],
pub corrections: Corrections,
pub klobuchar: KlobucharCoeffs,
pub beidou_klobuchar: Option<KlobucharCoeffs>,
pub met: SurfaceMet,
}
#[derive(Debug, Clone)]
pub enum SppError {
TooFewSatellites {
used: usize,
required: usize,
},
Singular(least_squares::SolveError),
DuplicateObservation {
satellite: GnssSatelliteId,
},
EphemerisLost {
satellite: GnssSatelliteId,
},
IonosphereUnsupported {
satellite: GnssSatelliteId,
},
}
impl core::fmt::Display for SppError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SppError::TooFewSatellites { used, required } => write!(
f,
"only {used} usable satellites; need at least {required} \
(3 position + 1 clock per GNSS)"
),
SppError::Singular(e) => write!(f, "degenerate geometry: {e}"),
SppError::DuplicateObservation { satellite } => {
write!(f, "satellite {satellite} observed more than once")
}
SppError::EphemerisLost { satellite } => {
write!(f, "satellite {satellite} lost ephemeris during the solve")
}
SppError::IonosphereUnsupported { satellite } => write!(
f,
"ionosphere correction has no modeled carrier frequency for {satellite}"
),
}
}
}
impl std::error::Error for SppError {}
impl From<least_squares::SolveError> for SppError {
fn from(e: least_squares::SolveError) -> Self {
SppError::Singular(e)
}
}
pub(crate) fn geodetic_from_ecef_m(x_m: f64, y_m: f64, z_m: f64) -> Wgs84Geodetic {
let x = x_m / 1000.0;
let y = y_m / 1000.0;
let z = z_m / 1000.0;
let x_au = x / AU_KM;
let y_au = y / AU_KM;
let z_au = z / AU_KM;
let a_au = WGS84_A_KM / AU_KM;
let r_xy = (x_au * x_au + y_au * y_au).sqrt();
let lon_raw = y_au.atan2(x_au);
let mut lon_shifted = (lon_raw - PI) % TAU;
if lon_shifted < 0.0 {
lon_shifted += TAU;
}
let lon = lon_shifted - PI;
let mut lat = z_au.atan2(r_xy);
let mut a_c = 0.0;
let mut hyp = 0.0;
for _ in 0..3 {
let sin_lat = lat.sin();
let e2_sin_lat = WGS84_E2 * sin_lat;
a_c = a_au / (1.0 - e2_sin_lat * sin_lat).sqrt();
hyp = z_au + a_c * e2_sin_lat;
lat = hyp.atan2(r_xy);
}
let height_au = (hyp * hyp + r_xy * r_xy).sqrt() - a_c;
let height_m = height_au * AU_KM * 1000.0;
Wgs84Geodetic {
lat_rad: lat,
lon_rad: lon,
height_m,
}
}
pub(crate) struct AzEl {
pub geodetic: Wgs84Geodetic,
pub az_rad: f64,
pub el_rad: f64,
}
pub(crate) fn az_el_from_ecef(rx_ecef_m: [f64; 3], sat_ecef_m: [f64; 3]) -> AzEl {
let dx = sat_ecef_m[0] - rx_ecef_m[0];
let dy = sat_ecef_m[1] - rx_ecef_m[1];
let dz = sat_ecef_m[2] - rx_ecef_m[2];
let geo = geodetic_from_ecef_m(rx_ecef_m[0], rx_ecef_m[1], rx_ecef_m[2]);
let sin_lat = geo.lat_rad.sin();
let cos_lat = geo.lat_rad.cos();
let sin_lon = geo.lon_rad.sin();
let cos_lon = geo.lon_rad.cos();
let e = -sin_lon * dx + cos_lon * dy;
let n = -sin_lat * cos_lon * dx - sin_lat * sin_lon * dy + cos_lat * dz;
let u = cos_lat * cos_lon * dx + cos_lat * sin_lon * dy + sin_lat * dz;
let rng = (e * e + n * n + u * u).sqrt();
let el = (u / rng).asin();
let mut az = e.atan2(n);
if az < 0.0 {
az += TAU;
}
AzEl {
geodetic: geo,
az_rad: az,
el_rad: el,
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) struct SatModel {
pub tau_s: f64,
pub t_tx_j2000_s: f64,
pub sat_ecef_m: [f64; 3],
pub dt_sat_s: f64,
pub theta_rad: f64,
pub sat_rot_ecef_m: [f64; 3],
pub rho_m: f64,
pub az_rad: f64,
pub el_rad: f64,
pub iono_m: f64,
pub tropo_m: f64,
pub p_hat_m: f64,
}
pub trait EphemerisSource {
fn position_clock_at_j2000_s(
&self,
sat: GnssSatelliteId,
t_j2000_s: f64,
) -> Option<([f64; 3], f64)>;
}
impl EphemerisSource for Sp3 {
fn position_clock_at_j2000_s(
&self,
sat: GnssSatelliteId,
t_j2000_s: f64,
) -> Option<([f64; 3], f64)> {
let state = self.position_at_j2000_seconds(sat, t_j2000_s).ok()?;
let clk = state.clock_s?;
Some((state.position.as_array(), clk))
}
}
fn klobuchar_for(system: GnssSystem, inputs: &SolveInputs) -> &KlobucharCoeffs {
match (system, &inputs.beidou_klobuchar) {
(GnssSystem::BeiDou, Some(bds)) => bds,
_ => &inputs.klobuchar,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn sat_model(
eph: &dyn EphemerisSource,
sat: GnssSatelliteId,
rx_ecef_m: [f64; 3],
b_m: f64,
p_meas_m: f64,
t_rx_j2000_s: f64,
t_rx_second_of_day_s: f64,
day_of_year: f64,
corrections: Corrections,
klobuchar: &KlobucharCoeffs,
met: &SurfaceMet,
) -> Option<SatModel> {
let mut tau = p_meas_m / C_M_S;
let mut t_tx = t_rx_j2000_s - tau;
let mut sat_pos = [0.0f64; 3];
let mut dt_sat = 0.0f64;
for _ in 0..TRANSMIT_TIME_ITERATIONS {
let (pos, clk) = eph.position_clock_at_j2000_s(sat, t_tx)?;
sat_pos = pos;
dt_sat = clk;
let d0x = sat_pos[0] - rx_ecef_m[0];
let d0y = sat_pos[1] - rx_ecef_m[1];
let d0z = sat_pos[2] - rx_ecef_m[2];
let rho0 = (d0x * d0x + d0y * d0y + d0z * d0z).sqrt();
tau = rho0 / C_M_S;
t_tx = t_rx_j2000_s - tau;
}
let theta = OMEGA_E_DOT_RAD_S * tau;
let cos_t = theta.cos();
let sin_t = theta.sin();
let sat_rot = [
cos_t * sat_pos[0] + sin_t * sat_pos[1],
-sin_t * sat_pos[0] + cos_t * sat_pos[1],
sat_pos[2],
];
let drx = sat_rot[0] - rx_ecef_m[0];
let dry = sat_rot[1] - rx_ecef_m[1];
let drz = sat_rot[2] - rx_ecef_m[2];
let rho = (drx * drx + dry * dry + drz * drz).sqrt();
let g = az_el_from_ecef(rx_ecef_m, sat_rot);
let mut iono_m = 0.0;
let mut tropo_m = 0.0;
if corrections.ionosphere {
let lat_deg = g.geodetic.lat_rad * 180.0 / PI;
let lon_deg = g.geodetic.lon_rad * 180.0 / PI;
let az_deg = g.az_rad * 180.0 / PI;
let el_deg = g.el_rad * 180.0 / PI;
let freq_hz = carrier_frequency_hz(sat.system).unwrap_or(F_L1_HZ);
iono_m = klobuchar_native(
&KlobucharParams {
alpha: klobuchar.alpha,
beta: klobuchar.beta,
},
lat_deg,
lon_deg,
az_deg,
el_deg,
t_rx_second_of_day_s,
freq_hz,
);
}
if corrections.troposphere {
tropo_m = slant_components(
g.el_rad,
g.geodetic.lat_rad,
g.geodetic.lon_rad,
g.geodetic.height_m,
met.pressure_hpa,
met.temperature_k,
met.relative_humidity,
day_of_year,
)
.slant_m;
}
let p_hat = rho + b_m - C_M_S * dt_sat + iono_m + tropo_m;
Some(SatModel {
tau_s: tau,
t_tx_j2000_s: t_tx,
sat_ecef_m: sat_pos,
dt_sat_s: dt_sat,
theta_rad: theta,
sat_rot_ecef_m: sat_rot,
rho_m: rho,
az_rad: g.az_rad,
el_rad: g.el_rad,
iono_m,
tropo_m,
p_hat_m: p_hat,
})
}
pub(crate) struct Selection {
pub used: Vec<GnssSatelliteId>,
pub rejected: Vec<RejectedSat>,
#[allow(dead_code)]
pub sqrt_weights: Vec<f64>,
pub weights: Vec<f64>,
}
pub(crate) fn select_sats(eph: &dyn EphemerisSource, inputs: &SolveInputs) -> Selection {
let rx0 = [
inputs.initial_guess[0],
inputs.initial_guess[1],
inputs.initial_guess[2],
];
let b0 = inputs.initial_guess[3];
let mut obs: Vec<&Observation> = inputs.observations.iter().collect();
obs.sort_by_key(|o| o.satellite_id);
let mut used = Vec::new();
let mut rejected = Vec::new();
let mut sqrt_weights = Vec::new();
let mut weights = Vec::new();
for ob in obs {
let model = sat_model(
eph,
ob.satellite_id,
rx0,
b0,
ob.pseudorange_m,
inputs.t_rx_j2000_s,
inputs.t_rx_second_of_day_s,
inputs.day_of_year,
inputs.corrections,
klobuchar_for(ob.satellite_id.system, inputs),
&inputs.met,
);
let model = match model {
Some(m) => m,
None => {
rejected.push(RejectedSat {
satellite_id: ob.satellite_id,
reason: RejectionReason::NoEphemeris,
});
continue;
}
};
if model.el_rad < ELEVATION_MASK_RAD {
rejected.push(RejectedSat {
satellite_id: ob.satellite_id,
reason: RejectionReason::LowElevation,
});
continue;
}
let sin_el = model.el_rad.sin();
let weight = (sin_el * sin_el) / (SIGMA0_M * SIGMA0_M);
used.push(ob.satellite_id);
weights.push(weight);
sqrt_weights.push(weight.sqrt());
}
Selection {
used,
rejected,
sqrt_weights,
weights,
}
}
pub(crate) fn clock_systems(used: &[GnssSatelliteId]) -> Vec<GnssSystem> {
let mut systems: Vec<GnssSystem> = used.iter().map(|s| s.system).collect();
systems.sort_unstable();
systems.dedup();
systems
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn residual_unweighted(
eph: &dyn EphemerisSource,
used: &[GnssSatelliteId],
obs_by_id: &[(GnssSatelliteId, f64)],
x: &[f64],
inputs: &SolveInputs,
) -> Result<Vec<f64>, GnssSatelliteId> {
let rx = [x[0], x[1], x[2]];
let systems = clock_systems(used);
let mut out = Vec::with_capacity(used.len());
for &sat in used {
let p_meas = obs_by_id
.iter()
.find(|(id, _)| *id == sat)
.map(|(_, p)| *p)
.ok_or(sat)?;
let sys_idx = systems.iter().position(|s| *s == sat.system).unwrap_or(0);
let b = x[3 + sys_idx];
let m = sat_model(
eph,
sat,
rx,
b,
p_meas,
inputs.t_rx_j2000_s,
inputs.t_rx_second_of_day_s,
inputs.day_of_year,
inputs.corrections,
klobuchar_for(sat.system, inputs),
&inputs.met,
)
.ok_or(sat)?;
out.push(p_meas - m.p_hat_m);
}
Ok(out)
}
pub fn solve(
eph: &dyn EphemerisSource,
inputs: &SolveInputs,
with_geodetic: bool,
) -> Result<ReceiverSolution, SppError> {
let mut ids: Vec<GnssSatelliteId> =
inputs.observations.iter().map(|o| o.satellite_id).collect();
ids.sort_unstable();
if let Some(w) = ids.windows(2).find(|w| w[0] == w[1]) {
return Err(SppError::DuplicateObservation { satellite: w[0] });
}
if inputs.corrections.ionosphere {
if let Some(sat) = ids
.iter()
.find(|s| carrier_frequency_hz(s.system).is_none())
{
return Err(SppError::IonosphereUnsupported { satellite: *sat });
}
}
let sel = select_sats(eph, inputs);
let systems = clock_systems(&sel.used);
let n_clocks = systems.len();
let n_params = 3 + n_clocks.max(1);
if sel.used.len() < n_params {
return Err(SppError::TooFewSatellites {
used: sel.used.len(),
required: n_params,
});
}
let obs_by_id: Vec<(GnssSatelliteId, f64)> = inputs
.observations
.iter()
.map(|o| (o.satellite_id, o.pseudorange_m))
.collect();
let used = sel.used.clone();
let inputs_ref = inputs.clone();
let obs_ref = obs_by_id.clone();
let eph_ref = eph;
let n_used = used.len();
let lost = std::rc::Rc::new(std::cell::Cell::new(None::<GnssSatelliteId>));
let lost_in = lost.clone();
let residual = move |x: &DVector<f64>| -> DVector<f64> {
match residual_unweighted(eph_ref, &used, &obs_ref, x.as_slice(), &inputs_ref) {
Ok(r) => DVector::from_vec(r),
Err(sat) => {
lost_in.set(Some(sat));
DVector::from_vec(vec![0.0; n_used])
}
}
};
let mut x0v = inputs.initial_guess.to_vec();
x0v.extend(std::iter::repeat_n(0.0, n_clocks - 1));
let x0 = DVector::from_vec(x0v);
let weights = DVector::from_row_slice(&sel.weights);
let problem = LeastSquaresProblem::with_weights(residual, x0, weights);
let opts = SolveOptions {
gtol: 1e-14,
ftol: 1e-15,
xtol: 1e-14,
max_nfev: 400,
};
let report_result = solve_trf(&problem, &opts);
if let Some(satellite) = lost.get() {
return Err(SppError::EphemerisLost { satellite });
}
let report = report_result?;
let xs = &report.x;
let position = ItrfPositionM::new(xs[0], xs[1], xs[2]);
let rx_clock_s = xs[3] / C_M_S;
let system_clocks_s: Vec<(GnssSystem, f64)> = systems
.iter()
.enumerate()
.map(|(i, &sys)| (sys, xs[3 + i] / C_M_S))
.collect();
let geodetic = if with_geodetic {
Some(geodetic_from_ecef_m(xs[0], xs[1], xs[2]))
} else {
None
};
let residuals_m = residual_unweighted(eph, &sel.used, &obs_by_id, xs.as_slice(), inputs)
.map_err(|satellite| SppError::EphemerisLost { satellite })?;
let rx_ecef = [xs[0], xs[1], xs[2]];
let geo = geodetic_from_ecef_m(xs[0], xs[1], xs[2]);
let mut los = Vec::with_capacity(sel.used.len());
let mut clock_index = Vec::with_capacity(sel.used.len());
for &sat in &sel.used {
let p_meas = obs_by_id
.iter()
.find(|(id, _)| *id == sat)
.map(|(_, p)| *p)
.ok_or(SppError::EphemerisLost { satellite: sat })?;
let m = sat_model(
eph,
sat,
rx_ecef,
xs[3],
p_meas,
inputs.t_rx_j2000_s,
inputs.t_rx_second_of_day_s,
inputs.day_of_year,
inputs.corrections,
klobuchar_for(sat.system, inputs),
&inputs.met,
)
.ok_or(SppError::EphemerisLost { satellite: sat })?;
let dx = m.sat_rot_ecef_m[0] - rx_ecef[0];
let dy = m.sat_rot_ecef_m[1] - rx_ecef[1];
let dz = m.sat_rot_ecef_m[2] - rx_ecef[2];
let n = (dx * dx + dy * dy + dz * dz).sqrt();
los.push(LineOfSight::new(dx / n, dy / n, dz / n));
let idx = systems.iter().position(|s| *s == sat.system).unwrap_or(0);
clock_index.push(idx);
}
let dop_result = if n_clocks == 1 {
dop(&los, &sel.weights, geo).ok()
} else {
dop_multi(&los, &clock_index, n_clocks, &sel.weights, geo).ok()
};
let converged = matches!(
report.status,
Status::GradientTolerance | Status::CostTolerance | Status::StepTolerance
);
Ok(ReceiverSolution {
position,
geodetic,
rx_clock_s,
system_clocks_s,
dop: dop_result,
residuals_m,
used_sats: sel.used,
rejected_sats: sel.rejected,
metadata: SolutionMetadata {
iterations: report.iterations,
converged,
status: report.status,
ionosphere_applied: inputs.corrections.ionosphere,
troposphere_applied: inputs.corrections.troposphere,
},
})
}
#[cfg(test)]
pub(crate) mod test_support {
use super::*;
pub fn geodetic_from_ecef_m_for_test(x_m: f64, y_m: f64, z_m: f64) -> Wgs84Geodetic {
geodetic_from_ecef_m(x_m, y_m, z_m)
}
pub fn sat_model_for_test(
eph: &dyn EphemerisSource,
sat: GnssSatelliteId,
rx: [f64; 3],
b_m: f64,
p_meas: f64,
t_rx_j2000_s: f64,
sod: f64,
doy: f64,
corrections: Corrections,
klobuchar: &KlobucharCoeffs,
met: &SurfaceMet,
) -> Option<SatModel> {
sat_model(
eph,
sat,
rx,
b_m,
p_meas,
t_rx_j2000_s,
sod,
doy,
corrections,
klobuchar,
met,
)
}
pub fn itrs_to_geodetic_core_km(x_km: f64, y_km: f64, z_km: f64) -> (f64, f64, f64) {
astrodynamics::frames::transforms::itrs_to_geodetic_compute(x_km, y_km, z_km)
}
}
#[cfg(test)]
mod tests;