use std::f64::consts::PI;
use crate::core::{PoliastroError, PoliastroResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PhasingOrbitResult {
pub r_original: f64,
pub a_phasing: f64,
pub r_phasing_periapsis: f64,
pub r_phasing_apoapsis: f64,
pub e_phasing: f64,
pub mu: f64,
pub v_original: f64,
pub v_phasing_periapsis: f64,
pub v_phasing_apoapsis: f64,
pub delta_v_enter: f64,
pub delta_v_exit: f64,
pub delta_v_total: f64,
pub period_original: f64,
pub period_phasing: f64,
pub num_phasing_orbits: f64,
pub phasing_time: f64,
pub phase_change_per_orbit: f64,
pub total_phase_change: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CoorbitalRendezvousResult {
pub r_orbit: f64,
pub mu: f64,
pub initial_phase_difference: f64,
pub chaser_ahead: bool,
pub phasing: PhasingOrbitResult,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CoplanarRendezvousResult {
pub r_chaser: f64,
pub r_target: f64,
pub mu: f64,
pub a_transfer: f64,
pub e_transfer: f64,
pub transfer_time: f64,
pub lead_angle: f64,
pub required_phase_angle: f64,
pub current_phase_angle: f64,
pub wait_time: f64,
pub wait_orbits: f64,
pub delta_v1: f64,
pub delta_v2: f64,
pub delta_v_total: f64,
}
pub struct Rendezvous;
impl Rendezvous {
pub fn phasing_orbit(
r_original: f64,
phase_change: f64,
num_orbits: f64,
mu: f64,
) -> PoliastroResult<PhasingOrbitResult> {
if r_original <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_original",
r_original,
"must be positive",
));
}
if mu <= 0.0 {
return Err(PoliastroError::invalid_parameter("mu", mu, "must be positive"));
}
if phase_change.abs() < 1e-10 {
return Err(PoliastroError::invalid_parameter(
"phase_change",
phase_change,
"must be non-zero",
));
}
if num_orbits < 1.0 {
return Err(PoliastroError::invalid_parameter(
"num_orbits",
num_orbits,
"must be at least 1",
));
}
let v_original = (mu / r_original).sqrt();
let period_original = 2.0 * PI * (r_original.powi(3) / mu).sqrt();
let omega_original = (mu / r_original.powi(3)).sqrt();
let phase_change_per_orbit = phase_change / num_orbits;
let period_phasing = (2.0 * PI - phase_change_per_orbit) / omega_original;
let a_phasing = ((period_phasing / (2.0 * PI)).powi(2) * mu).cbrt();
let (r_phasing_periapsis, r_phasing_apoapsis) = if phase_change > 0.0 {
(2.0 * a_phasing - r_original, r_original)
} else {
(r_original, 2.0 * a_phasing - r_original)
};
let min_safe_radius = r_original * 0.95; if r_phasing_periapsis < min_safe_radius {
return Err(PoliastroError::invalid_parameter(
"phase_change",
phase_change,
format!(
"phasing orbit periapsis ({r_phasing_periapsis:.0} m) too low - would be below safe altitude"
),
));
}
let e_phasing = (r_phasing_apoapsis - r_phasing_periapsis)
/ (r_phasing_apoapsis + r_phasing_periapsis);
let v_phasing_periapsis = (mu * (2.0 / r_phasing_periapsis - 1.0 / a_phasing)).sqrt();
let v_phasing_apoapsis = (mu * (2.0 / r_phasing_apoapsis - 1.0 / a_phasing)).sqrt();
let (delta_v_enter, delta_v_exit) = if phase_change > 0.0 {
let dv = (v_phasing_apoapsis - v_original).abs();
(dv, dv) } else {
let dv = (v_phasing_periapsis - v_original).abs();
(dv, dv) };
let delta_v_total = delta_v_enter + delta_v_exit;
let phasing_time = num_orbits * period_phasing;
Ok(PhasingOrbitResult {
r_original,
a_phasing,
r_phasing_periapsis,
r_phasing_apoapsis,
e_phasing,
mu,
v_original,
v_phasing_periapsis,
v_phasing_apoapsis,
delta_v_enter,
delta_v_exit,
delta_v_total,
period_original,
period_phasing,
num_phasing_orbits: num_orbits,
phasing_time,
phase_change_per_orbit,
total_phase_change: phase_change,
})
}
pub fn coorbital_rendezvous(
r_orbit: f64,
phase_difference: f64,
num_phasing_orbits: f64,
mu: f64,
) -> PoliastroResult<CoorbitalRendezvousResult> {
let mut phase_diff = phase_difference % (2.0 * PI);
if phase_diff < 0.0 {
phase_diff += 2.0 * PI;
}
let chaser_ahead = false;
let phase_change = phase_diff;
let phasing = Self::phasing_orbit(r_orbit, phase_change, num_phasing_orbits, mu)?;
Ok(CoorbitalRendezvousResult {
r_orbit,
mu,
initial_phase_difference: phase_diff,
chaser_ahead,
phasing,
})
}
pub fn coplanar_rendezvous(
r_chaser: f64,
r_target: f64,
current_phase: f64,
mu: f64,
) -> PoliastroResult<CoplanarRendezvousResult> {
if r_chaser <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_chaser",
r_chaser,
"must be positive",
));
}
if r_target <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_target",
r_target,
"must be positive",
));
}
if (r_chaser - r_target).abs() < 1e-6 {
return Err(PoliastroError::invalid_parameter(
"r_chaser, r_target",
r_chaser,
"orbits must be different - use coorbital_rendezvous instead",
));
}
if mu <= 0.0 {
return Err(PoliastroError::invalid_parameter("mu", mu, "must be positive"));
}
let mut phase = current_phase % (2.0 * PI);
if phase < 0.0 {
phase += 2.0 * PI;
}
let a_transfer = (r_chaser + r_target) / 2.0;
let e_transfer = (r_target - r_chaser).abs() / (r_target + r_chaser);
let transfer_time = PI * (a_transfer.powi(3) / mu).sqrt();
let omega_chaser = (mu / r_chaser.powi(3)).sqrt();
let omega_target = (mu / r_target.powi(3)).sqrt();
let lead_angle = omega_target * transfer_time;
let required_phase_angle = if r_chaser < r_target {
PI - lead_angle
} else {
PI - lead_angle
};
let mut req_phase = required_phase_angle % (2.0 * PI);
if req_phase < 0.0 {
req_phase += 2.0 * PI;
}
let phase_diff = req_phase - phase;
let omega_diff = omega_target - omega_chaser;
let wait_time = if omega_diff.abs() < 1e-12 {
0.0
} else {
let mut wt = phase_diff / omega_diff;
if wt < 0.0 {
let period_synodic = 2.0 * PI / omega_diff.abs();
wt += period_synodic;
}
wt
};
let wait_orbits = wait_time * omega_chaser / (2.0 * PI);
let v_chaser = (mu / r_chaser).sqrt();
let v_target = (mu / r_target).sqrt();
let v_transfer_at_chaser = (mu * (2.0 / r_chaser - 1.0 / a_transfer)).sqrt();
let v_transfer_at_target = (mu * (2.0 / r_target - 1.0 / a_transfer)).sqrt();
let delta_v1 = (v_transfer_at_chaser - v_chaser).abs();
let delta_v2 = (v_target - v_transfer_at_target).abs();
let delta_v_total = delta_v1 + delta_v2;
Ok(CoplanarRendezvousResult {
r_chaser,
r_target,
mu,
a_transfer,
e_transfer,
transfer_time,
lead_angle,
required_phase_angle: req_phase,
current_phase_angle: phase,
wait_time,
wait_orbits,
delta_v1,
delta_v2,
delta_v_total,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const EARTH_MU: f64 = 3.986004418e14; const EARTH_RADIUS: f64 = 6.371e6;
#[test]
fn test_phasing_orbit_catch_up() {
let r = EARTH_RADIUS + 400e3;
let phase_change = PI / 6.0; let num_orbits = 5.0;
let result = Rendezvous::phasing_orbit(r, phase_change, num_orbits, EARTH_MU).unwrap();
assert!(result.delta_v_total > 0.0);
assert!(result.phasing_time > 0.0);
assert_relative_eq!(result.total_phase_change, phase_change, epsilon = 1e-6);
assert_relative_eq!(
result.phase_change_per_orbit,
phase_change / num_orbits,
epsilon = 1e-6
);
assert!(result.a_phasing < r);
assert!(result.period_phasing < result.period_original);
}
#[test]
fn test_phasing_orbit_wait() {
let r = EARTH_RADIUS + 400e3;
let phase_change = -PI / 6.0; let num_orbits = 5.0;
let result = Rendezvous::phasing_orbit(r, phase_change, num_orbits, EARTH_MU).unwrap();
assert!(result.a_phasing > r);
assert!(result.period_phasing > result.period_original);
assert_relative_eq!(result.total_phase_change, phase_change, epsilon = 1e-6);
}
#[test]
fn test_coorbital_rendezvous() {
let r = EARTH_RADIUS + 400e3;
let phase_diff = PI / 4.0; let num_orbits = 10.0;
let result = Rendezvous::coorbital_rendezvous(r, phase_diff, num_orbits, EARTH_MU).unwrap();
assert_relative_eq!(result.r_orbit, r, epsilon = 1e-6);
assert_relative_eq!(result.initial_phase_difference, phase_diff, epsilon = 1e-6);
assert!(result.phasing.delta_v_total > 0.0);
assert_relative_eq!(
result.phasing.total_phase_change,
phase_diff,
epsilon = 1e-6
);
}
#[test]
fn test_coplanar_rendezvous() {
let r_leo = EARTH_RADIUS + 300e3;
let r_iss = EARTH_RADIUS + 400e3;
let current_phase = PI / 4.0;
let result =
Rendezvous::coplanar_rendezvous(r_leo, r_iss, current_phase, EARTH_MU).unwrap();
assert_relative_eq!(result.r_chaser, r_leo, epsilon = 1e-6);
assert_relative_eq!(result.r_target, r_iss, epsilon = 1e-6);
assert!(result.transfer_time > 0.0);
assert!(result.delta_v_total > 0.0);
assert!(result.wait_time >= 0.0);
assert!(result.a_transfer > r_leo);
assert!(result.a_transfer < r_iss);
}
#[test]
fn test_invalid_inputs() {
let r = EARTH_RADIUS + 400e3;
assert!(Rendezvous::phasing_orbit(r, 0.0, 2.0, EARTH_MU).is_err());
assert!(Rendezvous::phasing_orbit(-r, PI, 2.0, EARTH_MU).is_err());
assert!(Rendezvous::phasing_orbit(r, PI, 0.5, EARTH_MU).is_err());
assert!(Rendezvous::coplanar_rendezvous(r, r, PI, EARTH_MU).is_err());
}
}