use std::collections::HashMap;
use hifitime::Epoch;
use log::{debug, error, warn};
use thiserror::Error;
use nyx::cosmic::eclipse::{eclipse_state, EclipseState};
use nyx::cosmic::Orbit;
use nyx::md::prelude::{Arc, Cosm};
use nyx::md::prelude::{Bodies, Frame, LightTimeCalc};
use gnss::prelude::SV;
use nalgebra::{DVector, Matrix3, MatrixXx4, Vector3};
use crate::{
apriori::AprioriPosition,
bias::{IonosphericBias, TroposphericBias},
candidate::Candidate,
cfg::Config,
solutions::{PVTSVData, PVTSolution, PVTSolutionType},
};
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub enum Mode {
#[default]
SPP,
PPP,
}
impl std::fmt::Display for Mode {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::SPP => write!(fmt, "SPP"),
Self::PPP => write!(fmt, "PPP"),
}
}
}
#[derive(Debug, Clone, Error)]
pub enum Error {
#[error("need more candidates to resolve a {0} a solution")]
NotEnoughInputCandidates(PVTSolutionType),
#[error("not enough candidates fit criteria")]
NotEnoughFittingCandidates,
#[error("failed to invert navigation matrix")]
MatrixInversionError,
#[error("NaN: input or output error")]
HasNan,
#[error("undefined apriori position")]
UndefinedAprioriPosition,
#[error("at least one pseudo range observation is mandatory")]
NeedsAtLeastOnePseudoRange,
#[error("failed to model or measure ionospheric delay")]
MissingIonosphericDelayValue,
#[error("unresolved state: interpolation should have passed")]
UnresolvedState,
#[error("unable to form signal combination")]
SignalRecombination,
#[error("physical non sense: rx prior tx")]
PhysicalNonSenseRxPriorTx,
#[error("physical non sense: t_rx is too late")]
PhysicalNonSenseRxTooLate,
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct InterpolationResult {
pub position: Vector3<f64>,
pub velocity: Option<Vector3<f64>>,
pub elevation: f64,
pub azimuth: f64,
}
impl InterpolationResult {
pub(crate) fn position(&self) -> (f64, f64, f64) {
(self.position[0], self.position[1], self.position[2])
}
pub(crate) fn velocity(&self) -> Option<(f64, f64, f64)> {
let velocity = self.velocity?;
Some((velocity[0], velocity[1], velocity[2]))
}
pub(crate) fn orbit(&self, dt: Epoch, frame: Frame) -> Orbit {
let (x, y, z) = self.position();
let (v_x, v_y, v_z) = self.velocity().unwrap_or((0.0_f64, 0.0_f64, 0.0_f64));
Orbit::cartesian(x, y, z, v_x / 1000.0, v_y / 1000.0, v_z / 1000.0, dt, frame)
}
}
#[derive(Debug, Clone)]
pub struct Solver<I>
where
I: Fn(Epoch, SV, usize) -> Option<InterpolationResult>,
{
pub cfg: Config,
pub mode: Mode,
pub apriori: AprioriPosition,
pub interpolator: I,
cosmic: Arc<Cosm>,
earth_frame: Frame,
sun_frame: Frame,
}
impl<I: std::ops::Fn(Epoch, SV, usize) -> Option<InterpolationResult>> Solver<I> {
pub fn new(
mode: Mode,
apriori: AprioriPosition,
cfg: &Config,
interpolator: I,
) -> Result<Self, Error> {
let cosmic = Cosm::de438();
let sun_frame = cosmic.frame("Sun J2000");
let earth_frame = cosmic.frame("EME2000");
if cfg.modeling.relativistic_clock_corr {
warn!("relativistic clock corr. is not feasible at the moment");
}
if mode == Mode::SPP && cfg.min_sv_sunlight_rate.is_some() {
warn!("eclipse filter is not meaningful when using spp strategy");
}
Ok(Self {
mode,
cosmic,
sun_frame,
earth_frame,
apriori,
interpolator,
cfg: cfg.clone(),
})
}
fn elect_candidates(pool: Vec<Candidate>, mode: Mode) -> Vec<Candidate> {
pool.iter()
.filter_map(|c| {
let mode_compliant = match mode {
Mode::SPP => true,
Mode::PPP => true, };
if mode_compliant {
Some(c.clone())
} else {
None
}
})
.collect()
}
pub fn resolve(
&mut self,
t: Epoch,
solution: PVTSolutionType,
pool: Vec<Candidate>,
iono_bias: &IonosphericBias,
tropo_bias: &TroposphericBias,
) -> Result<(Epoch, PVTSolution), Error> {
let min_required = Self::min_required(solution, &self.cfg);
if pool.len() < min_required {
return Err(Error::NotEnoughInputCandidates(solution));
}
let (x0, y0, z0) = (
self.apriori.ecef.x,
self.apriori.ecef.y,
self.apriori.ecef.z,
);
let (lat_ddeg, lon_ddeg, altitude_above_sea_m) = (
self.apriori.geodetic.x,
self.apriori.geodetic.y,
self.apriori.geodetic.z,
);
let modeling = self.cfg.modeling;
let interp_order = self.cfg.interp_order;
let pool = Self::elect_candidates(pool, self.mode);
let mut pool: Vec<Candidate> = pool
.iter()
.filter_map(|c| {
let (t_tx, dt_ttx) = c.transmission_time(&self.cfg).ok()?;
if let Some(mut interpolated) = (self.interpolator)(t_tx, c.sv, interp_order) {
let mut c = c.clone();
if modeling.earth_rotation {
const EARTH_OMEGA_E_WGS84: f64 = 7.2921151467E-5;
let we = EARTH_OMEGA_E_WGS84 * dt_ttx;
let (we_cos, we_sin) = (we.cos(), we.sin());
let r = Matrix3::<f64>::new(
we_cos, we_sin, 0.0_f64, -we_sin, we_cos, 0.0_f64, 0.0_f64, 0.0_f64,
1.0_f64,
);
interpolated.position = r * interpolated.position;
}
debug!(
"{:?} ({}) : interpolated state: {:?}",
t_tx, c.sv, interpolated.position
);
c.state = Some(interpolated);
Some(c)
} else {
warn!("{:?} ({}) : interpolation failed", t_tx, c.sv);
None
}
})
.collect();
if let Some(min_elev) = self.cfg.min_sv_elev {
let mut idx: usize = 0;
let mut nb_removed: usize = 0;
while idx < pool.len() {
if let Some(state) = pool[idx - nb_removed].state {
if state.elevation < min_elev {
debug!(
"{:?} ({}) : below elevation mask",
pool[idx - nb_removed].t,
pool[idx - nb_removed].sv
);
let _ = pool.swap_remove(idx - nb_removed);
nb_removed += 1;
}
}
idx += 1;
}
}
if let Some(min_rate) = self.cfg.min_sv_sunlight_rate {
for idx in 0..pool.len() - 1 {
let state = pool[idx].state.unwrap(); let orbit = state.orbit(pool[idx].t, self.earth_frame);
let state = eclipse_state(&orbit, self.sun_frame, self.earth_frame, &self.cosmic);
let eclipsed = match state {
EclipseState::Umbra => true,
EclipseState::Visibilis => false,
EclipseState::Penumbra(r) => r < min_rate,
};
if eclipsed {
debug!(
"{:?} ({}): earth eclipsed, dropping",
pool[idx].t, pool[idx].sv
);
let _ = pool.swap_remove(idx);
}
}
}
let nb_candidates = pool.len();
if nb_candidates < min_required {
return Err(Error::NotEnoughFittingCandidates);
} else {
debug!("{:?}: {} elected sv", t, nb_candidates);
}
let mut y = DVector::<f64>::zeros(nb_candidates);
let mut g = MatrixXx4::<f64>::zeros(nb_candidates);
let mut pvt_sv_data = HashMap::<SV, PVTSVData>::with_capacity(nb_candidates);
for (row_index, cd) in pool.iter().enumerate() {
if let Ok(sv_data) = cd.resolve(
t,
&self.cfg,
self.mode,
(x0, y0, z0),
(lat_ddeg, lon_ddeg, altitude_above_sea_m),
iono_bias,
tropo_bias,
row_index,
&mut y,
&mut g,
) {
pvt_sv_data.insert(cd.sv, sv_data);
}
}
let mut pvt_solution = PVTSolution::new(g, y, pvt_sv_data)?;
if let Some(alt) = self.cfg.fixed_altitude {
pvt_solution.p.z = self.apriori.ecef.z - alt;
pvt_solution.v.z = 0.0_f64;
}
match solution {
PVTSolutionType::TimeOnly => {
pvt_solution.p = Vector3::<f64>::default();
pvt_solution.hdop = 0.0_f64;
pvt_solution.vdop = 0.0_f64;
},
_ => {},
}
Ok((t, pvt_solution))
}
fn sun_earth_vector(&mut self, t: Epoch) -> Vector3<f64> {
let sun_body = Bodies::Sun;
let orbit = self.cosmic.celestial_state(
sun_body.ephem_path(),
t,
self.earth_frame,
LightTimeCalc::None,
);
Vector3::new(
orbit.x_km * 1000.0,
orbit.y_km * 1000.0,
orbit.z_km * 1000.0,
)
}
fn min_required(solution: PVTSolutionType, cfg: &Config) -> usize {
match solution {
PVTSolutionType::TimeOnly => 1,
_ => {
let mut n = 4;
if cfg.fixed_altitude.is_some() {
n -= 1;
}
n
},
}
}
}