use std::f64::consts::PI;
use crate::core::{PoliastroError, PoliastroResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PlaneChangeResult {
pub velocity: f64,
pub delta_angle: f64,
pub delta_v: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CombinedPlaneChangeResult {
pub v_initial: f64,
pub v_final: f64,
pub delta_angle: f64,
pub delta_v: f64,
pub delta_v_orbit_only: f64,
pub delta_v_plane_only: f64,
pub delta_v_penalty: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OptimalPlaneChangeResult {
pub total_angle: f64,
pub angle_at_first: f64,
pub angle_at_second: f64,
pub v_first: f64,
pub v_second: f64,
pub delta_v_total: f64,
pub delta_v_first: f64,
pub delta_v_second: f64,
pub delta_v_saved: f64,
pub delta_v_saved_vs_low: f64,
}
pub struct PlaneChange;
impl PlaneChange {
pub fn pure_plane_change(velocity: f64, delta_angle: f64) -> PoliastroResult<PlaneChangeResult> {
if velocity <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"velocity",
velocity,
"must be positive",
));
}
if !(0.0..=PI).contains(&delta_angle) {
return Err(PoliastroError::out_of_range(
"delta_angle",
delta_angle,
0.0,
PI,
));
}
let delta_v = 2.0 * velocity * (delta_angle / 2.0).sin();
Ok(PlaneChangeResult {
velocity,
delta_angle,
delta_v,
})
}
pub fn combined_plane_change(
v_initial: f64,
v_final: f64,
delta_angle: f64,
) -> PoliastroResult<CombinedPlaneChangeResult> {
if v_initial <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"v_initial",
v_initial,
"must be positive",
));
}
if v_final <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"v_final",
v_final,
"must be positive",
));
}
if !(0.0..=PI).contains(&delta_angle) {
return Err(PoliastroError::out_of_range(
"delta_angle",
delta_angle,
0.0,
PI,
));
}
let delta_v = (v_initial.powi(2) + v_final.powi(2)
- 2.0 * v_initial * v_final * delta_angle.cos())
.sqrt();
let delta_v_orbit_only = (v_final - v_initial).abs();
let v_avg = (v_initial + v_final) / 2.0;
let delta_v_plane_only = 2.0 * v_avg * (delta_angle / 2.0).sin();
let delta_v_penalty = delta_v - delta_v_orbit_only;
Ok(CombinedPlaneChangeResult {
v_initial,
v_final,
delta_angle,
delta_v,
delta_v_orbit_only,
delta_v_plane_only,
delta_v_penalty,
})
}
pub fn optimal_plane_change_location(
v_low: f64,
v_high: f64,
v_transfer_low: f64,
v_transfer_high: f64,
total_angle: f64,
) -> PoliastroResult<OptimalPlaneChangeResult> {
if v_low <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"v_low",
v_low,
"must be positive",
));
}
if v_high <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"v_high",
v_high,
"must be positive",
));
}
if v_transfer_low <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"v_transfer_low",
v_transfer_low,
"must be positive",
));
}
if v_transfer_high <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"v_transfer_high",
v_transfer_high,
"must be positive",
));
}
if !(0.0..=PI).contains(&total_angle) {
return Err(PoliastroError::out_of_range(
"total_angle",
total_angle,
0.0,
PI,
));
}
let dv_low_all = Self::combined_plane_change(v_low, v_transfer_low, total_angle)?;
let dv_high_coplanar =
Self::combined_plane_change(v_transfer_high, v_high, 0.0)?;
let delta_v_all_low = dv_low_all.delta_v + dv_high_coplanar.delta_v;
let dv_low_coplanar = Self::combined_plane_change(v_low, v_transfer_low, 0.0)?;
let dv_high_all =
Self::combined_plane_change(v_transfer_high, v_high, total_angle)?;
let delta_v_all_high = dv_low_coplanar.delta_v + dv_high_all.delta_v;
let n_points = 100;
let mut best_angle_low = 0.0;
let mut best_delta_v = delta_v_all_high;
for i in 0..=n_points {
let angle_low = (i as f64 / n_points as f64) * total_angle;
let angle_high = total_angle - angle_low;
let dv_low = Self::combined_plane_change(v_low, v_transfer_low, angle_low)?;
let dv_high =
Self::combined_plane_change(v_transfer_high, v_high, angle_high)?;
let total_dv = dv_low.delta_v + dv_high.delta_v;
if total_dv < best_delta_v {
best_delta_v = total_dv;
best_angle_low = angle_low;
}
}
let best_angle_high = total_angle - best_angle_low;
let dv_first = Self::combined_plane_change(v_low, v_transfer_low, best_angle_low)?;
let dv_second =
Self::combined_plane_change(v_transfer_high, v_high, best_angle_high)?;
Ok(OptimalPlaneChangeResult {
total_angle,
angle_at_first: best_angle_low,
angle_at_second: best_angle_high,
v_first: v_low,
v_second: v_high,
delta_v_total: best_delta_v,
delta_v_first: dv_first.delta_v,
delta_v_second: dv_second.delta_v,
delta_v_saved: delta_v_all_high - best_delta_v,
delta_v_saved_vs_low: delta_v_all_low - best_delta_v,
})
}
pub fn plane_change_penalty(velocity: f64, delta_angle: f64) -> PoliastroResult<f64> {
if velocity <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"velocity",
velocity,
"must be positive",
));
}
if !(0.0..=PI).contains(&delta_angle) {
return Err(PoliastroError::out_of_range(
"delta_angle",
delta_angle,
0.0,
PI,
));
}
let penalty = 2.0 * velocity * (delta_angle / 2.0).sin();
Ok(penalty)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const MU_EARTH: f64 = 3.986004418e14;
#[test]
fn test_pure_plane_change_basic() {
let v = 7800.0; let angle = 5.0_f64.to_radians();
let result = PlaneChange::pure_plane_change(v, angle).unwrap();
assert_relative_eq!(result.delta_v, 680.0, epsilon = 10.0);
assert_eq!(result.velocity, v);
assert_eq!(result.delta_angle, angle);
}
#[test]
fn test_pure_plane_change_45_degrees() {
let v = 7800.0;
let angle = 45.0_f64.to_radians();
let result = PlaneChange::pure_plane_change(v, angle).unwrap();
assert_relative_eq!(result.delta_v, 5969.0, epsilon = 10.0);
}
#[test]
fn test_pure_plane_change_180_degrees() {
let v = 7800.0;
let angle = PI;
let result = PlaneChange::pure_plane_change(v, angle).unwrap();
assert_relative_eq!(result.delta_v, 2.0 * v, epsilon = 0.1);
}
#[test]
fn test_pure_plane_change_zero() {
let v = 7800.0;
let angle = 0.0;
let result = PlaneChange::pure_plane_change(v, angle).unwrap();
assert_relative_eq!(result.delta_v, 0.0, epsilon = 1e-6);
}
#[test]
fn test_combined_plane_change_hohmann_leo_to_geo() {
let r_leo = 6378.137e3 + 400e3; let r_geo = 42164e3;
let v_leo = (MU_EARTH / r_leo).sqrt();
let a_transfer = (r_leo + r_geo) / 2.0;
let v_transfer_leo = ((2.0 * MU_EARTH / r_leo) - (MU_EARTH / a_transfer)).sqrt();
let angle = 28.5_f64.to_radians();
let result =
PlaneChange::combined_plane_change(v_leo, v_transfer_leo, angle).unwrap();
assert!(result.delta_v > result.delta_v_orbit_only);
assert!(result.delta_v_penalty > 0.0);
assert!(result.delta_v_penalty > 1000.0); }
#[test]
fn test_combined_plane_change_coplanar() {
let v1 = 7800.0;
let v2 = 10000.0;
let angle = 0.0;
let result = PlaneChange::combined_plane_change(v1, v2, angle).unwrap();
assert_relative_eq!(result.delta_v, (v2 - v1).abs(), epsilon = 0.1);
assert_relative_eq!(result.delta_v_penalty, 0.0, epsilon = 1.0);
}
#[test]
fn test_combined_plane_change_90_degrees() {
let v1 = 7800.0;
let v2 = 7800.0; let angle = PI / 2.0;
let result = PlaneChange::combined_plane_change(v1, v2, angle).unwrap();
let expected = v1 * 2.0_f64.sqrt();
assert_relative_eq!(result.delta_v, expected, epsilon = 0.1);
}
#[test]
fn test_optimal_plane_change_location_leo_to_geo() {
let r_leo = 6378.137e3 + 400e3;
let r_geo = 42164e3;
let v_leo = (MU_EARTH / r_leo).sqrt();
let v_geo = (MU_EARTH / r_geo).sqrt();
let a_transfer = (r_leo + r_geo) / 2.0;
let v_transfer_leo = ((2.0 * MU_EARTH / r_leo) - (MU_EARTH / a_transfer)).sqrt();
let v_transfer_geo = ((2.0 * MU_EARTH / r_geo) - (MU_EARTH / a_transfer)).sqrt();
let angle = 28.5_f64.to_radians();
let result = PlaneChange::optimal_plane_change_location(
v_leo,
v_geo,
v_transfer_leo,
v_transfer_geo,
angle,
)
.unwrap();
assert!(result.angle_at_second > result.angle_at_first);
assert!(result.delta_v_saved > 0.0);
assert!(result.delta_v_saved_vs_low > result.delta_v_saved);
assert_relative_eq!(
result.delta_v_total,
result.delta_v_first + result.delta_v_second,
epsilon = 0.1
);
}
#[test]
fn test_optimal_plane_change_small_angle() {
let r_leo = 6378.137e3 + 400e3;
let r_geo = 42164e3;
let v_leo = (MU_EARTH / r_leo).sqrt();
let v_geo = (MU_EARTH / r_geo).sqrt();
let a_transfer = (r_leo + r_geo) / 2.0;
let v_transfer_leo = ((2.0 * MU_EARTH / r_leo) - (MU_EARTH / a_transfer)).sqrt();
let v_transfer_geo = ((2.0 * MU_EARTH / r_geo) - (MU_EARTH / a_transfer)).sqrt();
let angle = 5.0_f64.to_radians();
let result = PlaneChange::optimal_plane_change_location(
v_leo,
v_geo,
v_transfer_leo,
v_transfer_geo,
angle,
)
.unwrap();
assert!(result.angle_at_first > 0.0);
assert!(result.delta_v_saved >= 0.0);
}
#[test]
fn test_plane_change_penalty() {
let v = 7800.0;
let angle = 10.0_f64.to_radians();
let penalty = PlaneChange::plane_change_penalty(v, angle).unwrap();
let pure = PlaneChange::pure_plane_change(v, angle).unwrap();
assert_relative_eq!(penalty, pure.delta_v, epsilon = 0.1);
}
#[test]
fn test_error_negative_velocity() {
let result = PlaneChange::pure_plane_change(-100.0, 0.1);
assert!(result.is_err());
}
#[test]
fn test_error_invalid_angle() {
let result = PlaneChange::pure_plane_change(7800.0, 4.0); assert!(result.is_err());
}
#[test]
fn test_error_negative_angle() {
let result = PlaneChange::pure_plane_change(7800.0, -0.1);
assert!(result.is_err());
}
}