use astrodyn_quantities::aliases::Position;
use astrodyn_quantities::frame::RootInertial;
use glam::DVec3;
use uom::si::f64::{Length, Ratio};
fn generate_alpha(rho_adj: f64, delta: f64) -> f64 {
let rho2 = rho_adj * rho_adj;
let rho3 = rho2 * rho_adj;
let a3 = 0.758656 * rho2 + -0.0637441 * rho_adj - 1.08955;
let a2 = -1.2205 * rho2 + 0.61815 * rho_adj + 1.62909;
let a1 = 0.43723 * rho2 + -0.52921 * rho_adj + 0.473649;
let a0 = -0.01242 * rho3 + -0.0036715 * rho2 + 0.0150263 * rho_adj + -0.0078259;
a3 * delta * delta * delta + a2 * delta * delta + a1 * delta + a0
}
pub fn compute_shadow_fraction(
vehicle_pos: DVec3,
sun_pos: DVec3,
body_pos: DVec3,
body_radius: f64,
source_radius: f64,
) -> f64 {
let source_to_third_inrtl = body_pos - sun_pos;
let d_source_to_third = source_to_third_inrtl.length();
if d_source_to_third <= 0.0 {
return 1.0;
}
let source_to_third_hat_inrtl = source_to_third_inrtl / d_source_to_third;
let third_to_cg_inrtl = vehicle_pos - body_pos;
let r_par = third_to_cg_inrtl.dot(source_to_third_hat_inrtl);
if r_par < 0.0 {
return 1.0;
}
let r_mag2 = third_to_cg_inrtl.length_squared();
if r_mag2 <= 0.0 {
return 0.0;
}
let r_perp2 = r_mag2 - r_par * r_par;
let r_perp = if r_perp2 <= 0.0 { 0.0 } else { r_perp2.sqrt() };
assert!(
source_radius > 0.0,
"compute_shadow_fraction: source_radius must be positive, got {source_radius}. \
In JEOD, this is validated during RadiationThirdBody::initialize()."
);
assert!(
body_radius > 0.0,
"compute_shadow_fraction: body_radius must be positive, got {body_radius}. \
In JEOD, this is validated during RadiationThirdBody::initialize()."
);
let r_plus = body_radius + source_radius;
let r_minus = body_radius - source_radius;
let r_ratio = body_radius / source_radius;
let r_perp_x_d = r_perp * d_source_to_third;
let radius_x_d = body_radius * d_source_to_third;
if r_perp_x_d >= r_plus * r_par + radius_x_d {
return 1.0;
}
if r_perp_x_d <= r_minus * r_par + radius_x_d {
return 0.0;
}
let d_source_to_cg = (vehicle_pos - sun_pos).length();
let ang_ratio_2_a = r_ratio * d_source_to_cg;
let ang_ratio_2 = ang_ratio_2_a * ang_ratio_2_a / r_mag2;
if r_perp_x_d <= -(r_minus * r_par + radius_x_d) {
return 1.0 - ang_ratio_2;
}
let ang_ratio = ang_ratio_2.sqrt();
let delta_numer = radius_x_d - r_perp_x_d + (body_radius + source_radius) * r_par;
let result = if ang_ratio_2 >= 1.0 {
let delta = delta_numer / (2.0 * source_radius * r_par);
1.0 - generate_alpha(1.0 / ang_ratio, delta)
} else {
let delta = delta_numer / (2.0 * body_radius * (d_source_to_third + r_par));
1.0 - ang_ratio_2 * generate_alpha(ang_ratio, delta)
};
result.clamp(0.0, 1.0)
}
pub fn compute_shadow_fraction_typed(
vehicle_pos: Position<RootInertial>,
sun_pos: Position<RootInertial>,
body_pos: Position<RootInertial>,
body_radius: Length,
source_radius: Length,
) -> Ratio {
let raw = compute_shadow_fraction(
vehicle_pos.raw_si(),
sun_pos.raw_si(),
body_pos.raw_si(),
body_radius.value,
source_radius.value,
);
Ratio::new::<uom::si::ratio::ratio>(raw)
}
#[cfg(test)]
mod tests {
use super::*;
const EARTH_RADIUS: f64 = 6_378_137.0;
const SUN_RADIUS: f64 = 6.98e8;
const AU: f64 = 1.496e11;
#[test]
fn vehicle_in_umbra() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO; let vehicle = DVec3::new(-7_000_000.0, 0.0, 0.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert_eq!(
frac, 0.0,
"Vehicle directly behind Earth should be in full shadow"
);
}
#[test]
fn vehicle_perpendicular_full_sun() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let vehicle = DVec3::new(0.0, 0.0, 7_000_000.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert_eq!(
frac, 1.0,
"Vehicle perpendicular to Sun-Earth line should be in full sun"
);
}
#[test]
fn vehicle_sunside_full_sun() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let vehicle = DVec3::new(7_000_000.0, 0.0, 0.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert_eq!(frac, 1.0, "Vehicle on Sun side should be in full sun");
}
#[test]
fn vehicle_behind_but_offset() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let vehicle = DVec3::new(-10_000.0, 100_000_000.0, 0.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert_eq!(
frac, 1.0,
"Vehicle far from shadow axis should be in full sun"
);
}
#[test]
fn vehicle_in_penumbra() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let vehicle = DVec3::new(-1_000_000.0, EARTH_RADIUS + 1000.0, 0.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert!(
frac > 0.0 && frac < 1.0,
"Vehicle near shadow edge should be in penumbra, got {frac}"
);
}
#[test]
fn shadow_monotonic_with_offset() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let x_behind = -1_000_000.0;
let mut prev_frac = 0.0;
for y_offset in (0..20).map(|i| (i as f64) * 1_000_000.0) {
let vehicle = DVec3::new(x_behind, y_offset, 0.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert!(
frac >= prev_frac,
"Shadow fraction should increase with offset: at y={y_offset}, frac={frac} < prev={prev_frac}"
);
prev_frac = frac;
}
}
#[test]
fn shadow_symmetric() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let x = -500_000.0;
let frac_y = compute_shadow_fraction(
DVec3::new(x, 5_000_000.0, 0.0),
sun,
body,
EARTH_RADIUS,
SUN_RADIUS,
);
let frac_neg_y = compute_shadow_fraction(
DVec3::new(x, -5_000_000.0, 0.0),
sun,
body,
EARTH_RADIUS,
SUN_RADIUS,
);
let frac_z = compute_shadow_fraction(
DVec3::new(x, 0.0, 5_000_000.0),
sun,
body,
EARTH_RADIUS,
SUN_RADIUS,
);
assert!(
(frac_y - frac_neg_y).abs() < 1e-12,
"Shadow should be symmetric in +/-Y"
);
assert!(
(frac_y - frac_z).abs() < 1e-12,
"Shadow should be symmetric between Y and Z offsets"
);
}
#[test]
fn generate_alpha_range() {
let alpha_start = generate_alpha(1.0, 0.0);
assert!(
alpha_start.abs() < 0.05,
"Alpha at first contact should be near 0, got {alpha_start}"
);
let alpha_end = generate_alpha(1.0, 1.0);
assert!(
(alpha_end - 1.0).abs() < 0.05,
"Alpha at totality should be near 1, got {alpha_end}"
);
}
#[test]
fn annular_eclipse_region_d() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let vehicle = DVec3::new(-2.0e9, 0.0, 0.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert!(
frac > 0.0 && frac < 1.0,
"Vehicle in antumbra should have partial illumination, got {frac}"
);
}
#[test]
fn vehicle_between_source_and_body() {
let sun = DVec3::new(AU, 0.0, 0.0);
let body = DVec3::ZERO;
let vehicle = DVec3::new(AU / 2.0, 0.0, 0.0);
let frac = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
assert_eq!(
frac, 1.0,
"Vehicle between source and body should be full sun"
);
}
#[test]
fn compute_shadow_fraction_typed_matches_untyped() {
use uom::si::length::meter;
let sun = DVec3::new(-AU, 0.0, 0.0);
let body = DVec3::ZERO;
let vehicle = DVec3::new(7_000_000.0, 0.0, 0.0);
let untyped = compute_shadow_fraction(vehicle, sun, body, EARTH_RADIUS, SUN_RADIUS);
let typed = compute_shadow_fraction_typed(
Position::<RootInertial>::from_raw_si(vehicle),
Position::<RootInertial>::from_raw_si(sun),
Position::<RootInertial>::from_raw_si(body),
Length::new::<meter>(EARTH_RADIUS),
Length::new::<meter>(SUN_RADIUS),
);
assert_eq!(typed.value, untyped);
let vehicle_sunny = DVec3::new(AU / 2.0, 0.0, 0.0);
let untyped_sunny =
compute_shadow_fraction(vehicle_sunny, sun, body, EARTH_RADIUS, SUN_RADIUS);
let typed_sunny = compute_shadow_fraction_typed(
Position::<RootInertial>::from_raw_si(vehicle_sunny),
Position::<RootInertial>::from_raw_si(sun),
Position::<RootInertial>::from_raw_si(body),
Length::new::<meter>(EARTH_RADIUS),
Length::new::<meter>(SUN_RADIUS),
);
assert_eq!(typed_sunny.value, untyped_sunny);
}
}