use std::f64::consts::PI;
use crate::core::{PoliastroError, PoliastroResult};
use crate::maneuvers::hohmann::HohmannTransfer;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BiellipticTransferResult {
pub r_initial: f64,
pub r_final: f64,
pub r_intermediate: f64,
pub mu: f64,
pub delta_v1: f64,
pub delta_v2: f64,
pub delta_v3: f64,
pub delta_v_total: f64,
pub transfer_time: f64,
pub transfer1_sma: f64,
pub transfer2_sma: f64,
pub transfer1_eccentricity: f64,
pub transfer2_eccentricity: f64,
pub v_initial: f64,
pub v_final: f64,
pub v_transfer1_periapsis: f64,
pub v_transfer1_apoapsis: f64,
pub v_transfer2_apoapsis: f64,
pub v_transfer2_periapsis: f64,
}
pub struct BiellipticTransfer;
impl BiellipticTransfer {
pub fn calculate(
r_initial: f64,
r_final: f64,
r_intermediate: f64,
mu: f64,
) -> PoliastroResult<BiellipticTransferResult> {
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 r_intermediate <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_intermediate",
r_intermediate,
"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 r_max_orbit = r_initial.max(r_final);
if r_intermediate <= r_max_orbit {
return Err(PoliastroError::invalid_parameter(
"r_intermediate",
r_intermediate,
format!(
"must be larger than both initial and final radii (max = {r_max_orbit:.1} m)"
),
));
}
let v_initial = (mu / r_initial).sqrt();
let v_final = (mu / r_final).sqrt();
let transfer1_sma = (r_initial + r_intermediate) / 2.0;
let transfer2_sma = (r_final + r_intermediate) / 2.0;
let transfer1_eccentricity =
(r_intermediate - r_initial) / (r_intermediate + r_initial);
let transfer2_eccentricity = (r_intermediate - r_final) / (r_intermediate + r_final);
let v_transfer1_periapsis = (mu * (2.0 / r_initial - 1.0 / transfer1_sma)).sqrt();
let v_transfer1_apoapsis = (mu * (2.0 / r_intermediate - 1.0 / transfer1_sma)).sqrt();
let v_transfer2_apoapsis = (mu * (2.0 / r_intermediate - 1.0 / transfer2_sma)).sqrt();
let v_transfer2_periapsis = (mu * (2.0 / r_final - 1.0 / transfer2_sma)).sqrt();
let delta_v1 = (v_transfer1_periapsis - v_initial).abs();
let delta_v2 = (v_transfer2_apoapsis - v_transfer1_apoapsis).abs();
let delta_v3 = (v_final - v_transfer2_periapsis).abs();
let delta_v_total = delta_v1 + delta_v2 + delta_v3;
let transfer_time =
PI * (transfer1_sma.powi(3) / mu).sqrt() + PI * (transfer2_sma.powi(3) / mu).sqrt();
Ok(BiellipticTransferResult {
r_initial,
r_final,
r_intermediate,
mu,
delta_v1,
delta_v2,
delta_v3,
delta_v_total,
transfer_time,
transfer1_sma,
transfer2_sma,
transfer1_eccentricity,
transfer2_eccentricity,
v_initial,
v_final,
v_transfer1_periapsis,
v_transfer1_apoapsis,
v_transfer2_apoapsis,
v_transfer2_periapsis,
})
}
pub fn find_optimal_intermediate(
r_initial: f64,
r_final: f64,
mu: f64,
search_limit_factor: f64,
) -> PoliastroResult<(f64, BiellipticTransferResult)> {
let r_max = r_initial.max(r_final);
let r_min_intermediate = r_max * 1.01; let r_max_intermediate = r_max * search_limit_factor;
let n_samples = 100;
let mut best_dv = f64::INFINITY;
let mut best_r_intermediate = r_min_intermediate;
let mut best_result: Option<BiellipticTransferResult> = None;
for i in 0..n_samples {
let log_min = r_min_intermediate.ln();
let log_max = r_max_intermediate.ln();
let log_r = log_min + (log_max - log_min) * (i as f64) / ((n_samples - 1) as f64);
let r_intermediate = log_r.exp();
if let Ok(result) = Self::calculate(r_initial, r_final, r_intermediate, mu) {
if result.delta_v_total < best_dv {
best_dv = result.delta_v_total;
best_r_intermediate = r_intermediate;
best_result = Some(result);
}
}
}
match best_result {
Some(result) => Ok((best_r_intermediate, result)),
None => Err(PoliastroError::invalid_state(
"Could not find valid bi-elliptic transfer in search range",
)),
}
}
pub fn compare_with_hohmann(
r_initial: f64,
r_final: f64,
r_intermediate: f64,
mu: f64,
) -> PoliastroResult<(BiellipticTransferResult, crate::maneuvers::hohmann::HohmannTransferResult, f64, f64)> {
let bielliptic = Self::calculate(r_initial, r_final, r_intermediate, mu)?;
let hohmann = HohmannTransfer::calculate(r_initial, r_final, mu)?;
let dv_savings = hohmann.delta_v_total - bielliptic.delta_v_total;
let time_penalty = bielliptic.transfer_time / hohmann.transfer_time;
Ok((bielliptic, hohmann, dv_savings, time_penalty))
}
pub fn is_more_efficient_than_hohmann(
r_initial: f64,
r_final: f64,
r_intermediate: f64,
mu: f64,
) -> PoliastroResult<bool> {
let (_, _, dv_savings, _) = Self::compare_with_hohmann(r_initial, r_final, r_intermediate, mu)?;
Ok(dv_savings > 0.0)
}
pub fn critical_radius_ratio() -> f64 {
15.58
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const EARTH_MU: f64 = 3.986004418e14; const EARTH_RADIUS: f64 = 6.3781e6;
#[test]
fn test_bielliptic_calculation_basic() {
let r_initial = EARTH_RADIUS + 400e3; let r_final = EARTH_RADIUS + 35_786e3; let r_intermediate = r_final * 3.0;
let result = BiellipticTransfer::calculate(r_initial, r_final, r_intermediate, EARTH_MU);
assert!(result.is_ok());
let transfer = result.unwrap();
assert!(transfer.delta_v1 > 0.0);
assert!(transfer.delta_v2 > 0.0);
assert!(transfer.delta_v3 > 0.0);
assert_relative_eq!(
transfer.delta_v_total,
transfer.delta_v1 + transfer.delta_v2 + transfer.delta_v3,
epsilon = 1e-6
);
}
#[test]
fn test_bielliptic_transfer_time_positive() {
let r_initial = EARTH_RADIUS + 400e3;
let r_final = EARTH_RADIUS + 35_786e3;
let r_intermediate = r_final * 5.0;
let result = BiellipticTransfer::calculate(r_initial, r_final, r_intermediate, EARTH_MU).unwrap();
assert!(result.transfer_time > 0.0);
let hohmann = HohmannTransfer::calculate(r_initial, r_final, EARTH_MU).unwrap();
assert!(result.transfer_time > hohmann.transfer_time);
}
#[test]
fn test_bielliptic_intermediate_validation() {
let r_initial = EARTH_RADIUS + 400e3;
let r_final = EARTH_RADIUS + 35_786e3;
let result = BiellipticTransfer::calculate(r_initial, r_final, r_final * 0.5, EARTH_MU);
assert!(result.is_err());
let result = BiellipticTransfer::calculate(r_initial, r_final, r_final, EARTH_MU);
assert!(result.is_err());
}
#[test]
fn test_bielliptic_vs_hohmann_large_ratio() {
let r_initial = EARTH_RADIUS + 400e3; let r_final = r_initial * 20.0; let r_intermediate = r_final * 5.0;
let (bielliptic, hohmann, dv_savings, time_penalty) =
BiellipticTransfer::compare_with_hohmann(r_initial, r_final, r_intermediate, EARTH_MU)
.unwrap();
assert!(dv_savings > 0.0);
assert!(time_penalty > 1.0);
println!("Bi-elliptic ΔV: {:.1} m/s", bielliptic.delta_v_total);
println!("Hohmann ΔV: {:.1} m/s", hohmann.delta_v_total);
println!("Savings: {:.1} m/s ({:.2}%)", dv_savings, 100.0 * dv_savings / hohmann.delta_v_total);
println!("Time penalty: {:.2}x", time_penalty);
}
#[test]
fn test_critical_radius_ratio() {
let ratio = BiellipticTransfer::critical_radius_ratio();
assert_relative_eq!(ratio, 15.58, epsilon = 0.01);
}
#[test]
fn test_energy_conservation() {
let r_initial = EARTH_RADIUS + 400e3;
let r_final = EARTH_RADIUS + 35_786e3;
let r_intermediate = r_final * 3.0;
let result = BiellipticTransfer::calculate(r_initial, r_final, r_intermediate, EARTH_MU).unwrap();
let epsilon_initial = -EARTH_MU / (2.0 * r_initial);
assert_relative_eq!(
result.v_initial.powi(2) / 2.0 - EARTH_MU / r_initial,
epsilon_initial,
epsilon = 1.0
);
}
#[test]
fn test_find_optimal_intermediate() {
let r_initial = EARTH_RADIUS + 400e3;
let r_final = r_initial * 20.0;
let result = BiellipticTransfer::find_optimal_intermediate(r_initial, r_final, EARTH_MU, 50.0);
assert!(result.is_ok());
let (r_opt, transfer) = result.unwrap();
assert!(r_opt > r_final);
assert!(transfer.delta_v_total > 0.0);
}
#[test]
fn test_parameter_validation() {
let r1 = EARTH_RADIUS + 400e3;
let r2 = EARTH_RADIUS + 35_786e3;
let rb = r2 * 2.0;
assert!(BiellipticTransfer::calculate(-r1, r2, rb, EARTH_MU).is_err());
assert!(BiellipticTransfer::calculate(r1, -r2, rb, EARTH_MU).is_err());
assert!(BiellipticTransfer::calculate(r1, r2, -rb, EARTH_MU).is_err());
assert!(BiellipticTransfer::calculate(r1, r2, rb, -EARTH_MU).is_err());
assert!(BiellipticTransfer::calculate(r1, r1, rb, EARTH_MU).is_err());
}
}