solarsystems 0.0.1

N-body solar system engine — gravitational dynamics, orbital mechanics, perturbations, event detection, and full celestial orchestration
Documentation
use sciforge::hub::domain::common::constants::G;
use std::f64::consts::PI;

use crate::Vec3;

pub struct TransferResult {
    pub delta_v1: f64,
    pub delta_v2: f64,
    pub total_delta_v: f64,
    pub transfer_time: f64,
    pub semi_major_axis: f64,
}

pub fn hohmann_transfer(r1: f64, r2: f64, mu: f64) -> TransferResult {
    let a_t = (r1 + r2) / 2.0;
    let v_circ1 = (mu / r1).sqrt();
    let v_circ2 = (mu / r2).sqrt();
    let v_peri = (mu * (2.0 / r1 - 1.0 / a_t)).sqrt();
    let v_apo = (mu * (2.0 / r2 - 1.0 / a_t)).sqrt();
    let dv1 = (v_peri - v_circ1).abs();
    let dv2 = (v_circ2 - v_apo).abs();
    let tof = PI * (a_t.powi(3) / mu).sqrt();
    TransferResult {
        delta_v1: dv1,
        delta_v2: dv2,
        total_delta_v: dv1 + dv2,
        transfer_time: tof,
        semi_major_axis: a_t,
    }
}

pub struct BiEllipticResult {
    pub delta_v1: f64,
    pub delta_v2: f64,
    pub delta_v3: f64,
    pub total_delta_v: f64,
    pub transfer_time: f64,
}

pub fn bielliptic_transfer(r1: f64, r2: f64, r_intermediate: f64, mu: f64) -> BiEllipticResult {
    let a1 = (r1 + r_intermediate) / 2.0;
    let a2 = (r2 + r_intermediate) / 2.0;

    let v_circ1 = (mu / r1).sqrt();
    let v_circ2 = (mu / r2).sqrt();

    let v1_depart = (mu * (2.0 / r1 - 1.0 / a1)).sqrt();
    let v1_arrive = (mu * (2.0 / r_intermediate - 1.0 / a1)).sqrt();
    let v2_depart = (mu * (2.0 / r_intermediate - 1.0 / a2)).sqrt();
    let v2_arrive = (mu * (2.0 / r2 - 1.0 / a2)).sqrt();

    let dv1 = (v1_depart - v_circ1).abs();
    let dv2 = (v2_depart - v1_arrive).abs();
    let dv3 = (v_circ2 - v2_arrive).abs();

    let tof = PI * (a1.powi(3) / mu).sqrt() + PI * (a2.powi(3) / mu).sqrt();

    BiEllipticResult {
        delta_v1: dv1,
        delta_v2: dv2,
        delta_v3: dv3,
        total_delta_v: dv1 + dv2 + dv3,
        transfer_time: tof,
    }
}

pub fn hohmann_vs_bielliptic_threshold(r1: f64, mu: f64) -> f64 {
    11.94 * r1 * (mu / (mu + 1.0)).powf(0.0)
}

pub fn synodic_period(t1: f64, t2: f64) -> f64 {
    let diff = (1.0 / t1 - 1.0 / t2).abs();
    if diff < 1e-15 {
        f64::INFINITY
    } else {
        1.0 / diff
    }
}

pub fn launch_window_phase_angle(r1: f64, r2: f64, mu: f64) -> f64 {
    let a_t = (r1 + r2) / 2.0;
    let tof = PI * (a_t.powi(3) / mu).sqrt();
    let omega2 = (mu / r2.powi(3)).sqrt();
    PI - omega2 * tof
}

pub fn launch_window_next(phase_current: f64, phase_required: f64, synodic: f64) -> f64 {
    let diff = (phase_required - phase_current) % (2.0 * PI);
    let diff = if diff < 0.0 { diff + 2.0 * PI } else { diff };
    diff / (2.0 * PI) * synodic
}

pub fn characteristic_energy(v_infinity: f64) -> f64 {
    v_infinity * v_infinity
}

pub fn v_infinity_from_c3(c3: f64) -> f64 {
    c3.abs().sqrt()
}

