#[cfg(feature = "serde")]
use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;
use crate::{
kepler::{
brent_dekker_solver::solve_kepuni_brent_dekker,
prelim_elliptic, prelim_hyperbolic,
prelim_kepler::prelim_parabolic::{prelim_parabolic, ParabolicPrelimMethod},
solve_kepuni_with_guess, UniversalKeplerSolution,
},
OutfitError,
};
use super::orbit_type::OrbitType;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy)]
pub struct SolverParams {
pub convergency: f64,
pub psi_guess: Option<f64>,
pub max_iter_prelim_kepuni: usize,
pub parabolic_solving_method: ParabolicPrelimMethod,
}
impl Default for SolverParams {
fn default() -> Self {
Self {
convergency: 100.0 * f64::EPSILON,
psi_guess: None,
max_iter_prelim_kepuni: 20,
parabolic_solving_method: ParabolicPrelimMethod::Cardano,
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy)]
pub enum SolverKind {
NewtonRaphson,
BrentDecker,
Auto,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy)]
pub struct SolverType {
pub kind: SolverKind,
pub params: SolverParams,
}
impl Default for SolverType {
fn default() -> Self {
Self {
kind: SolverKind::NewtonRaphson,
params: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct UniversalKeplerParams {
pub dt: f64,
pub r0: f64,
pub sig0: f64,
pub mu: f64,
pub alpha: f64,
pub e0: f64,
pub solver_type: SolverType,
}
impl UniversalKeplerParams {
pub fn orbit_type(&self) -> OrbitType {
OrbitType::from_alpha(self.alpha)
}
#[inline]
pub fn reject_parabolic_orbit(&self) -> Option<()> {
match self.orbit_type() {
OrbitType::Parabolic => None,
OrbitType::Elliptic | OrbitType::Hyperbolic => Some(()),
}
}
pub fn solve(&self) -> Result<UniversalKeplerSolution, OutfitError> {
match self.solver_type.kind {
SolverKind::NewtonRaphson => {
solve_kepuni_with_guess(self).ok_or(OutfitError::NewtonRaphsonKeplerConvergence)
}
SolverKind::BrentDecker => {
solve_kepuni_brent_dekker(self).ok_or(OutfitError::BrentDekkerKeplerConvergence)
}
SolverKind::Auto => solve_kepuni_with_guess(self)
.or_else(|| solve_kepuni_brent_dekker(self))
.ok_or(OutfitError::BrentDekkerKeplerConvergence),
}
}
pub fn prelim_kepuni(&self) -> Option<f64> {
match self.orbit_type() {
OrbitType::Elliptic => Some(prelim_elliptic(self)),
OrbitType::Hyperbolic => Some(prelim_hyperbolic(self)),
OrbitType::Parabolic => Some(prelim_parabolic(self)),
}
}
}
#[cfg(test)]
mod kepler_params_tests {
use crate::kepler::s_funct;
use super::*;
use approx::assert_abs_diff_eq;
use proptest::prelude::*;
const MU_SUN: f64 = 2.959_122_082_855_911e-4;
fn kepler_residual(sol: &UniversalKeplerSolution, params: &UniversalKeplerParams) -> f64 {
let (_, s1, s2, s3) = s_funct(sol.universal_anomaly, params.alpha);
(params.r0 * s1 + params.sig0 * s2 + s3 - params.mu.sqrt() * params.dt).abs()
}
fn elliptic_params(dt: f64, a: f64, kind: SolverKind) -> UniversalKeplerParams {
let alpha = -1.0 / a;
let r0 = a;
let sig0 = 0.0;
UniversalKeplerParams {
dt,
r0,
sig0,
mu: MU_SUN,
alpha,
e0: 0.01,
solver_type: SolverType {
kind,
params: SolverParams::default(),
},
}
}
fn hyperbolic_params(dt: f64, c3: f64, kind: SolverKind) -> UniversalKeplerParams {
let alpha = c3 / MU_SUN; let r0 = 1.5; let sig0 = 0.001;
UniversalKeplerParams {
dt,
r0,
sig0,
mu: MU_SUN,
alpha,
e0: 1.5,
solver_type: SolverType {
kind,
params: SolverParams::default(),
},
}
}
#[test]
fn newton_solves_elliptic_short_arc() {
let params = elliptic_params(
10.0, 1.0, SolverKind::NewtonRaphson,
);
let sol = params
.solve()
.expect("Newton should converge on elliptic short arc");
assert!(
kepler_residual(&sol, ¶ms) < 1e-10,
"residual too large: {}",
kepler_residual(&sol, ¶ms)
);
}
#[test]
fn newton_solves_elliptic_full_orbit() {
let params = elliptic_params(365.0, 1.0, SolverKind::NewtonRaphson);
let sol = params
.solve()
.expect("Newton should converge on full elliptic orbit");
assert!(
kepler_residual(&sol, ¶ms) < 1e-9,
"residual too large: {}",
kepler_residual(&sol, ¶ms)
);
}
#[test]
fn newton_solves_elliptic_negative_dt() {
let params = elliptic_params(-30.0, 1.0, SolverKind::NewtonRaphson);
let sol = params
.solve()
.expect("Newton should converge for negative dt");
assert!(kepler_residual(&sol, ¶ms) < 1e-10);
}
#[test]
fn newton_solves_hyperbolic() {
let params = hyperbolic_params(
5.0,
1e-5, SolverKind::NewtonRaphson,
);
let sol = params
.solve()
.expect("Newton should converge on hyperbolic orbit");
assert!(
kepler_residual(&sol, ¶ms) < 1e-10,
"residual: {}",
kepler_residual(&sol, ¶ms)
);
}
#[test]
fn newton_warm_start_consistent_with_cold_start() {
let mut cold = elliptic_params(50.0, 2.0, SolverKind::NewtonRaphson);
let sol_cold = cold.solve().expect("cold start should converge");
cold.solver_type = SolverType {
kind: SolverKind::NewtonRaphson,
params: SolverParams {
psi_guess: Some(sol_cold.universal_anomaly),
..Default::default()
},
};
let sol_warm = cold.solve().expect("warm start should converge");
assert_abs_diff_eq!(
sol_cold.universal_anomaly,
sol_warm.universal_anomaly,
epsilon = 1e-10
);
}
#[test]
fn brent_solves_elliptic_short_arc() {
let params = elliptic_params(10.0, 1.0, SolverKind::BrentDecker);
let sol = params
.solve()
.expect("Brent should converge on elliptic short arc");
assert!(
kepler_residual(&sol, ¶ms) < 1e-10,
"residual: {}",
kepler_residual(&sol, ¶ms)
);
}
#[test]
fn brent_solves_elliptic_full_orbit() {
let params = elliptic_params(365.0, 1.0, SolverKind::BrentDecker);
let sol = params
.solve()
.expect("Brent should converge on full elliptic orbit");
assert!(kepler_residual(&sol, ¶ms) < 1e-9);
}
#[test]
fn brent_solves_elliptic_negative_dt() {
let params = elliptic_params(-30.0, 1.0, SolverKind::BrentDecker);
let sol = params
.solve()
.expect("Brent should converge for negative dt");
assert!(kepler_residual(&sol, ¶ms) < 1e-10);
}
#[test]
fn brent_solves_hyperbolic() {
let params = hyperbolic_params(5.0, 1e-5, SolverKind::BrentDecker);
let sol = params
.solve()
.expect("Brent should converge on hyperbolic orbit");
assert!(
kepler_residual(&sol, ¶ms) < 1e-10,
"residual: {}",
kepler_residual(&sol, ¶ms)
);
}
const PSI_CONSISTENCY_TOL: f64 = 1e-12;
fn assert_solvers_consistent(params_template: UniversalKeplerParams) {
let mut newton_params = params_template;
newton_params.solver_type.kind = SolverKind::NewtonRaphson;
let mut brent_params = params_template;
brent_params.solver_type.kind = SolverKind::BrentDecker;
let sol_newton = newton_params.solve().expect("Newton must converge");
let sol_brent = brent_params.solve().expect("Brent must converge");
assert!(
kepler_residual(&sol_newton, &newton_params) < 1e-10,
"Newton residual too large"
);
assert!(
kepler_residual(&sol_brent, &brent_params) < 1e-10,
"Brent residual too large"
);
assert_abs_diff_eq!(
sol_newton.universal_anomaly,
sol_brent.universal_anomaly,
epsilon = PSI_CONSISTENCY_TOL
);
}
#[test]
fn solvers_consistent_elliptic_short_arc() {
assert_solvers_consistent(elliptic_params(10.0, 1.0, SolverKind::BrentDecker));
}
#[test]
fn solvers_consistent_elliptic_long_arc() {
assert_solvers_consistent(elliptic_params(200.0, 2.5, SolverKind::BrentDecker));
}
#[test]
fn solvers_consistent_elliptic_negative_dt() {
assert_solvers_consistent(elliptic_params(-45.0, 1.5, SolverKind::BrentDecker));
}
#[test]
fn solvers_consistent_hyperbolic() {
assert_solvers_consistent(hyperbolic_params(5.0, 1e-5, SolverKind::BrentDecker));
}
#[test]
fn solvers_consistent_outer_solar_system_elliptic() {
assert_solvers_consistent(elliptic_params(1000.0, 5.2, SolverKind::BrentDecker));
}
fn elliptic_strategy() -> impl Strategy<Value = (f64, f64)> {
(
0.3_f64..10.0_f64, 1.0_f64..500.0_f64, )
}
fn hyperbolic_strategy() -> impl Strategy<Value = (f64, f64)> {
(1e-6_f64..1e-3_f64, 1.0_f64..100.0_f64)
}
proptest! {
#[test]
fn proptest_newton_elliptic_residual(
(a, dt) in elliptic_strategy()
) {
let params = elliptic_params(dt, a, SolverKind::NewtonRaphson);
if let Ok(sol) = params.solve() {
prop_assert!(
kepler_residual(&sol, ¶ms) < 1e-9,
"Newton residual too large: {}",
kepler_residual(&sol, ¶ms)
);
}
}
#[test]
fn proptest_brent_elliptic_residual(
(a, dt) in elliptic_strategy()
) {
let params = elliptic_params(dt, a, SolverKind::BrentDecker);
if let Ok(sol) = params.solve() {
prop_assert!(
kepler_residual(&sol, ¶ms) < 1e-9,
"Brent residual too large: {}",
kepler_residual(&sol, ¶ms)
);
}
}
#[test]
fn proptest_solvers_consistent_elliptic(
(a, dt) in elliptic_strategy()
) {
let newton_params = elliptic_params(dt, a, SolverKind::NewtonRaphson);
let mut brent_params = newton_params;
brent_params.solver_type.kind = SolverKind::BrentDecker;
if let (Ok(sol_n), Ok(sol_b)) = (newton_params.solve(), brent_params.solve()) {
prop_assert!(
(sol_n.universal_anomaly - sol_b.universal_anomaly).abs()
< PSI_CONSISTENCY_TOL,
"psi mismatch: Newton={}, Brent={}",
sol_n.universal_anomaly,
sol_b.universal_anomaly
);
}
}
#[test]
fn proptest_solvers_consistent_hyperbolic(
(c3, dt) in hyperbolic_strategy()
) {
let newton_params = hyperbolic_params(dt, c3, SolverKind::NewtonRaphson);
let mut brent_params = newton_params;
brent_params.solver_type.kind = SolverKind::BrentDecker;
if let (Ok(sol_n), Ok(sol_b)) = (newton_params.solve(), brent_params.solve()) {
prop_assert!(
(sol_n.universal_anomaly - sol_b.universal_anomaly).abs()
< PSI_CONSISTENCY_TOL,
"psi mismatch on hyperbolic: Newton={}, Brent={}",
sol_n.universal_anomaly,
sol_b.universal_anomaly
);
}
}
#[test]
fn proptest_orbit_type_consistent_with_alpha(
alpha in prop_oneof![
(-1e-2_f64..-1e-6_f64), (1e-6_f64..1e-2_f64), ]
) {
let params = UniversalKeplerParams {
dt: 10.0,
r0: 1.0,
sig0: 0.0,
mu: MU_SUN,
alpha,
e0: 0.5,
solver_type: SolverType::default(),
};
match params.orbit_type() {
OrbitType::Elliptic => prop_assert!(alpha < 0.0),
OrbitType::Hyperbolic => prop_assert!(alpha > 0.0),
OrbitType::Parabolic => prop_assert!(alpha == 0.0),
}
}
}
}
#[cfg(test)]
mod tests_prelim_kepuni {
use super::*;
const MU: f64 = 1.0;
const CONTR: f64 = 1e-12;
fn make_params(
dt: f64,
r0: f64,
sig0: f64,
mu: f64,
alpha: f64,
e0: f64,
) -> UniversalKeplerParams {
UniversalKeplerParams {
dt,
r0,
sig0,
mu,
alpha,
e0,
solver_type: SolverType {
params: SolverParams {
convergency: CONTR,
..Default::default()
},
..Default::default()
},
}
}
#[test]
fn test_returns_none_for_alpha_zero() {
let params = make_params(1.0, 1.0, 0.0, MU, 0.0, 0.1);
let res = params.prelim_kepuni().unwrap();
assert_eq!(res, 0.8846222003969053);
}
#[test]
fn test_elliptic_small_eccentricity() {
let params = make_params(0.5, 1.0, 0.1, MU, -1.0, 1e-8);
let result = params.prelim_kepuni();
assert!(result.is_some());
assert!(result.unwrap().is_finite());
}
#[test]
fn test_elliptic_high_eccentricity() {
let params = make_params(0.1, 0.5, 0.2, MU, -1.0, 0.8);
let result = params.prelim_kepuni();
assert!(result.is_some());
assert!(result.unwrap().is_finite());
}
#[test]
fn test_hyperbolic_case() {
let params = make_params(0.3, 2.0, -0.1, MU, 1.0, 1.5);
let result = params.prelim_kepuni();
assert!(result.is_some());
assert!(result.unwrap().is_finite());
}
#[test]
fn test_negative_sig0_changes_direction() {
let alpha = -1.0;
let r0 = 1.0;
let e0 = 0.5;
let dt = 0.25;
let params_pos = make_params(dt, r0, 0.1, MU, alpha, e0);
let params_neg = make_params(dt, r0, -0.1, MU, alpha, e0);
let psi_pos = params_pos.prelim_kepuni().unwrap();
let psi_neg = params_neg.prelim_kepuni().unwrap();
assert!(
(psi_pos - psi_neg).abs() > 1e-8,
"psi did not change significantly when changing sig0 sign: {psi_pos} vs {psi_neg}"
);
}
#[test]
fn test_stability_long_dt() {
let params = make_params(50.0, 1.0, 0.1, MU, -1.0, 0.5);
let result = params.prelim_kepuni();
assert!(result.is_some());
assert!(result.unwrap().is_finite());
}
#[test]
fn test_edge_cosine_limits() {
let params = make_params(0.25, 2.0, 0.1, MU, -1.0, 0.1);
let result = params.prelim_kepuni();
assert!(result.is_some());
}
#[test]
fn test_prelim_kepuni_real_data() {
let dt = -20.765849999996135;
let r0 = 1.3803870211345761;
let sig0 = 3.701_354_484_003_874_8E-3;
let mu = 2.959_122_082_855_911_5E-4;
let alpha = -0.554_947_829_724_638_7;
let e0 = 0.283_599_599_137_344_5;
let params = UniversalKeplerParams {
dt,
r0,
sig0,
mu,
alpha,
e0,
solver_type: SolverType::default(),
};
let psi = params.prelim_kepuni().unwrap();
assert_eq!(psi, -0.2636637076378094);
let params2 = UniversalKeplerParams {
alpha: 0.554_947_829_724_638_7,
..params
};
let psi = params2.prelim_kepuni().unwrap();
assert_eq!(psi, -1.258980225923225);
let params3 = UniversalKeplerParams {
alpha: 0.0,
..params
};
let psi = params3.prelim_kepuni().unwrap();
assert_eq!(psi, -0.2568229151088884);
}
mod kepuni_prop_tests {
use super::*;
use proptest::prelude::*;
fn arb_params() -> impl Strategy<Value = UniversalKeplerParams> {
(
-10.0..10.0f64,
0.1..5.0f64,
-2.0..2.0f64,
0.5..2.0f64,
prop_oneof![(-5.0..-0.01f64), (0.01..5.0f64)],
0.0..3.0f64,
1e-14..1e-8f64,
)
.prop_map(|(dt, r0, sig0, mu, alpha, e0, contr)| {
UniversalKeplerParams {
dt,
r0,
sig0,
mu,
alpha,
e0,
solver_type: SolverType {
params: SolverParams {
convergency: contr,
..Default::default()
},
..Default::default()
},
}
})
}
proptest! {
#[test]
fn prop_prelim_kepuni_behaves_well(params in arb_params()) {
let result = params.prelim_kepuni();
prop_assert!(result.is_some());
prop_assert!(result.unwrap().is_finite());
}
}
proptest! {
#[test]
fn prop_prelim_kepuni_alpha_zero(
dt in -10.0..10.0f64,
r0 in 0.1..5.0f64,
sig0 in -2.0..2.0f64,
mu in 0.5..2.0f64,
e0 in 0.0..3.0f64,
contr in 1e-14..1e-8f64
) {
let params = UniversalKeplerParams { dt, r0, sig0, mu, alpha: 0.0, e0, solver_type: SolverType {
params: SolverParams {
convergency: contr,
..Default::default()
},
..Default::default()
}
};
let result = params.prelim_kepuni();
if let Some(psi0) = result {
prop_assert!(psi0.is_finite());
}
}
}
proptest! {
#[test]
fn prop_sig0_influences_psi(params in arb_params()) {
prop_assume!(params.dt.abs() > 1e-6);
prop_assume!(params.e0 > 1e-6);
prop_assume!(params.r0 > 1e-6);
let mut params_pos = params;
params_pos.sig0 = 0.1;
let mut params_neg = params;
params_neg.sig0 = -0.1;
let res_pos = params_pos.prelim_kepuni();
let res_neg = params_neg.prelim_kepuni();
prop_assume!(res_pos.is_some() && res_neg.is_some());
let diff = (res_pos.unwrap() - res_neg.unwrap()).abs();
prop_assert!(diff >= 0.0);
}
}
}
}