use crate::outfit_errors::OutfitError;
use std::cmp::Ordering::{Equal, Greater, Less};
use std::fmt;
pub mod gauss;
pub mod gauss_result;
#[derive(Debug, Clone)]
pub struct IODParams {
pub n_noise_realizations: usize,
pub noise_scale: f64,
pub extf: f64,
pub dtmax: f64,
pub dt_min: f64,
pub dt_max_triplet: f64,
pub optimal_interval_time: f64,
pub max_obs_for_triplets: usize,
pub max_triplets: u32,
pub gap_max: f64,
pub max_ecc: f64,
pub max_perihelion_au: f64,
pub min_rho2_au: f64,
pub aberth_max_iter: u32,
pub aberth_eps: f64,
pub kepler_eps: f64,
pub max_tested_solutions: usize,
pub r2_min_au: f64,
pub r2_max_au: f64,
pub newton_eps: f64,
pub newton_max_it: usize,
pub root_imag_eps: f64,
#[cfg(feature = "parallel")]
pub batch_size: usize,
}
impl IODParams {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> IODParamsBuilder {
IODParamsBuilder::new()
}
}
impl Default for IODParams {
fn default() -> Self {
IODParams {
n_noise_realizations: 20,
noise_scale: 1.0,
extf: -1.0,
dtmax: 30.0,
dt_min: 0.03,
dt_max_triplet: 150.0,
optimal_interval_time: 20.0,
max_obs_for_triplets: 100,
max_triplets: 10,
gap_max: 8.0 / 24.0,
max_ecc: 5.0,
max_perihelion_au: 1.0e3,
min_rho2_au: 0.01,
aberth_max_iter: 50,
aberth_eps: 1.0e-6,
kepler_eps: 1e3 * f64::EPSILON,
max_tested_solutions: 3,
r2_min_au: 0.05,
r2_max_au: 200.0,
newton_eps: 1.0e-10,
newton_max_it: 50,
root_imag_eps: 1.0e-6,
#[cfg(feature = "parallel")]
batch_size: 4,
}
}
}
#[derive(Debug, Clone)]
pub struct IODParamsBuilder {
params: IODParams,
}
impl Default for IODParamsBuilder {
fn default() -> Self {
Self::new()
}
}
impl IODParamsBuilder {
pub fn new() -> Self {
Self {
params: IODParams::default(),
}
}
pub fn n_noise_realizations(mut self, v: usize) -> Self {
self.params.n_noise_realizations = v;
self
}
pub fn noise_scale(mut self, v: f64) -> Self {
self.params.noise_scale = v;
self
}
pub fn extf(mut self, v: f64) -> Self {
self.params.extf = v;
self
}
pub fn dtmax(mut self, v: f64) -> Self {
self.params.dtmax = v;
self
}
pub fn dt_min(mut self, v: f64) -> Self {
self.params.dt_min = v;
self
}
pub fn dt_max_triplet(mut self, v: f64) -> Self {
self.params.dt_max_triplet = v;
self
}
pub fn optimal_interval_time(mut self, v: f64) -> Self {
self.params.optimal_interval_time = v;
self
}
pub fn max_obs_for_triplets(mut self, v: usize) -> Self {
self.params.max_obs_for_triplets = v;
self
}
pub fn max_triplets(mut self, v: u32) -> Self {
self.params.max_triplets = v;
self
}
pub fn gap_max(mut self, v: f64) -> Self {
self.params.gap_max = v;
self
}
pub fn max_ecc(mut self, v: f64) -> Self {
self.params.max_ecc = v;
self
}
pub fn max_perihelion_au(mut self, v: f64) -> Self {
self.params.max_perihelion_au = v;
self
}
pub fn min_rho2_au(mut self, v: f64) -> Self {
self.params.min_rho2_au = v;
self
}
pub fn r2_min_au(mut self, v: f64) -> Self {
self.params.r2_min_au = v;
self
}
pub fn r2_max_au(mut self, v: f64) -> Self {
self.params.r2_max_au = v;
self
}
pub fn aberth_max_iter(mut self, v: u32) -> Self {
self.params.aberth_max_iter = v;
self
}
pub fn aberth_eps(mut self, v: f64) -> Self {
self.params.aberth_eps = v;
self
}
pub fn kepler_eps(mut self, v: f64) -> Self {
self.params.kepler_eps = v;
self
}
pub fn max_tested_solutions(mut self, v: usize) -> Self {
self.params.max_tested_solutions = v;
self
}
pub fn newton_eps(mut self, v: f64) -> Self {
self.params.newton_eps = v;
self
}
pub fn newton_max_it(mut self, v: usize) -> Self {
self.params.newton_max_it = v;
self
}
pub fn root_imag_eps(mut self, v: f64) -> Self {
self.params.root_imag_eps = v;
self
}
#[cfg(feature = "parallel")]
pub fn batch_size(mut self, v: usize) -> Self {
self.params.batch_size = v;
self
}
#[inline]
fn gt0(x: f64) -> bool {
x.partial_cmp(&0.0) == Some(Greater)
}
#[inline]
fn ge0(x: f64) -> bool {
matches!(x.partial_cmp(&0.0), Some(Greater) | Some(Equal))
}
#[inline]
fn le(a: f64, b: f64) -> bool {
matches!(a.partial_cmp(&b), Some(Less) | Some(Equal))
}
pub fn build(self) -> Result<IODParams, OutfitError> {
let p = &self.params;
if !Self::ge0(p.noise_scale) {
return Err(OutfitError::InvalidIODParameter(
"noise_scale must be non-negative".into(),
));
}
if !Self::ge0(p.dt_min) || !Self::ge0(p.dt_max_triplet) || !Self::ge0(p.dtmax) {
return Err(OutfitError::InvalidIODParameter(
"time parameters must be non-negative".into(),
));
}
if !Self::ge0(p.max_ecc) {
return Err(OutfitError::InvalidIODParameter(
"max_ecc must be >= 0".into(),
));
}
if !Self::ge0(p.root_imag_eps) {
return Err(OutfitError::InvalidIODParameter(
"root_imag_eps must be >= 0".into(),
));
}
if !Self::gt0(p.max_perihelion_au) {
return Err(OutfitError::InvalidIODParameter(
"max_perihelion_au must be > 0".into(),
));
}
if !Self::gt0(p.min_rho2_au) {
return Err(OutfitError::InvalidIODParameter(
"min_rho2_au must be > 0".into(),
));
}
if !Self::gt0(p.aberth_eps) {
return Err(OutfitError::InvalidIODParameter(
"aberth_eps must be > 0".into(),
));
}
if !Self::gt0(p.kepler_eps) {
return Err(OutfitError::InvalidIODParameter(
"kepler_eps must be > 0".into(),
));
}
if !Self::gt0(p.newton_eps) {
return Err(OutfitError::InvalidIODParameter(
"newton_eps must be > 0".into(),
));
}
if p.newton_max_it == 0 {
return Err(OutfitError::InvalidIODParameter(
"newton_max_it must be >= 1".into(),
));
}
if p.aberth_max_iter == 0 {
return Err(OutfitError::InvalidIODParameter(
"aberth_max_iter must be >= 1".into(),
));
}
if p.max_tested_solutions < 1 {
return Err(OutfitError::InvalidIODParameter(
"max_tested_solutions must be >= 1".into(),
));
}
let ok_r2min = Self::gt0(p.r2_min_au);
let ok_r2max = Self::gt0(p.r2_max_au);
let ok_order = Self::le(p.r2_min_au, p.r2_max_au);
if !(ok_r2min && ok_r2max && ok_order) {
return Err(OutfitError::InvalidIODParameter(
"require 0 < r2_min_au <= r2_max_au".into(),
));
}
Ok(self.params)
}
}
impl fmt::Display for IODParams {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
const PARAM_COL: usize = 50; writeln!(f, "Initial Orbit Determination Parameters")?;
writeln!(f, "-------------------------------------")?;
macro_rules! line {
($fmt:expr, $val:expr, $comment:expr) => {{
let s = format!($fmt, $val);
let pad = if s.len() < PARAM_COL {
" ".repeat(PARAM_COL - s.len())
} else {
" ".to_string()
};
writeln!(f, " {}{}# {}", s, pad, $comment)
}};
}
writeln!(f, "[Triplet generation / Monte Carlo]")?;
line!(
"n_noise_realizations = {}",
self.n_noise_realizations,
"Number of noisy copies per triplet"
)?;
line!(
"noise_scale = {:.3}",
self.noise_scale,
"Scale factor for RA/DEC uncertainties"
)?;
line!(
"extf = {:.3}",
self.extf,
"Extrapolation factor for RMS window"
)?;
line!(
"dtmax = {:.3} d",
self.dtmax,
"Minimum RMS evaluation window"
)?;
line!(
"dt_min = {:.3} d",
self.dt_min,
"Minimum span between first/last obs"
)?;
line!(
"dt_max_triplet = {:.3} d",
self.dt_max_triplet,
"Maximum allowed triplet span"
)?;
line!(
"optimal_interval_time= {:.3} d",
self.optimal_interval_time,
"Target spacing inside triplet"
)?;
line!(
"max_obs_for_triplets = {}",
self.max_obs_for_triplets,
"Cap on obs used to build triplets"
)?;
line!(
"max_triplets = {}",
self.max_triplets,
"Maximum number of triplets evaluated"
)?;
line!(
"gap_max = {:.3} d",
self.gap_max,
"Max intra-batch time gap"
)?;
writeln!(f, "\n[Physical plausibility / filtering]")?;
line!(
"max_ecc = {:.3}",
self.max_ecc,
"Maximum eccentricity accepted"
)?;
line!(
"max_perihelion_au = {:.3} AU",
self.max_perihelion_au,
"Maximum perihelion distance"
)?;
line!(
"min_rho2_au = {:.3} AU",
self.min_rho2_au,
"Minimum topocentric distance"
)?;
line!(
"r2_min_au = {:.3} AU",
self.r2_min_au,
"Minimum heliocentric distance"
)?;
line!(
"r2_max_au = {:.3} AU",
self.r2_max_au,
"Maximum heliocentric distance"
)?;
writeln!(f, "\n[Numerical tolerances / iterations]")?;
line!(
"newton_eps = {:.1e}",
self.newton_eps,
"Tolerance for Newton–Raphson"
)?;
line!(
"newton_max_it = {}",
self.newton_max_it,
"Max Newton–Raphson iterations"
)?;
line!(
"root_imag_eps = {:.1e}",
self.root_imag_eps,
"Max imaginary part for promoted roots"
)?;
line!(
"aberth_max_iter = {}",
self.aberth_max_iter,
"Max iterations for Aberth solver"
)?;
line!(
"aberth_eps = {:.1e}",
self.aberth_eps,
"Convergence tolerance for Aberth solver"
)?;
line!(
"kepler_eps = {:.1e}",
self.kepler_eps,
"Tolerance in Kepler solver"
)?;
line!(
"max_tested_solutions = {}",
self.max_tested_solutions,
"Max Gauss solutions kept"
)?;
#[cfg(feature = "parallel")]
{
line!(
"batch_size = {}",
self.batch_size,
"Number of trajectory batches to process in parallel"
)?;
}
Ok(())
} else {
write!(
f,
"IODParams(n_noise_realizations={}, noise_scale={:.2}, extf={:.2}, dtmax={:.1}d, dt_min={:.2}d, dt_max_triplet={:.1}d, max_triplets={}, max_ecc={:.2}, perihelion≤{:.1}AU, r2∈[{:.2},{:.1}]AU)",
self.n_noise_realizations,
self.noise_scale,
self.extf,
self.dtmax,
self.dt_min,
self.dt_max_triplet,
self.max_triplets,
self.max_ecc,
self.max_perihelion_au,
self.r2_min_au,
self.r2_max_au,
)
}
}
}