pub fn hyperbolic_excess_velocity(v_departure: f64, v_circular: f64) -> f64 {
    (v_departure * v_departure - v_circular * v_circular)
        .abs()
        .sqrt()
}

pub fn gravity_assist_delta_v(v_infinity_in: f64, flyby_mass: f64, periapsis_radius: f64) -> f64 {
    let mu = G * flyby_mass;
    let deflection = 2.0 * (mu / (periapsis_radius * v_infinity_in * v_infinity_in + mu)).asin();
    2.0 * v_infinity_in * (deflection / 2.0).sin()
}

pub fn gravity_assist_max_deflection(v_infinity: f64, flyby_mass: f64, min_periapsis: f64) -> f64 {
    let mu = G * flyby_mass;
    2.0 * (mu / (min_periapsis * v_infinity * v_infinity + mu)).asin()
}

pub fn oberth_effect_dv(v_periapsis: f64, delta_v_applied: f64) -> f64 {
    let e_before = 0.5 * v_periapsis * v_periapsis;
    let v_after = v_periapsis + delta_v_applied;
    let e_after = 0.5 * v_after * v_after;
    (2.0 * (e_after - e_before)).abs().sqrt()
}

pub fn plane_change_dv(v: f64, angle: f64) -> f64 {
    2.0 * v * (angle / 2.0).sin()
}

pub fn combined_plane_change_dv(v1: f64, v2: f64, angle: f64) -> f64 {
    (v1 * v1 + v2 * v2 - 2.0 * v1 * v2 * angle.cos()).sqrt()
}

pub fn lambert_tof_estimate(r1: Vec3, r2: Vec3, mu: f64, prograde: bool) -> f64 {
    let r1_mag = r1.magnitude();
    let r2_mag = r2.magnitude();
    let cos_dnu = r1.dot(&r2) / (r1_mag * r2_mag);
    let sin_dnu = if prograde {
        r1.cross(&r2).z.signum() * (1.0 - cos_dnu * cos_dnu).sqrt()
    } else {
        -r1.cross(&r2).z.signum() * (1.0 - cos_dnu * cos_dnu).sqrt()
    };
    let dnu = sin_dnu.atan2(cos_dnu);
    let dnu = if dnu < 0.0 { dnu + 2.0 * PI } else { dnu };

    let k = r1_mag * r2_mag * (1.0 - cos_dnu);
    let l = r1_mag + r2_mag;
    let m = r1_mag * r2_mag * (1.0 + cos_dnu);

    let p_min = k / (l + (2.0 * m).sqrt());
    let a = m * k * p_min / (2.0 * m - l * l * p_min);
    let f = 1.0 - r2_mag / p_min * (1.0 - cos_dnu);
    let g = r1_mag * r2_mag * sin_dnu / (mu * p_min).sqrt();

    let tof_parabolic = g.abs();
    let tof_elliptic = PI * (a.abs().powi(3) / mu).sqrt();
    let correction = dnu.sin() * f.abs() * 1e-15;
    tof_parabolic.max(tof_elliptic) + correction
}

pub fn vis_viva(mu: f64, r: f64, a: f64) -> f64 {
    (mu * (2.0 / r - 1.0 / a)).abs().sqrt()
}

pub fn circular_velocity(mu: f64, r: f64) -> f64 {
    (mu / r).sqrt()
}

pub fn escape_velocity(mu: f64, r: f64) -> f64 {
    (2.0 * mu / r).sqrt()
}

pub fn flight_path_angle(r: f64, v: f64, h: f64) -> f64 {
    let cos_fpa = h / (r * v);
    cos_fpa.clamp(-1.0, 1.0).acos()
}

pub fn delta_v_budget(maneuvers: &[f64]) -> f64 {
    maneuvers.iter().map(|dv| dv.abs()).sum()
}

pub fn tsiolkovsky_mass_ratio(delta_v: f64, isp: f64) -> f64 {
    let g0 = 9.80665;
    (delta_v / (isp * g0)).exp()
}

pub fn burn_time(delta_v: f64, thrust: f64, initial_mass: f64) -> f64 {
    initial_mass * delta_v / thrust
}