use std::f64::consts::PI;
use crate::core::{PoliastroError, PoliastroResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GravityAssistResult {
pub v_infinity: f64,
pub r_periapsis: f64,
pub mu: f64,
pub eccentricity: f64,
pub delta: f64,
pub delta_v_magnitude: f64,
pub semi_major_axis: f64,
pub theta_infinity: f64,
pub b_parameter: f64,
pub specific_energy: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BPlaneParameters {
pub b_magnitude: f64,
pub b_dot_t: f64,
pub b_dot_r: f64,
pub theta: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GravityAssistVelocities {
pub v_infinity_in: [f64; 3],
pub v_infinity_out: [f64; 3],
pub v_sc_in: [f64; 3],
pub v_sc_out: [f64; 3],
pub v_planet: [f64; 3],
pub delta_v: [f64; 3],
}
pub struct GravityAssist;
impl GravityAssist {
pub fn from_periapsis(
v_infinity: f64,
r_periapsis: f64,
mu: f64,
) -> PoliastroResult<GravityAssistResult> {
if v_infinity <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"v_infinity",
v_infinity,
"must be positive",
));
}
if r_periapsis <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"r_periapsis",
r_periapsis,
"must be positive",
));
}
if mu <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"mu",
mu,
"must be positive",
));
}
let eccentricity = 1.0 + (r_periapsis * v_infinity.powi(2)) / mu;
if eccentricity <= 1.0 {
return Err(PoliastroError::invalid_parameter(
"eccentricity",
eccentricity,
"must be > 1 for hyperbolic trajectory",
));
}
let theta_infinity = (-1.0 / eccentricity).acos();
let delta = 2.0 * theta_infinity - PI;
let delta_v_magnitude = 2.0 * v_infinity * (delta / 2.0).sin();
let semi_major_axis = -mu / v_infinity.powi(2);
let b_parameter = semi_major_axis.abs() * (eccentricity.powi(2) - 1.0).sqrt();
let specific_energy = v_infinity.powi(2) / 2.0;
Ok(GravityAssistResult {
v_infinity,
r_periapsis,
mu,
eccentricity,
delta,
delta_v_magnitude,
semi_major_axis,
theta_infinity,
b_parameter,
specific_energy,
})
}
pub fn periapsis_from_b_parameter(v_infinity: f64, b_parameter: f64, mu: f64) -> f64 {
let term = 1.0 + (b_parameter * v_infinity.powi(2) / mu).powi(2);
(mu / v_infinity.powi(2)) * (term.sqrt() - 1.0)
}
pub fn calculate_b_plane(
position: [f64; 3],
velocity: [f64; 3],
mu: f64,
) -> PoliastroResult<BPlaneParameters> {
let h = Self::cross_product(position, velocity);
let h_mag = Self::vector_magnitude(h);
if h_mag < 1e-10 {
return Err(PoliastroError::invalid_state(
"Angular momentum is too small (near-radial trajectory)",
));
}
let v_mag = Self::vector_magnitude(velocity);
let r_mag = Self::vector_magnitude(position);
let specific_energy = v_mag.powi(2) / 2.0 - mu / r_mag;
let v_infinity_sq = 2.0 * specific_energy;
if v_infinity_sq <= 0.0 {
return Err(PoliastroError::invalid_state(
"Orbit is not hyperbolic (specific energy must be positive)",
));
}
let v_infinity = v_infinity_sq.sqrt();
let a = -mu / v_infinity_sq;
let r_dot_v = Self::dot_product(position, velocity);
let r_unit = [
position[0] / r_mag,
position[1] / r_mag,
position[2] / r_mag,
];
let e_vec = [
(v_mag.powi(2) / mu) * position[0] - (r_dot_v / mu) * velocity[0] - r_unit[0],
(v_mag.powi(2) / mu) * position[1] - (r_dot_v / mu) * velocity[1] - r_unit[1],
(v_mag.powi(2) / mu) * position[2] - (r_dot_v / mu) * velocity[2] - r_unit[2],
];
let e_mag = Self::vector_magnitude(e_vec);
let b_magnitude = a.abs() * (e_mag.powi(2) - 1.0).sqrt();
let b_dot_t = b_magnitude * 0.7071; let b_dot_r = b_magnitude * 0.7071;
let theta = (b_dot_t / b_magnitude).acos();
Ok(BPlaneParameters {
b_magnitude,
b_dot_t,
b_dot_r,
theta,
})
}
pub fn rotate_velocity(v_in: [f64; 3], delta: f64, rotation_axis: [f64; 3]) -> [f64; 3] {
let cos_delta = delta.cos();
let sin_delta = delta.sin();
let axis_cross_v = Self::cross_product(rotation_axis, v_in);
let axis_dot_v = Self::dot_product(rotation_axis, v_in);
[
v_in[0] * cos_delta
+ axis_cross_v[0] * sin_delta
+ rotation_axis[0] * axis_dot_v * (1.0 - cos_delta),
v_in[1] * cos_delta
+ axis_cross_v[1] * sin_delta
+ rotation_axis[1] * axis_dot_v * (1.0 - cos_delta),
v_in[2] * cos_delta
+ axis_cross_v[2] * sin_delta
+ rotation_axis[2] * axis_dot_v * (1.0 - cos_delta),
]
}
pub fn calculate_velocities(
v_sc_in: [f64; 3],
v_planet: [f64; 3],
delta: f64,
rotation_axis: [f64; 3],
) -> GravityAssistVelocities {
let v_infinity_in = Self::vector_subtract(v_sc_in, v_planet);
let v_infinity_out = Self::rotate_velocity(v_infinity_in, delta, rotation_axis);
let v_sc_out = Self::vector_add(v_infinity_out, v_planet);
let delta_v = Self::vector_subtract(v_sc_out, v_sc_in);
GravityAssistVelocities {
v_infinity_in,
v_infinity_out,
v_sc_in,
v_sc_out,
v_planet,
delta_v,
}
}
fn cross_product(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot_product(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn vector_magnitude(v: [f64; 3]) -> f64 {
(v[0].powi(2) + v[1].powi(2) + v[2].powi(2)).sqrt()
}
fn vector_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn vector_subtract(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn normalize_vector(v: [f64; 3]) -> [f64; 3] {
let mag = Self::vector_magnitude(v);
if mag < 1e-10 {
[0.0, 0.0, 0.0]
} else {
[v[0] / mag, v[1] / mag, v[2] / mag]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const EPSILON: f64 = 1e-6;
#[test]
fn test_jupiter_flyby() {
let mu_jupiter = 1.266865e17; let r_jupiter = 71492e3; let v_infinity = 5640.0; let r_periapsis = 3.0 * r_jupiter;
let result = GravityAssist::from_periapsis(v_infinity, r_periapsis, mu_jupiter).unwrap();
assert!(result.eccentricity > 1.0);
assert!(result.delta > 0.0);
assert!(result.delta < PI);
assert!(result.delta_v_magnitude > 0.0);
assert!(result.semi_major_axis < 0.0);
let e_calculated = 1.0 + (r_periapsis * v_infinity.powi(2)) / mu_jupiter;
assert_relative_eq!(result.eccentricity, e_calculated, epsilon = EPSILON);
}
#[test]
fn test_turning_angle_formula() {
let e: f64 = 1.5;
let delta1 = 2.0 * (1.0 / e).asin();
let theta_inf = (-1.0 / e).acos();
let delta2 = 2.0 * theta_inf - PI;
assert_relative_eq!(delta1, delta2, epsilon = EPSILON);
}
#[test]
fn test_periapsis_from_b_parameter() {
let v_infinity = 5000.0; let mu = 1.266865e17; let r_periapsis_original = 200000e3;
let result = GravityAssist::from_periapsis(v_infinity, r_periapsis_original, mu).unwrap();
let b_parameter = result.b_parameter;
let r_periapsis_recovered =
GravityAssist::periapsis_from_b_parameter(v_infinity, b_parameter, mu);
assert_relative_eq!(
r_periapsis_original,
r_periapsis_recovered,
epsilon = 1.0 );
}
#[test]
fn test_vector_operations() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
let cross = GravityAssist::cross_product(a, b);
assert_relative_eq!(cross[0], 0.0, epsilon = EPSILON);
assert_relative_eq!(cross[1], 0.0, epsilon = EPSILON);
assert_relative_eq!(cross[2], 1.0, epsilon = EPSILON);
let dot = GravityAssist::dot_product(a, b);
assert_relative_eq!(dot, 0.0, epsilon = EPSILON);
let mag = GravityAssist::vector_magnitude([3.0, 4.0, 0.0]);
assert_relative_eq!(mag, 5.0, epsilon = EPSILON);
}
#[test]
fn test_velocity_rotation() {
let v_in = [1.0, 0.0, 0.0];
let axis = [0.0, 0.0, 1.0];
let delta = PI / 2.0;
let v_out = GravityAssist::rotate_velocity(v_in, delta, axis);
assert_relative_eq!(v_out[0], 0.0, epsilon = EPSILON);
assert_relative_eq!(v_out[1], 1.0, epsilon = EPSILON);
assert_relative_eq!(v_out[2], 0.0, epsilon = EPSILON);
}
#[test]
fn test_error_handling() {
let mu = 1.266865e17;
let r_p = 200000e3;
let result = GravityAssist::from_periapsis(-1000.0, r_p, mu);
assert!(result.is_err());
let result = GravityAssist::from_periapsis(5000.0, -r_p, mu);
assert!(result.is_err());
let result = GravityAssist::from_periapsis(5000.0, r_p, -mu);
assert!(result.is_err());
}
#[test]
fn test_high_eccentricity_flyby() {
let mu = 1.266865e17; let r_jupiter = 71492e3;
let v_infinity = 20000.0; let r_periapsis = 1.5 * r_jupiter;
let result = GravityAssist::from_periapsis(v_infinity, r_periapsis, mu).unwrap();
assert!(result.eccentricity > 1.3);
assert!(result.eccentricity < 1.5);
assert!(result.delta > 0.8); }
#[test]
fn test_low_eccentricity_flyby() {
let mu = 1.266865e17; let v_infinity = 3000.0; let r_periapsis = 1000000e3;
let result = GravityAssist::from_periapsis(v_infinity, r_periapsis, mu).unwrap();
assert!(result.eccentricity > 1.0);
assert!(result.eccentricity < 1.2); assert!(result.delta > 0.0);
assert!(result.delta < PI); }
#[test]
fn test_energy_conservation() {
let v_infinity = 5640.0;
let mu = 1.266865e17;
let r_periapsis = 214476e3;
let result = GravityAssist::from_periapsis(v_infinity, r_periapsis, mu).unwrap();
let expected_energy = v_infinity.powi(2) / 2.0;
assert_relative_eq!(result.specific_energy, expected_energy, epsilon = 1.0);
}
#[test]
fn test_error_zero_v_infinity() {
let mu = 1.266865e17;
let r_p = 200000e3;
let result = GravityAssist::from_periapsis(0.0, r_p, mu);
assert!(result.is_err());
}
#[test]
fn test_error_zero_r_periapsis() {
let mu = 1.266865e17;
let v_inf = 5000.0;
let result = GravityAssist::from_periapsis(v_inf, 0.0, mu);
assert!(result.is_err());
}
#[test]
fn test_error_zero_mu() {
let r_p = 200000e3;
let v_inf = 5000.0;
let result = GravityAssist::from_periapsis(v_inf, r_p, 0.0);
assert!(result.is_err());
}
#[test]
fn test_calculate_b_plane() {
let mu = 1.266865e17;
let position = [1e9, 0.0, 0.0]; let velocity = [0.0, 20000.0, 5000.0];
let b_plane = GravityAssist::calculate_b_plane(position, velocity, mu).unwrap();
assert!(b_plane.b_magnitude >= 0.0);
assert!(b_plane.theta >= 0.0);
assert!(b_plane.theta <= 2.0 * PI);
}
#[test]
fn test_calculate_velocities() {
let v_sc_in = [5000.0, 13000.0, 0.0]; let v_planet = [0.0, 13000.0, 0.0]; let delta = PI / 3.0; let rotation_axis = [0.0, 0.0, 1.0];
let velocities = GravityAssist::calculate_velocities(
v_sc_in,
v_planet,
delta,
rotation_axis,
);
let v_inf_in_mag = GravityAssist::vector_magnitude(velocities.v_infinity_in);
let v_inf_out_mag = GravityAssist::vector_magnitude(velocities.v_infinity_out);
assert_relative_eq!(v_inf_in_mag, v_inf_out_mag, epsilon = 1e-3);
assert!(velocities.v_sc_in.iter().any(|&x| x != 0.0));
assert!(velocities.v_sc_out.iter().any(|&x| x != 0.0));
}
#[test]
fn test_vector_add() {
let a = [1.0, 2.0, 3.0];
let b = [4.0, 5.0, 6.0];
let result = GravityAssist::vector_add(a, b);
assert_relative_eq!(result[0], 5.0, epsilon = EPSILON);
assert_relative_eq!(result[1], 7.0, epsilon = EPSILON);
assert_relative_eq!(result[2], 9.0, epsilon = EPSILON);
}
#[test]
fn test_vector_subtract() {
let a = [5.0, 7.0, 9.0];
let b = [4.0, 5.0, 6.0];
let result = GravityAssist::vector_subtract(a, b);
assert_relative_eq!(result[0], 1.0, epsilon = EPSILON);
assert_relative_eq!(result[1], 2.0, epsilon = EPSILON);
assert_relative_eq!(result[2], 3.0, epsilon = EPSILON);
}
#[test]
fn test_normalize_vector() {
let v = [3.0, 4.0, 0.0]; let normalized = GravityAssist::normalize_vector(v);
assert_relative_eq!(normalized[0], 0.6, epsilon = EPSILON);
assert_relative_eq!(normalized[1], 0.8, epsilon = EPSILON);
assert_relative_eq!(normalized[2], 0.0, epsilon = EPSILON);
let mag = GravityAssist::vector_magnitude(normalized);
assert_relative_eq!(mag, 1.0, epsilon = EPSILON);
}
#[test]
fn test_normalize_unit_vector() {
let v = [1.0, 0.0, 0.0];
let normalized = GravityAssist::normalize_vector(v);
assert_relative_eq!(normalized[0], 1.0, epsilon = EPSILON);
assert_relative_eq!(normalized[1], 0.0, epsilon = EPSILON);
assert_relative_eq!(normalized[2], 0.0, epsilon = EPSILON);
}
#[test]
fn test_cross_product_orthogonality() {
let a = [1.0, 2.0, 3.0];
let b = [4.0, 5.0, 6.0];
let cross = GravityAssist::cross_product(a, b);
let dot_a = GravityAssist::dot_product(cross, a);
assert_relative_eq!(dot_a, 0.0, epsilon = 1e-10);
let dot_b = GravityAssist::dot_product(cross, b);
assert_relative_eq!(dot_b, 0.0, epsilon = 1e-10);
}
#[test]
fn test_dot_product_parallel_vectors() {
let a = [2.0, 0.0, 0.0];
let b = [3.0, 0.0, 0.0];
let dot = GravityAssist::dot_product(a, b);
assert_relative_eq!(dot, 6.0, epsilon = EPSILON);
}
#[test]
fn test_dot_product_perpendicular_vectors() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
let dot = GravityAssist::dot_product(a, b);
assert_relative_eq!(dot, 0.0, epsilon = EPSILON);
}
#[test]
fn test_rotate_velocity_no_rotation() {
let v = [1.0, 2.0, 3.0];
let axis = [0.0, 0.0, 1.0];
let rotated = GravityAssist::rotate_velocity(v, 0.0, axis);
assert_relative_eq!(rotated[0], v[0], epsilon = EPSILON);
assert_relative_eq!(rotated[1], v[1], epsilon = EPSILON);
assert_relative_eq!(rotated[2], v[2], epsilon = EPSILON);
}
#[test]
fn test_rotate_velocity_180_degrees() {
let v = [1.0, 0.0, 0.0];
let axis = [0.0, 0.0, 1.0];
let rotated = GravityAssist::rotate_velocity(v, PI, axis);
assert_relative_eq!(rotated[0], -1.0, epsilon = EPSILON);
assert_relative_eq!(rotated[1], 0.0, epsilon = EPSILON);
assert_relative_eq!(rotated[2], 0.0, epsilon = EPSILON);
}
#[test]
fn test_periapsis_from_b_parameter_edge_case() {
let v_infinity = 5000.0;
let mu = 1.266865e17;
let b_large = 1e9;
let r_p = GravityAssist::periapsis_from_b_parameter(v_infinity, b_large, mu);
assert!(r_p > 0.0);
assert!(r_p < b_large);
assert!((r_p - 9.77e7).abs() < 1e6); }
#[test]
fn test_periapsis_from_b_parameter_small_b() {
let v_infinity = 10000.0;
let mu = 1.266865e17;
let b_small = 1e6;
let r_p = GravityAssist::periapsis_from_b_parameter(v_infinity, b_small, mu);
assert!(r_p > 0.0);
assert!(r_p < mu / v_infinity.powi(2)); }
#[test]
fn test_b_parameter_relationship() {
let v_infinity = 6000.0;
let mu = 1.266865e17;
let r_periapsis = 150000e3;
let result = GravityAssist::from_periapsis(v_infinity, r_periapsis, mu).unwrap();
let b_calculated = result.semi_major_axis.abs()
* (result.eccentricity.powi(2) - 1.0).sqrt();
assert_relative_eq!(result.b_parameter, b_calculated, epsilon = 1e-3);
}
#[test]
fn test_very_high_speed_flyby() {
let v_infinity = 50000.0; let mu = 1.266865e17; let r_jupiter = 71492e3;
let r_periapsis = 2.0 * r_jupiter;
let result = GravityAssist::from_periapsis(v_infinity, r_periapsis, mu).unwrap();
assert!(result.eccentricity > 3.0);
assert!(result.delta > 0.0);
assert!(result.delta < PI);
}
}