use std::f64::consts::PI;
use crate::core::{PoliastroError, PoliastroResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HohmannTransferResult {
pub r_initial: f64,
pub r_final: f64,
pub mu: f64,
pub delta_v1: f64,
pub delta_v2: f64,
pub delta_v_total: f64,
pub transfer_time: f64,
pub transfer_sma: f64,
pub transfer_eccentricity: f64,
pub v_initial: f64,
pub v_final: f64,
pub v_transfer_periapsis: f64,
pub v_transfer_apoapsis: f64,
}
pub struct HohmannTransfer;
impl HohmannTransfer {
pub fn calculate(r_initial: f64, r_final: f64, mu: f64) -> PoliastroResult<HohmannTransferResult> {
if r_initial <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_initial",
r_initial,
"must be positive"
));
}
if r_final <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_final",
r_final,
"must be positive"
));
}
if mu <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"mu",
mu,
"must be positive"
));
}
if (r_initial - r_final).abs() < 1e-6 {
return Err(PoliastroError::invalid_parameter(
"r_initial, r_final",
r_initial,
"radii must be different - no transfer needed"
));
}
let v_initial = (mu / r_initial).sqrt();
let v_final = (mu / r_final).sqrt();
let transfer_sma = (r_initial + r_final) / 2.0;
let r_min = r_initial.min(r_final);
let r_max = r_initial.max(r_final);
let transfer_eccentricity = (r_max - r_min) / (r_max + r_min);
let v_transfer_periapsis = (mu * (2.0 / r_min - 1.0 / transfer_sma)).sqrt();
let v_transfer_apoapsis = (mu * (2.0 / r_max - 1.0 / transfer_sma)).sqrt();
let (delta_v1, delta_v2) = if r_initial < r_final {
let dv1 = v_transfer_periapsis - v_initial;
let dv2 = v_final - v_transfer_apoapsis;
(dv1, dv2)
} else {
let dv1 = v_initial - v_transfer_apoapsis;
let dv2 = v_transfer_periapsis - v_final;
(dv1, dv2)
};
let delta_v_total = delta_v1 + delta_v2;
let transfer_time = PI * (transfer_sma.powi(3) / mu).sqrt();
Ok(HohmannTransferResult {
r_initial,
r_final,
mu,
delta_v1,
delta_v2,
delta_v_total,
transfer_time,
transfer_sma,
transfer_eccentricity,
v_initial,
v_final,
v_transfer_periapsis,
v_transfer_apoapsis,
})
}
pub fn phase_angle(r_initial: f64, r_final: f64, mu: f64) -> PoliastroResult<f64> {
let result = Self::calculate(r_initial, r_final, mu)?;
let n_final = (mu / r_final.powi(3)).sqrt();
let theta_target = n_final * result.transfer_time;
let mut phase_angle = PI - theta_target;
while phase_angle < 0.0 {
phase_angle += 2.0 * PI;
}
while phase_angle >= 2.0 * PI {
phase_angle -= 2.0 * PI;
}
Ok(phase_angle)
}
pub fn synodic_period(r_initial: f64, r_final: f64, mu: f64) -> PoliastroResult<f64> {
if r_initial <= 0.0 || r_final <= 0.0 || mu <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_initial, r_final, mu",
0.0,
"all parameters must be positive"
));
}
let t_initial = 2.0 * PI * (r_initial.powi(3) / mu).sqrt();
let t_final = 2.0 * PI * (r_final.powi(3) / mu).sqrt();
let synodic_period = 1.0 / ((1.0 / t_initial - 1.0 / t_final).abs());
Ok(synodic_period)
}
pub fn time_to_transfer_window(
current_phase: f64,
r_initial: f64,
r_final: f64,
mu: f64,
) -> PoliastroResult<f64> {
let optimal_phase = Self::phase_angle(r_initial, r_final, mu)?;
let synodic = Self::synodic_period(r_initial, r_final, mu)?;
let mut delta_phase = optimal_phase - current_phase;
while delta_phase < 0.0 {
delta_phase += 2.0 * PI;
}
while delta_phase >= 2.0 * PI {
delta_phase -= 2.0 * PI;
}
let wait_time = (delta_phase / (2.0 * PI)) * synodic;
Ok(wait_time)
}
}
#[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_leo_to_geo_transfer() {
let r_leo = EARTH_RADIUS + 400e3;
let r_geo = EARTH_RADIUS + 35_786e3;
let result = HohmannTransfer::calculate(r_leo, r_geo, EARTH_MU).unwrap();
assert!(result.delta_v1 > 0.0, "First burn should be positive");
assert!(result.delta_v2 > 0.0, "Second burn should be positive");
assert_relative_eq!(result.delta_v_total, result.delta_v1 + result.delta_v2, epsilon = 1e-6);
assert_relative_eq!(result.delta_v1, 2427.0, epsilon = 50.0);
assert_relative_eq!(result.delta_v2, 1469.0, epsilon = 50.0);
assert_relative_eq!(result.delta_v_total, 3896.0, epsilon = 100.0);
let expected_time_hours = 5.25;
assert_relative_eq!(
result.transfer_time / 3600.0,
expected_time_hours,
epsilon = 0.1
);
}
#[test]
fn test_geo_to_leo_transfer() {
let r_leo = EARTH_RADIUS + 400e3;
let r_geo = EARTH_RADIUS + 35_786e3;
let result = HohmannTransfer::calculate(r_geo, r_leo, EARTH_MU).unwrap();
assert!(result.delta_v1 > 0.0);
assert!(result.delta_v2 > 0.0);
let ascending = HohmannTransfer::calculate(r_leo, r_geo, EARTH_MU).unwrap();
assert_relative_eq!(result.delta_v_total, ascending.delta_v_total, epsilon = 1e-6);
}
#[test]
fn test_transfer_orbit_properties() {
let r1 = EARTH_RADIUS + 400e3;
let r2 = EARTH_RADIUS + 10_000e3;
let result = HohmannTransfer::calculate(r1, r2, EARTH_MU).unwrap();
assert_relative_eq!(result.transfer_sma, (r1 + r2) / 2.0, epsilon = 1e-6);
assert!(result.transfer_eccentricity >= 0.0);
assert!(result.transfer_eccentricity < 1.0);
let expected_v_peri = (EARTH_MU * (2.0 / r1 - 1.0 / result.transfer_sma)).sqrt();
assert_relative_eq!(result.v_transfer_periapsis, expected_v_peri, epsilon = 1e-6);
}
#[test]
fn test_small_altitude_change() {
let r1 = EARTH_RADIUS + 400e3;
let r2 = EARTH_RADIUS + 500e3;
let result = HohmannTransfer::calculate(r1, r2, EARTH_MU).unwrap();
assert!(result.delta_v_total < 100.0); assert!(result.transfer_time < 3600.0); }
#[test]
fn test_error_handling() {
assert!(HohmannTransfer::calculate(-1.0, 1e7, EARTH_MU).is_err());
assert!(HohmannTransfer::calculate(0.0, 1e7, EARTH_MU).is_err());
assert!(HohmannTransfer::calculate(1e7, 1e7, EARTH_MU).is_err());
assert!(HohmannTransfer::calculate(7e6, 1e7, -1.0).is_err());
}
#[test]
fn test_phase_angle_calculation() {
let r_leo = EARTH_RADIUS + 400e3;
let r_geo = EARTH_RADIUS + 35_786e3;
let phase = HohmannTransfer::phase_angle(r_leo, r_geo, EARTH_MU).unwrap();
assert!(phase >= 0.0);
assert!(phase < 2.0 * PI);
assert_relative_eq!(phase, 1.75, epsilon = 0.01);
}
#[test]
fn test_synodic_period() {
let r1 = EARTH_RADIUS + 400e3;
let r2 = EARTH_RADIUS + 800e3;
let synodic = HohmannTransfer::synodic_period(r1, r2, EARTH_MU).unwrap();
assert!(synodic > 0.0);
let t1 = 2.0 * PI * (r1.powi(3) / EARTH_MU).sqrt();
let t2 = 2.0 * PI * (r2.powi(3) / EARTH_MU).sqrt();
assert!(synodic > t1);
assert!(synodic > t2);
}
#[test]
fn test_time_to_transfer_window() {
let r_leo = EARTH_RADIUS + 400e3;
let r_geo = EARTH_RADIUS + 35_786e3;
let wait_time = HohmannTransfer::time_to_transfer_window(0.0, r_leo, r_geo, EARTH_MU).unwrap();
assert!(wait_time >= 0.0);
let synodic = HohmannTransfer::synodic_period(r_leo, r_geo, EARTH_MU).unwrap();
assert!(wait_time <= synodic);
}
#[test]
fn test_energy_conservation() {
let r1 = EARTH_RADIUS + 400e3;
let r2 = EARTH_RADIUS + 10_000e3;
let result = HohmannTransfer::calculate(r1, r2, EARTH_MU).unwrap();
let e_peri = 0.5 * result.v_transfer_periapsis.powi(2) - EARTH_MU / r1;
let e_apo = 0.5 * result.v_transfer_apoapsis.powi(2) - EARTH_MU / r2;
assert_relative_eq!(e_peri, e_apo, epsilon = 1e-3);
let e_expected = -EARTH_MU / (2.0 * result.transfer_sma);
assert_relative_eq!(e_peri, e_expected, epsilon = 1e-3);
}
#[test]
fn test_earth_mars_transfer() {
const AU: f64 = 1.495978707e11; const SUN_MU: f64 = 1.32712440018e20;
let r_earth = 1.0 * AU;
let r_mars = 1.524 * AU;
let result = HohmannTransfer::calculate(r_earth, r_mars, SUN_MU).unwrap();
let expected_days = 259.0;
let actual_days = result.transfer_time / 86400.0;
assert_relative_eq!(actual_days, expected_days, epsilon = 10.0);
assert_relative_eq!(result.delta_v_total / 1000.0, 5.7, epsilon = 0.5);
}
}