use std::ops::ControlFlow;
use aberth::StopReason;
use nalgebra::Matrix3;
use nalgebra::Vector3;
use rand_distr::StandardNormal;
use smallvec::SmallVec;
use crate::constants::Radian;
use crate::constants::{GAUSS_GRAV, VLIGHT_AU};
use crate::initial_orbit_determination::gauss_result::GaussResult;
use crate::initial_orbit_determination::IODParams;
use crate::kepler::velocity_correction_with_guess;
use crate::orb_elem::eccentricity_control;
use crate::orbit_type::OrbitalElements;
use crate::outfit::Outfit;
use crate::outfit_errors::OutfitError;
use aberth::aberth;
use rand::Rng;
#[derive(Debug, PartialEq, Clone)]
pub struct GaussObs {
pub(crate) idx_obs: Vector3<usize>,
pub(crate) ra: Vector3<Radian>,
pub(crate) dec: Vector3<Radian>,
pub(crate) time: Vector3<f64>,
pub(crate) observer_helio_position: Matrix3<f64>,
}
type GaussPrelimResult = Result<
(
f64,
f64,
Matrix3<f64>,
Matrix3<f64>,
Vector3<f64>,
Vector3<f64>,
),
OutfitError,
>;
#[inline]
fn descartes_upper_bound_deg8_sparse(c0: f64, c3: f64, c6: f64, zero_eps: f64) -> u32 {
#[inline]
fn s(v: f64, eps: f64) -> i8 {
if v.abs() <= eps {
0
} else if v.is_sign_positive() {
1
} else {
-1
}
}
let seq = [1_i8, s(c6, zero_eps), s(c3, zero_eps), s(c0, zero_eps)];
let mut last = 0_i8;
let mut count = 0_u32;
for &cur in &seq {
if cur == 0 {
continue;
}
if last != 0 && cur != last {
count += 1;
}
last = cur;
}
count
}
impl GaussObs {
pub fn with_observer_position(
idx_obs: Vector3<usize>,
ra: Vector3<f64>,
dec: Vector3<f64>,
mjd_time: Vector3<f64>,
observer_helio_position: Matrix3<f64>,
) -> GaussObs {
GaussObs {
idx_obs,
ra,
dec,
time: mjd_time,
observer_helio_position,
}
}
pub fn realizations_iter<'a, R: Rng + 'a>(
&'a self,
errors_ra: &'a Vector3<f64>,
errors_dec: &'a Vector3<f64>,
n_realizations: usize,
noise_scale: f64,
rng: &'a mut R,
) -> impl Iterator<Item = GaussObs> + 'a {
let s_ra = *errors_ra * noise_scale;
let s_dec = *errors_dec * noise_scale;
let mut i = 0usize;
std::iter::from_fn(move || {
if i == 0 {
i += 1;
return Some(self.clone());
}
if i > n_realizations {
return None;
}
i += 1;
let (z_ra0, z_ra1, z_ra2): (f64, f64, f64) = (
rng.sample(StandardNormal),
rng.sample(StandardNormal),
rng.sample(StandardNormal),
);
let (z_dec0, z_dec1, z_dec2): (f64, f64, f64) = (
rng.sample(StandardNormal),
rng.sample(StandardNormal),
rng.sample(StandardNormal),
);
let ra = Vector3::new(
self.ra.x + z_ra0 * s_ra.x,
self.ra.y + z_ra1 * s_ra.y,
self.ra.z + z_ra2 * s_ra.z,
);
let dec = Vector3::new(
self.dec.x + z_dec0 * s_dec.x,
self.dec.y + z_dec1 * s_dec.y,
self.dec.z + z_dec2 * s_dec.z,
);
Some(GaussObs {
idx_obs: self.idx_obs,
ra,
dec,
time: self.time,
observer_helio_position: self.observer_helio_position,
})
})
}
pub fn generate_noisy_realizations(
&self,
errors_ra: &Vector3<f64>,
errors_dec: &Vector3<f64>,
n_realizations: usize,
noise_scale: f64,
rng: &mut impl Rng,
) -> Vec<GaussObs> {
self.realizations_iter(errors_ra, errors_dec, n_realizations, noise_scale, rng)
.collect()
}
fn unit_vector(ra: f64, dec: f64, cos_dec: f64) -> Vector3<f64> {
Vector3::new(
f64::cos(ra) * cos_dec,
f64::sin(ra) * cos_dec,
f64::sin(dec),
)
}
fn unit_matrix(&self) -> Matrix3<f64> {
Matrix3::from_columns(&[
GaussObs::unit_vector(self.ra[0], self.dec[0], f64::cos(self.dec[0])),
GaussObs::unit_vector(self.ra[1], self.dec[1], f64::cos(self.dec[1])),
GaussObs::unit_vector(self.ra[2], self.dec[2], f64::cos(self.dec[2])),
])
}
pub fn gauss_prelim(&self) -> GaussPrelimResult {
let tau1 = GAUSS_GRAV * (self.time[0] - self.time[1]);
let tau3 = GAUSS_GRAV * (self.time[2] - self.time[1]);
let tau13 = tau3 - tau1;
let vector_a = Vector3::new(tau3 / tau13, -1.0, -(tau1 / tau13));
let vector_b = Vector3::new(
vector_a[0] * (tau13.powi(2) - tau3.powi(2)) / 6.0,
0.0,
vector_a[2] * (tau13.powi(2) - tau1.powi(2)) / 6.0,
);
let unit_matrix = self.unit_matrix();
let inv_unit_matrix = unit_matrix
.try_inverse()
.ok_or(OutfitError::SingularDirectionMatrix)?;
Ok((tau1, tau3, unit_matrix, inv_unit_matrix, vector_a, vector_b))
}
pub fn coeff_eight_poly(
&self,
unit_matrix: &Matrix3<f64>,
unit_invmatrix: &Matrix3<f64>,
vector_a: &Vector3<f64>,
vector_b: &Vector3<f64>,
) -> (f64, f64, f64) {
let observer_position_t = self.observer_helio_position.transpose();
let ra = self.observer_helio_position * vector_a;
let rb = self.observer_helio_position * vector_b;
let second_row_t = unit_invmatrix.row(1).transpose();
let a2star = second_row_t.dot(&ra);
let b2star = second_row_t.dot(&rb);
let r22 = observer_position_t
.row(1)
.component_mul(&observer_position_t.row(1))
.sum();
let s2r2 = unit_matrix
.column(1)
.transpose()
.dot(&observer_position_t.row(1));
(
-(a2star.powi(2)) - r22 - (2.0 * a2star * s2r2),
-(2.0 * b2star * (a2star + s2r2)),
-(b2star.powi(2)),
)
}
pub fn solve_8poly(
&self,
polynom: &[f64; 9],
max_iterations: u32,
aberth_epsilon: f64,
root_acceptance_epsilon: f64,
) -> Result<Vec<f64>, OutfitError> {
let roots = aberth(polynom, max_iterations, aberth_epsilon);
match roots.stop_reason {
StopReason::Converged(_) | StopReason::MaxIteration(_) => {
Ok(roots
.iter()
.filter(|complex| complex.re > 0. && complex.im.abs() < root_acceptance_epsilon)
.map(|complex| complex.re)
.collect::<Vec<f64>>())
}
StopReason::Failed(_) => Err(OutfitError::PolynomialRootFindingFailed),
}
}
pub fn position_vector_and_reference_epoch(
&self,
iod_params: &IODParams,
unit_matrix: &Matrix3<f64>,
unit_matinv: &Matrix3<f64>,
vector_c: &Vector3<f64>,
) -> Result<(Matrix3<f64>, f64), OutfitError> {
let obs_pos_t = self.observer_helio_position;
let gcap = obs_pos_t * vector_c;
let crhom = unit_matinv * gcap;
let rho: Vector3<f64> = -crhom.component_div(vector_c);
if rho[1] < iod_params.min_rho2_au {
return Err(OutfitError::SpuriousRootDetected);
}
let rho_unit = Matrix3::from_columns(&[
rho[0] * unit_matrix.column(0),
rho[1] * unit_matrix.column(1),
rho[2] * unit_matrix.column(2),
]);
let reference_epoch = self.time[1] - rho[1] / VLIGHT_AU;
Ok((obs_pos_t + rho_unit, reference_epoch))
}
pub fn gibbs_correction(
&self,
ast_pos_vector: &Matrix3<f64>,
tau1: f64,
tau3: f64,
) -> Vector3<f64> {
let tau13 = tau3 - tau1;
let r1m3 = 1.0 / ast_pos_vector.column(0).norm().powi(3); let r2m3 = 1.0 / ast_pos_vector.column(1).norm().powi(3); let r3m3 = 1.0 / ast_pos_vector.column(2).norm().powi(3);
let d1 = tau3 * (r1m3 / 12.0 - 1.0 / (tau1 * tau13));
let d2 = (tau1 + tau3) * (r2m3 / 12.0 - 1.0 / (tau1 * tau3));
let d3 = -tau1 * (r3m3 / 12.0 + 1.0 / (tau3 * tau13));
let d_vect = Vector3::new(-d1, d2, d3);
Vector3::new(
GAUSS_GRAV * ast_pos_vector.row(0).dot(&d_vect.transpose()), GAUSS_GRAV * ast_pos_vector.row(1).dot(&d_vect.transpose()), GAUSS_GRAV * ast_pos_vector.row(2).dot(&d_vect.transpose()), )
}
#[allow(clippy::too_many_arguments)]
pub fn accept_root(
&self,
iod_params: &IODParams,
root: f64,
unit_matrix: &Matrix3<f64>,
inv_unit_matrix: &Matrix3<f64>,
vect_a: &Vector3<f64>,
vect_b: &Vector3<f64>,
tau1: f64,
tau3: f64,
) -> Option<(Matrix3<f64>, Vector3<f64>, f64)> {
let r2m3 = 1.0 / root.powi(3);
let vector_c = Vector3::new(
vect_a[0] + vect_b[0] * r2m3,
-1.0,
vect_a[2] + vect_b[2] * r2m3,
);
let Some((ast_pos_all_time, reference_epoch)) = self
.position_vector_and_reference_epoch(
iod_params,
unit_matrix,
inv_unit_matrix,
&vector_c,
)
.ok()
else {
return None; };
let ast_pos_second_time = ast_pos_all_time.column(1).into();
let asteroid_vel = self.gibbs_correction(&ast_pos_all_time, tau1, tau3);
let (is_accepted, _, _, _) = eccentricity_control(
&ast_pos_second_time,
&asteroid_vel,
iod_params.max_perihelion_au,
iod_params.max_ecc,
)?;
if is_accepted {
Some((ast_pos_all_time, asteroid_vel, reference_epoch))
} else {
None
}
}
fn compute_orbit_from_state(
&self,
state: &Outfit,
&asteroid_position: &Vector3<f64>,
&asteroid_velocity: &Vector3<f64>,
reference_epoch: f64,
) -> Result<OrbitalElements, OutfitError> {
let roteqec = state.get_rot_equmj2000_to_eclmj2000();
let matrix_elc_transform = roteqec.transpose();
let ecl_pos = matrix_elc_transform * asteroid_position;
let ecl_vel = matrix_elc_transform * asteroid_velocity;
Ok(OrbitalElements::from_orbital_state(
&ecl_pos,
&ecl_vel,
reference_epoch,
))
}
#[inline]
fn visit_real_positive_roots(
poly: &[f64; 9],
max_iterations: u32,
aberth_epsilon: f64,
root_imag_eps: f64,
mut on_root: impl FnMut(f64) -> ControlFlow<(), ()>,
) -> Result<(), OutfitError> {
let roots = aberth(poly, max_iterations, aberth_epsilon);
match roots.stop_reason {
aberth::StopReason::Converged(_) | aberth::StopReason::MaxIteration(_) => {
for z in roots.iter() {
if z.re > 0.0 && z.im.abs() < root_imag_eps {
if let ControlFlow::Break(()) = on_root(z.re) {
break;
}
}
}
Ok(())
}
aberth::StopReason::Failed(_) => Err(OutfitError::PolynomialRootFindingFailed),
}
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn try_accept_root(
&self,
iod_params: &IODParams,
r2: f64,
unit_m: &Matrix3<f64>,
inv_unit_m: &Matrix3<f64>,
a: &Vector3<f64>,
b: &Vector3<f64>,
tau1: f64,
tau3: f64,
) -> Option<(nalgebra::Matrix3<f64>, Vector3<f64>, f64)> {
self.accept_root(iod_params, r2, unit_m, inv_unit_m, a, b, tau1, tau3)
}
#[inline]
fn build_result(
&self,
state: &Outfit,
pos_all_time: &Matrix3<f64>,
vel_t2: &Vector3<f64>,
epoch: f64,
corrected: bool,
) -> Option<GaussResult> {
let r_t2: Vector3<f64> = pos_all_time.column(1).into();
match self.compute_orbit_from_state(state, &r_t2, vel_t2, epoch) {
Ok(orbit) if corrected => Some(GaussResult::CorrectedOrbit(orbit)),
Ok(orbit) => Some(GaussResult::PrelimOrbit(orbit)),
Err(_) => None,
}
}
pub fn prelim_orbit_all(
&self,
state: &Outfit,
iod_params: &IODParams,
) -> Result<Vec<GaussResult>, OutfitError> {
let (tau1, tau3, unit_m, inv_unit_m, vec_a, vec_b) = self.gauss_prelim()?;
let (c6, c3, c0) = self.coeff_eight_poly(&unit_m, &inv_unit_m, &vec_a, &vec_b);
let poly = [c0, 0.0, 0.0, c3, 0.0, 0.0, c6, 0.0, 1.0];
let ub = descartes_upper_bound_deg8_sparse(c0, c3, c6, 0.0);
if ub == 0 {
return Err(OutfitError::GaussNoRootsFound);
}
let mut solutions: SmallVec<[GaussResult; 4]> = SmallVec::new();
Self::visit_real_positive_roots(
&poly,
iod_params.aberth_max_iter,
iod_params.aberth_eps,
iod_params.root_imag_eps,
|r2| {
if !(iod_params.r2_min_au..=iod_params.r2_max_au).contains(&r2) {
return ControlFlow::Continue(());
}
match self.try_accept_root(
iod_params,
r2,
&unit_m,
&inv_unit_m,
&vec_a,
&vec_b,
tau1,
tau3,
) {
None => ControlFlow::Continue(()),
Some((pos_all, v_pre, epoch_ref)) => {
if let Some((pos_cor, v_cor, epoch_cor)) = self.pos_and_vel_correction(
iod_params,
&pos_all,
&v_pre,
&unit_m,
&inv_unit_m,
iod_params.max_perihelion_au,
iod_params.max_ecc,
iod_params.newton_eps,
iod_params.newton_max_it,
) {
if let Some(res) =
self.build_result(state, &pos_cor, &v_cor, epoch_cor, true)
{
if solutions.len() < iod_params.max_tested_solutions {
solutions.push(res);
}
}
} else if let Some(res) =
self.build_result(state, &pos_all, &v_pre, epoch_ref, false)
{
if solutions.len() < iod_params.max_tested_solutions {
solutions.push(res);
}
}
if solutions.len() >= iod_params.max_tested_solutions {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}
}
},
)?;
if solutions.is_empty() {
Err(OutfitError::GaussNoRootsFound)
} else {
Ok(solutions.into_vec())
}
}
pub fn prelim_orbit(
&self,
state: &Outfit,
iod_params: &IODParams,
) -> Result<GaussResult, OutfitError> {
let all = self.prelim_orbit_all(state, iod_params)?;
if let Some(best_corr) = all
.iter()
.find(|s| matches!(s, GaussResult::CorrectedOrbit(_)))
{
return Ok(best_corr.clone());
}
all.into_iter().next().ok_or(OutfitError::GaussNoRootsFound)
}
#[allow(clippy::too_many_arguments)]
pub fn pos_and_vel_correction(
&self,
iod_params: &IODParams,
asteroid_position: &Matrix3<f64>,
asteroid_velocity: &Vector3<f64>,
unit_matrix: &Matrix3<f64>,
inv_unit_matrix: &Matrix3<f64>,
peri_max: f64,
ecc_max: f64,
err_max: f64,
itmax: usize,
) -> Option<(Matrix3<f64>, Vector3<f64>, f64)> {
let mut pos = *asteroid_position;
let mut vel = *asteroid_velocity;
let mut epoch = 0.0_f64;
let mut chi_guess_01: Option<f64> = None; let mut chi_guess_21: Option<f64> = None;
let dt_01 = self.time[0] - self.time[1];
let dt_21 = self.time[2] - self.time[1];
if dt_01.abs() <= f64::EPSILON || dt_21.abs() <= f64::EPSILON {
return None;
}
for _iter in 0..itmax {
let r1 = pos.column(0).into_owned();
let r2 = pos.column(1).into_owned();
let r3 = pos.column(2).into_owned();
let res_left = velocity_correction_with_guess(
&r1,
&r2,
&vel,
dt_01,
peri_max,
ecc_max,
chi_guess_01,
iod_params.kepler_eps,
);
let res_right = velocity_correction_with_guess(
&r3,
&r2,
&vel,
dt_21,
peri_max,
ecc_max,
chi_guess_21,
iod_params.kepler_eps,
);
let ((v1, f1, g1, chi1), (v2, f2, g2, chi2)) = match (res_left, res_right) {
(Ok(a), Ok(b)) => (a, b),
_ => continue, };
chi_guess_01 = Some(chi1);
chi_guess_21 = Some(chi2);
if !g1.is_finite() || !g2.is_finite() {
continue;
}
let new_vel = (v1 + v2) * 0.5;
let fl = f1 * g2 - f2 * g1;
if !fl.is_finite() || fl.abs() < f64::EPSILON {
continue;
}
let inv_f = 1.0 / fl;
let c_vec = Vector3::new(g2 * inv_f, -1.0, -g1 * inv_f);
let (new_pos, new_epoch) = match self.position_vector_and_reference_epoch(
iod_params,
unit_matrix,
inv_unit_matrix,
&c_vec,
) {
Ok(v) => v,
_ => continue, };
let r_mid = new_pos.column(1).into_owned();
let accepted = match eccentricity_control(&r_mid, &new_vel, peri_max, ecc_max) {
Some((ok, _, _, _)) => ok,
None => return None, };
if !accepted {
return None; }
let denom = new_pos.norm();
if !denom.is_finite() || denom <= f64::EPSILON {
continue;
}
let rel_err = (new_pos - pos).norm() / denom;
pos = new_pos;
vel = new_vel;
epoch = new_epoch;
if rel_err <= err_max {
break;
}
}
Some((pos, vel, epoch))
}
}
#[cfg(test)]
#[cfg(feature = "jpl-download")]
pub(crate) mod gauss_test {
use super::*;
use crate::{
orbit_type::{keplerian_element::KeplerianElements, orbit_type_test::approx_equal},
unit_test_global::OUTFIT_HORIZON_TEST,
};
#[test]
fn test_gauss_prelim() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::new(1.6893715963476696, 1.6898894500811472, 1.7527345385664372),
dec: Vector3::new(
1.082_468_037_385_525,
0.943_580_504_794_621_6,
0.827_376_240_789_998_6,
),
time: Vector3::new(
57028.479297592596,
57_049.245_147_592_59,
57_063.977_117_592_59,
),
observer_helio_position: Matrix3::zeros(),
};
let (tau1, tau3, unit_matrix, inv_unit_matrix, vector_a, vector_b) =
gauss.gauss_prelim().unwrap();
assert_eq!(tau1, -0.35721620648079105);
assert_eq!(tau3, 0.25342080566844405);
let unit_mat_array: [f64; 9] = unit_matrix
.as_slice()
.try_into()
.expect("Conversion failed");
assert_eq!(
unit_mat_array,
[
-0.05549934652247514,
0.46585594034226024,
0.8831183756345503,
-0.06972979004485365,
0.5827357012279389,
0.8096646582966821,
-0.12245931009139571,
0.6656387438390606,
0.7361581216507068
]
);
let inv_unit_mat_array: [f64; 9] = inv_unit_matrix
.as_slice()
.try_into()
.expect("Conversion failed");
assert_eq!(
inv_unit_mat_array,
[
-18.774792915974594,
41.814279122702025,
-23.466669573973437,
-8.16479071034311,
11.489343729350427,
-2.8418335594428186,
4.259482782736117,
-3.432964304649723,
0.024345794753282718
]
);
let vect_a_array: [f64; 3] = vector_a.as_slice().try_into().expect("Conversion failed");
assert_eq!(
vect_a_array,
[0.41501055557783634, -1.0, 0.5849894444221637]
);
let vect_b_array: [f64; 3] = vector_b.as_slice().try_into().expect("Conversion failed");
assert_eq!(
vect_b_array,
[0.021349212036493866, 0.0, 0.023913797385599792]
);
}
#[test]
fn test_coeff_8poly() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::new(1.6893715963476696, 1.6898894500811472, 1.7527345385664372),
dec: Vector3::new(
1.082_468_037_385_525,
0.943_580_504_794_621_6,
0.827_376_240_789_998_6,
),
time: Vector3::new(
57028.479297592596,
57_049.245_147_592_59,
57_063.977_117_592_59,
),
observer_helio_position: Matrix3::new(
-0.26456661713915464,
0.868_935_164_369_495,
0.376_699_621_109_192_2,
-0.589_163_185_217_412_7,
0.723_887_251_679_477_7,
0.313_818_651_652_458_5,
-0.774_387_443_796_959_6,
0.561_288_470_926_116_4,
0.24334971075289916,
)
.transpose(),
};
let (_, _, unit_matrix, inv_unit_matrix, vector_a, vector_b) =
gauss.gauss_prelim().unwrap();
let (coeff_6, coeff_3, coeff_0) =
gauss.coeff_eight_poly(&unit_matrix, &inv_unit_matrix, &vector_a, &vector_b);
assert_eq!(coeff_6, -2.615803718759013);
assert_eq!(coeff_3, 2.0305173353541064);
assert_eq!(coeff_0, -0.4771346939201045);
}
#[test]
fn test_solving_polynom() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::zeros(),
dec: Vector3::zeros(),
time: Vector3::zeros(),
observer_helio_position: Matrix3::zeros(),
};
let polynomial = [
-0.477_134_693_920_104_8,
0.,
0.,
2.0305173353541064,
0.,
0.,
-2.615_803_718_759_011,
0.,
1.,
];
let roots = gauss.solve_8poly(&polynomial, 50, 1e-6, 1e-6).unwrap();
assert_eq!(
roots,
vec![1.3856312487504954, 0.7328107254669438, 0.9540135094917113]
);
}
#[test]
fn test_asteroid_position() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::new(1.6893715963476696, 1.6898894500811472, 1.7527345385664372),
dec: Vector3::new(
1.082_468_037_385_525,
0.943_580_504_794_621_6,
0.827_376_240_789_998_6,
),
time: Vector3::new(
57028.479297592596,
57_049.245_147_592_59,
57_063.977_117_592_59,
),
observer_helio_position: Matrix3::new(
-0.26456661713915464,
0.868_935_164_369_495,
0.376_699_621_109_192_2,
-0.589_163_185_217_412_7,
0.723_887_251_679_477_7,
0.313_818_651_652_458_5,
-0.774_387_443_796_959_6,
0.561_288_470_926_116_4,
0.24334971075289916,
)
.transpose(),
};
let (_, _, unit_matrix, inv_unit_matrix, vector_a, vector_b) =
gauss.gauss_prelim().unwrap();
let first_root: f64 = 0.732_810_725_466_943_7;
let r2m3 = 1. / first_root.powi(3);
let vector_c: Vector3<f64> = Vector3::new(
vector_a[0] + vector_b[0] * r2m3,
-1.,
vector_a[2] + vector_b[2] * r2m3,
);
let ast_pos = gauss.position_vector_and_reference_epoch(
&IODParams::default(),
&unit_matrix,
&inv_unit_matrix,
&vector_c,
);
assert!(ast_pos.is_err());
let second_root: f64 = 1.3856312487504951;
let r2m3 = 1. / second_root.powi(3);
let vector_c: Vector3<f64> = Vector3::new(
vector_a[0] + vector_b[0] * r2m3,
-1.,
vector_a[2] + vector_b[2] * r2m3,
);
let (ast_pos, ref_epoch) = gauss
.position_vector_and_reference_epoch(
&IODParams::default(),
&unit_matrix,
&inv_unit_matrix,
&vector_c,
)
.unwrap();
let ast_pos_slice: [f64; 9] = ast_pos
.as_slice()
.try_into()
.expect("test_asteroid_position result matrix into slice failed");
assert_eq!(
ast_pos_slice,
[
-0.28811969067349597,
1.06663729794052,
0.7514815481797275,
-0.6235500510031637,
1.0112601855976917,
0.713100363506241,
-0.8445850475187664,
0.9428539454255418,
0.6653391541170498
]
);
assert_eq!(ref_epoch, 57049.24229942721);
}
#[test]
fn test_gibbs_correction() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::new(1.6893715963476696, 1.6898894500811472, 1.7527345385664372),
dec: Vector3::new(
1.082_468_037_385_525,
0.943_580_504_794_621_6,
0.827_376_240_789_998_6,
),
time: Vector3::new(
57028.479297592596,
57_049.245_147_592_59,
57_063.977_117_592_59,
),
observer_helio_position: Matrix3::zeros(),
};
let (tau1, tau3, _, _, _, _) = gauss.gauss_prelim().unwrap();
let ast_pos = Matrix3::new(
-0.28811969067349597,
1.06663729794052,
0.7514815481797275,
-0.6235500510031637,
1.0112601855976917,
0.713100363506241,
-0.8445850475187664,
0.9428539454255418,
0.6653391541170498,
)
.transpose();
let asteroid_velocity = gauss.gibbs_correction(&ast_pos, tau1, tau3);
let ast_vel_slice = asteroid_velocity.as_slice();
assert_eq!(
ast_vel_slice,
[
-0.015549845137774663,
-0.003876936109837664,
-0.0027014074002979886
]
)
}
#[test]
fn test_solve_orbit() {
let env = &OUTFIT_HORIZON_TEST.0;
let tol = 1e-13;
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::new(1.6894680985108945, 1.6898614520910629, 1.7526450904422723),
dec: Vector3::new(
1.0825984522657437,
0.943_679_018_934_623_1,
0.827_517_321_571_201_4,
),
time: Vector3::new(
57_028.454_047_592_59,
57_049.231_857_592_59,
57_063.959_487_592_59,
),
observer_helio_position: Matrix3::new(
-0.264_135_633_607_079,
-0.588_973_552_650_573_5,
-0.774_192_148_350_372,
0.869_046_620_910_086,
0.724_011_718_791_646,
0.561_510_219_548_918_2,
0.376_746_685_666_572_5,
0.313_873_420_677_094,
0.243_444_791_401_658_5,
),
};
let binding = gauss.prelim_orbit(env, &IODParams::default()).unwrap();
let prelim_orbit = binding.get_orbit();
let expected_orbit = OrbitalElements::Keplerian(KeplerianElements {
reference_epoch: 57_049.229_045_244_22,
semi_major_axis: 1.8014943988486352,
eccentricity: 0.283_514_142_249_080_7,
inclination: 0.20264170920820326,
ascending_node_longitude: 8.118_562_444_269_591E-3,
periapsis_argument: 1.244_795_311_814_302,
mean_anomaly: 0.44065425435816186,
});
assert!(approx_equal(prelim_orbit, &expected_orbit, tol));
let a = GaussObs {
idx_obs: [[23, 24, 33]].into(),
ra: [[1.6893715963476699, 1.689861452091063, 1.7527345385664372]].into(),
dec: [[1.082468037385525, 0.9436790189346231, 0.8273762407899986]].into(),
time: [[57028.479297592596, 57049.2318575926, 57063.97711759259]].into(),
observer_helio_position: [
[-0.2645666171486676, 0.8689351643673471, 0.3766996211112465],
[-0.5889735526502539, 0.7240117187952059, 0.3138734206791042],
[-0.7743874438017259, 0.5612884709246775, 0.2433497107566823],
]
.into(),
};
let binding = a.prelim_orbit(env, &IODParams::default()).unwrap();
let prelim_orbit_a = binding.get_orbit();
let expected_orbit = OrbitalElements::Keplerian(KeplerianElements {
reference_epoch: 57049.22904525282,
semi_major_axis: 1.801490008178814,
eccentricity: 0.28350961635625993,
inclination: 0.20264261257939395,
ascending_node_longitude: 0.008105552171682476,
periapsis_argument: 1.244832121745955,
mean_anomaly: 0.4406444535028061,
});
assert!(approx_equal(prelim_orbit_a, &expected_orbit, tol));
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::new(
1.6894680552416277,
1.689_861_821_442_152,
1.7526488678231147,
),
dec: Vector3::new(
1.0825994437405373,
0.943_679_863_334_145,
0.827_517_360_507_228_6,
),
time: Vector3::new(
57_028.454_047_592_59,
57_049.231_857_592_59,
57_063.959_487_592_59,
),
observer_helio_position: Matrix3::new(
-0.264_135_633_607_079,
-0.588_973_552_650_573_5,
-0.774_192_148_350_372,
0.869_046_620_910_086,
0.724_011_718_791_646,
0.561_510_219_548_918_2,
0.376_746_685_666_572_5,
0.313_873_420_677_094,
0.243_444_791_401_658_5,
),
};
let binding = gauss.prelim_orbit(env, &IODParams::default()).unwrap();
let prelim_orbit_b = binding.get_orbit();
let expected_orbfit = OrbitalElements::Keplerian(KeplerianElements {
reference_epoch: 57_049.229_045_608_86,
semi_major_axis: 1.8013098187420686,
eccentricity: 0.28347096712267805,
inclination: 0.202_617_665_872_441_2,
ascending_node_longitude: 8.194_805_420_465_082E-3,
periapsis_argument: 1.2446747244785052,
mean_anomaly: 0.44073731381184733,
});
assert!(approx_equal(prelim_orbit_b, &expected_orbfit, tol));
}
#[test]
fn test_orbit_correction() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: Vector3::new(1.6893715963476696, 1.6898894500811472, 1.7527345385664372),
dec: Vector3::new(
1.082_468_037_385_525,
0.943_580_504_794_621_6,
0.827_376_240_789_998_6,
),
time: Vector3::new(
57028.479297592596,
57_049.245_147_592_59,
57_063.977_117_592_59,
),
observer_helio_position: Matrix3::new(
-0.26456661713915464,
0.868_935_164_369_495,
0.376_699_621_109_192_2,
-0.589_163_185_217_412_7,
0.723_887_251_679_477_7,
0.313_818_651_652_458_5,
-0.774_387_443_796_959_6,
0.561_288_470_926_116_4,
0.24334971075289916,
)
.transpose(),
};
let ast_pos = Matrix3::new(
-0.288_119_690_673_496,
1.0666372979405205,
0.751_481_548_179_728_3,
-0.623_550_051_003_163_9,
1.011_260_185_597_692,
0.713_100_363_506_241_4,
-0.844_585_047_518_766_5,
0.942_853_945_425_542_1,
0.665_339_154_117_050_1,
)
.transpose();
let ast_vel = Vector3::new(
-1.554_984_513_777_466_3E-2,
-3.876_936_109_837_657_7E-3,
-2.701_407_400_297_996_4E-3,
);
let unit_matrix = Matrix3::new(
-5.549_934_652_247_514E-2,
0.46585594034226024,
0.883_118_375_634_550_3,
-6.972_979_004_485_365E-2,
0.582_735_701_227_938_9,
0.809_664_658_296_682_1,
-0.12245931009139571,
0.665_638_743_839_060_6,
0.736_158_121_650_706_8,
)
.transpose();
let inv_unit_matrix = Matrix3::new(
-18.774792915974604,
41.814_279_122_702_03,
-23.466669573973437,
-8.164_790_710_343_109,
11.489343729350425,
-2.8418335594428177,
4.259_482_782_736_117,
-3.432_964_304_649_721,
2.434_579_475_328_179E-2,
)
.transpose();
let (new_ast_pos, new_ast_vel, corrected_epoch) = gauss
.pos_and_vel_correction(
&IODParams::default(),
&ast_pos,
&ast_vel,
&unit_matrix,
&inv_unit_matrix,
1e3,
1.,
1e-10,
50,
)
.unwrap();
assert_eq!(
new_ast_pos.as_slice(),
[
-0.2878540141559046,
1.06440723593647,
0.7472540422835181,
-0.6231216182863288,
1.0076797497536536,
0.7081256342111117,
-0.8435611164802848,
0.9372882749205874,
0.6591838430228918
]
);
assert_eq!(
new_ast_vel.as_slice(),
[
-0.015524309979972159,
-0.003984105628190921,
-0.0027640157742952693
]
);
assert_eq!(corrected_epoch, 57049.24233491307);
}
mod test_generate_noisy_realizations {
use super::*;
use nalgebra::vector;
use rand::{rngs::StdRng, SeedableRng};
#[test]
fn test_generate_noisy_realizations_count_and_identity() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: vector![1.0, 2.0, 3.0],
dec: vector![0.1, 0.2, 0.3],
time: vector![59000.0, 59001.0, 59002.0],
observer_helio_position: Matrix3::identity(),
};
let errors_ra = vector![1e-6, 1e-6, 1e-6];
let errors_dec = vector![2e-6, 2e-6, 2e-6];
let mut rng = StdRng::seed_from_u64(42_u64);
let realizations =
gauss.generate_noisy_realizations(&errors_ra, &errors_dec, 5, 1.0, &mut rng);
assert_eq!(realizations.len(), 6);
assert_eq!(realizations[0].ra, gauss.ra);
assert_eq!(realizations[0].dec, gauss.dec);
let some_different = realizations[1..]
.iter()
.any(|g| g.ra != gauss.ra || g.dec != gauss.dec);
assert!(
some_different,
"Expected at least one noisy realization to differ"
);
}
#[test]
fn test_generate_noisy_realizations_with_zero_noise() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: vector![1.5, 1.6, 1.7],
dec: vector![0.9, 1.0, 1.1],
time: vector![59000.0, 59001.0, 59002.0],
observer_helio_position: Matrix3::identity(),
};
let errors_ra = vector![0.0, 0.0, 0.0];
let errors_dec = vector![0.0, 0.0, 0.0];
let mut rng = StdRng::seed_from_u64(123);
let realizations =
gauss.generate_noisy_realizations(&errors_ra, &errors_dec, 3, 1.0, &mut rng);
for g in realizations {
assert_eq!(g.ra, gauss.ra);
assert_eq!(g.dec, gauss.dec);
}
}
#[test]
fn test_no_noise() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: vector![1.5, 1.6, 1.7],
dec: vector![0.9, 1.0, 1.1],
time: vector![59000.0, 59001.0, 59002.0],
observer_helio_position: Matrix3::identity(),
};
let errors_ra = vector![0.0, 0.0, 0.0];
let errors_dec = vector![0.0, 0.0, 0.0];
let mut rng = StdRng::seed_from_u64(123);
let realizations =
gauss.generate_noisy_realizations(&errors_ra, &errors_dec, 0, 1.0, &mut rng);
assert_eq!(realizations.len(), 1); assert_eq!(realizations[0], gauss);
}
#[test]
fn test_generate_noisy_realizations_with_custom_scale() {
let gauss = GaussObs {
idx_obs: Vector3::new(0, 1, 2),
ra: vector![1.0, 1.1, 1.2],
dec: vector![0.5, 0.6, 0.7],
time: vector![59000.0, 59001.0, 59002.0],
observer_helio_position: Matrix3::identity(),
};
let errors_ra = vector![1e-6, 1e-6, 1e-6];
let errors_dec = vector![1e-6, 1e-6, 1e-6];
let mut rng_low = StdRng::seed_from_u64(42);
let mut rng_high = StdRng::seed_from_u64(42);
let low_noise =
gauss.generate_noisy_realizations(&errors_ra, &errors_dec, 1, 0.1, &mut rng_low);
let high_noise =
gauss.generate_noisy_realizations(&errors_ra, &errors_dec, 1, 10.0, &mut rng_high);
let diff_low = (low_noise[1].ra - gauss.ra).norm();
let diff_high = (high_noise[1].ra - gauss.ra).norm();
assert!(
diff_high > diff_low,
"High noise should cause greater deviation than low noise"
);
}
}
}