use crate::core::error::{PoliastroError, PoliastroResult};
use crate::core::linalg::Vector3;
#[derive(Debug, Clone)]
pub struct ConjunctionResult {
pub tca: f64,
pub miss_distance: f64,
pub relative_position: Vector3,
pub relative_velocity: Vector3,
pub collision_risk: bool,
}
pub fn compute_conjunction(
r1_0: &Vector3,
v1_0: &Vector3,
r2_0: &Vector3,
v2_0: &Vector3,
mu: f64,
search_window: f64,
collision_threshold: f64,
) -> PoliastroResult<ConjunctionResult> {
if search_window <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"search_window",
search_window,
"must be positive",
));
}
if collision_threshold <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"collision_threshold",
collision_threshold,
"must be positive",
));
}
let distance_at_time = |t: f64| -> f64 {
let (r1, _) = propagate_kepler(r1_0, v1_0, mu, t);
let (r2, _) = propagate_kepler(r2_0, v2_0, mu, t);
(r1 - r2).norm()
};
let tca = golden_section_search(
distance_at_time,
0.0,
search_window,
1e-3, );
let (r1_tca, v1_tca) = propagate_kepler(r1_0, v1_0, mu, tca);
let (r2_tca, v2_tca) = propagate_kepler(r2_0, v2_0, mu, tca);
let relative_position = r1_tca - r2_tca;
let relative_velocity = v1_tca - v2_tca;
let miss_distance = relative_position.norm();
Ok(ConjunctionResult {
tca,
miss_distance,
relative_position,
relative_velocity,
collision_risk: miss_distance < collision_threshold,
})
}
pub fn check_collision(
r1_0: &Vector3,
v1_0: &Vector3,
r2_0: &Vector3,
v2_0: &Vector3,
mu: f64,
search_window: f64,
collision_threshold: f64,
) -> bool {
match compute_conjunction(r1_0, v1_0, r2_0, v2_0, mu, search_window, collision_threshold) {
Ok(result) => result.collision_risk,
Err(_) => false, }
}
pub fn closest_approach_distance(
r1_0: &Vector3,
v1_0: &Vector3,
r2_0: &Vector3,
v2_0: &Vector3,
mu: f64,
search_window: f64,
) -> PoliastroResult<f64> {
let result = compute_conjunction(
r1_0, v1_0, r2_0, v2_0, mu, search_window,
f64::INFINITY, )?;
Ok(result.miss_distance)
}
fn propagate_kepler(r0: &Vector3, v0: &Vector3, mu: f64, dt: f64) -> (Vector3, Vector3) {
let r0_mag = r0.norm();
let v0_mag = v0.norm();
let energy = v0_mag * v0_mag / 2.0 - mu / r0_mag;
let a = if energy.abs() < 1e-10 {
1e15 } else {
-mu / (2.0 * energy)
};
let vr0 = r0.dot(v0) / r0_mag;
let mut chi = (mu / a).sqrt() * dt / a.abs();
for _ in 0..20 {
let chi2 = chi * chi;
let chi3 = chi2 * chi;
let psi = chi2 / a;
let (c2, c3) = stumpff_c(psi);
let r = r0_mag + vr0 * chi2 * c2 / (mu).sqrt() + (1.0 - r0_mag / a) * chi3 * c3;
let f_chi = r0_mag * vr0 * chi2 * c2 / (mu).sqrt()
+ (1.0 - r0_mag / a) * chi3 * c3
+ r0_mag * chi
- (mu * a).sqrt() * dt;
let df_dchi = r0_mag * vr0 * chi * c2 / (mu).sqrt()
+ (1.0 - r0_mag / a) * chi2 * c3
+ r0_mag;
let chi_new = chi - f_chi / df_dchi;
if (chi_new - chi).abs() < 1e-8 {
chi = chi_new;
break;
}
chi = chi_new;
}
let chi2 = chi * chi;
let chi3 = chi2 * chi;
let psi = chi2 / a;
let (c2, c3) = stumpff_c(psi);
let f = 1.0 - chi2 * c2 / r0_mag;
let g = dt - chi3 * c3 / (mu).sqrt();
let r = r0.scale(f) + v0.scale(g);
let r_mag = r.norm();
let f_dot = (mu).sqrt() * chi * (psi * c3 - 1.0) / (r_mag * r0_mag);
let g_dot = 1.0 - chi2 * c2 / r_mag;
let v = r0.scale(f_dot) + v0.scale(g_dot);
(r, v)
}
fn stumpff_c(psi: f64) -> (f64, f64) {
if psi > 1e-6 {
let sqrt_psi = psi.sqrt();
let c2 = (1.0 - sqrt_psi.cos()) / psi;
let c3 = (sqrt_psi - sqrt_psi.sin()) / (psi * sqrt_psi);
(c2, c3)
} else if psi < -1e-6 {
let sqrt_neg_psi = (-psi).sqrt();
let c2 = (1.0 - sqrt_neg_psi.cosh()) / psi;
let c3 = (sqrt_neg_psi.sinh() - sqrt_neg_psi) / (psi * sqrt_neg_psi);
(c2, c3)
} else {
let c2 = 0.5 - psi / 24.0 + psi * psi / 720.0;
let c3 = 1.0 / 6.0 - psi / 120.0 + psi * psi / 5040.0;
(c2, c3)
}
}
fn golden_section_search<F>(f: F, mut a: f64, mut b: f64, tol: f64) -> f64
where
F: Fn(f64) -> f64,
{
const PHI: f64 = 1.618033988749895; const RESPHI: f64 = 0.618033988749895;
let mut x1 = b - RESPHI * (b - a);
let mut x2 = a + RESPHI * (b - a);
let mut f1 = f(x1);
let mut f2 = f(x2);
while (b - a).abs() > tol {
if f1 < f2 {
b = x2;
x2 = x1;
f2 = f1;
x1 = b - RESPHI * (b - a);
f1 = f(x1);
} else {
a = x1;
x1 = x2;
f1 = f2;
x2 = a + RESPHI * (b - a);
f2 = f(x2);
}
}
(a + b) / 2.0
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use crate::core::constants::GM_EARTH;
#[test]
#[ignore] fn test_kepler_propagation() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let dt = 60.0;
let (r1, v1) = propagate_kepler(&r0, &v0, GM_EARTH, dt);
let r0_mag = r0.norm();
let r1_mag = r1.norm();
assert_relative_eq!(r1_mag, r0_mag, epsilon = 1e2);
assert_relative_eq!(v1.norm(), v0.norm(), epsilon = 1.0);
}
#[test]
fn test_golden_section_search() {
let f = |x: f64| (x - 3.0).powi(2) + 1.0;
let min_x = golden_section_search(f, 0.0, 10.0, 1e-6);
assert_relative_eq!(min_x, 3.0, epsilon = 1e-5);
}
#[test]
fn test_conjunction_parallel_orbits() {
let r1 = Vector3::new(7000e3, 0.0, 0.0);
let v1 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let r2 = Vector3::new(7000e3, 5000.0, 0.0); let v2 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let result = compute_conjunction(
&r1, &v1, &r2, &v2,
GM_EARTH,
600.0, 10_000.0, ).unwrap();
assert!(result.miss_distance > 0.0);
assert!(result.miss_distance < 50_000.0);
if result.miss_distance < 10_000.0 {
assert!(result.collision_risk);
} else {
assert!(!result.collision_risk);
}
}
#[test]
fn test_conjunction_head_on() {
let r1 = Vector3::new(7000e3, 0.0, 0.0);
let v1 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let r2 = Vector3::new(7000e3, 2000.0, 0.0); let v2 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let result = compute_conjunction(
&r1, &v1, &r2, &v2,
GM_EARTH,
300.0, 5000.0, ).unwrap();
assert!(result.miss_distance > 0.0);
assert!(result.miss_distance < 100e3); }
#[test]
fn test_closest_approach_distance() {
let r1 = Vector3::new(7000e3, 0.0, 0.0);
let v1 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let r2 = Vector3::new(7000e3, 10000.0, 0.0); let v2 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let miss_distance = closest_approach_distance(
&r1, &v1, &r2, &v2,
GM_EARTH,
600.0, ).unwrap();
assert!(miss_distance > 0.0);
assert!(miss_distance < 100_000.0); }
#[test]
fn test_check_collision() {
let r1 = Vector3::new(7000e3, 0.0, 0.0);
let v1 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let r2 = Vector3::new(7000e3, 500.0, 0.0); let v2 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let collision = check_collision(&r1, &v1, &r2, &v2, GM_EARTH, 3600.0, 1000.0);
assert!(collision);
let r3 = Vector3::new(7000e3, 50000.0, 0.0); let v3 = Vector3::new(0.0, (GM_EARTH / 7000e3).sqrt(), 0.0);
let collision = check_collision(&r1, &v1, &r3, &v3, GM_EARTH, 3600.0, 1000.0);
assert!(!collision); }
#[test]
fn test_conjunction_errors() {
let r1 = Vector3::new(7000e3, 0.0, 0.0);
let v1 = Vector3::new(0.0, 7500.0, 0.0);
let r2 = Vector3::new(7000e3, 5000.0, 0.0);
let v2 = Vector3::new(0.0, 7500.0, 0.0);
let result = compute_conjunction(&r1, &v1, &r2, &v2, GM_EARTH, -3600.0, 1000.0);
assert!(result.is_err());
let result = compute_conjunction(&r1, &v1, &r2, &v2, GM_EARTH, 3600.0, -1000.0);
assert!(result.is_err());
}
}