use arika::eclipse::{self, ShadowModel};
use nalgebra::Vector3;
const ZERO: Vector3<f64> = Vector3::new(0.0, 0.0, 0.0);
struct GeometryCase {
observer: Vector3<f64>,
light_pos: Vector3<f64>,
occulter_pos: Vector3<f64>,
light_radius: f64,
occulter_radius: f64,
}
impl GeometryCase {
fn from_angles(a: f64, b: f64, c: f64, d_light: f64, d_occulter: f64) -> Self {
Self {
observer: ZERO,
light_pos: Vector3::new(d_light, 0.0, 0.0),
occulter_pos: Vector3::new(d_occulter * c.cos(), d_occulter * c.sin(), 0.0),
light_radius: d_light * a.sin(),
occulter_radius: d_occulter * b.sin(),
}
}
fn illumination(&self, model: ShadowModel) -> f64 {
eclipse::illumination(
&self.observer,
&self.light_pos,
&self.occulter_pos,
self.light_radius,
self.occulter_radius,
model,
)
}
fn illumination_conical(&self) -> f64 {
self.illumination(ShadowModel::Conical)
}
#[allow(dead_code)]
fn illumination_cylindrical(&self) -> f64 {
self.illumination(ShadowModel::Cylindrical)
}
}
fn assert_close(actual: f64, expected: f64, tol: f64, msg: &str) {
let err = (actual - expected).abs();
assert!(
err < tol,
"{msg}: expected {expected:.12e}, got {actual:.12e}, err={err:.3e} (tol={tol:.0e})"
);
}
const D_LIGHT: f64 = 1e6; const D_OCCULTER: f64 = 1e4;
#[test]
fn conical_full_sun_wide_separation() {
let a = 0.01_f64; let b = 0.02;
let c = 0.5; let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
1.0,
1e-12,
"full sun wide separation",
);
}
#[test]
fn conical_full_sun_barely_separated() {
let a = 0.01_f64;
let b = 0.02;
let c = a + b + 1e-8;
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
1.0,
1e-6,
"full sun barely separated",
);
}
#[test]
fn conical_total_eclipse_centered() {
let a = 0.005_f64; let b = 0.02; let c = 0.0;
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
0.0,
1e-12,
"total eclipse centered",
);
}
#[test]
fn conical_total_eclipse_offset() {
let a = 0.005_f64;
let b = 0.02;
let c = 0.01; let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
0.0,
1e-12,
"total eclipse offset",
);
}
#[test]
fn conical_total_eclipse_boundary() {
let a = 0.005_f64;
let b = 0.02;
let c = b - a; let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
0.0,
1e-7,
"total eclipse boundary",
);
}
#[test]
fn conical_annular_centered() {
let a = 0.02_f64; let b = 0.01; let c = 0.0;
let expected = 1.0 - (b / a).powi(2); let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
expected,
1e-10,
"annular centered",
);
}
#[test]
fn conical_annular_offset() {
let a = 0.02_f64;
let b = 0.005;
let c = 0.005; let expected = 1.0 - (b / a).powi(2); let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
expected,
1e-10,
"annular offset",
);
}
#[test]
fn conical_annular_boundary() {
let a = 0.02_f64;
let b = 0.01;
let c = a - b; let expected = 1.0 - (b / a).powi(2); let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
expected,
1e-9,
"annular boundary",
);
}
#[test]
fn conical_partial_half_overlap_symmetric() {
let a = 0.01_f64;
let b = a;
let c = a;
let expected = 1.0 / 3.0 + 3.0_f64.sqrt() / (2.0 * std::f64::consts::PI);
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
expected,
1e-6,
"partial half overlap symmetric",
);
}
#[test]
fn conical_partial_in_between() {
let a = 0.01_f64;
let b = 0.015;
let c = 0.02; let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
let illum = case.illumination_conical();
assert!(
illum > 0.0 && illum < 1.0,
"partial eclipse should be in (0,1), got {illum}"
);
}
#[test]
fn conical_tangent_outer() {
let a = 0.01_f64;
let b = 0.02;
let c = a + b;
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(case.illumination_conical(), 1.0, 1e-6, "tangent outer");
}
#[test]
fn conical_tangent_inner_umbra() {
let a = 0.005_f64;
let b = 0.02;
let c = b - a;
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
0.0,
1e-6,
"tangent inner umbra",
);
}
#[test]
fn conical_tangent_inner_annular() {
let a = 0.02_f64;
let b = 0.005;
let c = a - b;
let expected = 1.0 - (b / a).powi(2);
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
assert_close(
case.illumination_conical(),
expected,
1e-6,
"tangent inner annular",
);
}
#[test]
fn conical_occulter_behind_observer() {
let case = GeometryCase {
observer: ZERO,
light_pos: Vector3::new(1e6, 0.0, 0.0),
occulter_pos: Vector3::new(-1e4, 0.0, 0.0), light_radius: 695700.0,
occulter_radius: 6371.0,
};
assert_close(
case.illumination_conical(),
1.0,
1e-12,
"occulter behind observer",
);
}
#[test]
fn conical_occulter_beside_observer() {
let case = GeometryCase {
observer: ZERO,
light_pos: Vector3::new(1e6, 0.0, 0.0),
occulter_pos: Vector3::new(0.0, 1e4, 0.0), light_radius: 695700.0,
occulter_radius: 6371.0,
};
assert_close(
case.illumination_conical(),
1.0,
1e-12,
"occulter beside observer",
);
}
#[test]
fn conical_monotonicity_umbra_to_sun() {
let a = 0.005_f64;
let b = 0.02;
let n = 100;
let c_max = a + b + 0.001; let mut prev = -1.0_f64;
for i in 0..=n {
let c = c_max * (i as f64) / (n as f64);
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
let illum = case.illumination_conical();
assert!(
illum >= prev - 1e-10,
"monotonicity violated at c={c:.6}: prev={prev:.10}, current={illum:.10}"
);
prev = illum;
}
}
#[test]
fn conical_monotonicity_annular_to_sun() {
let a = 0.02_f64;
let b = 0.005;
let n = 100;
let c_max = a + b + 0.001;
let mut prev = -1.0_f64;
for i in 0..=n {
let c = c_max * (i as f64) / (n as f64);
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
let illum = case.illumination_conical();
assert!(
illum >= prev - 1e-10,
"monotonicity violated at c={c:.6}: prev={prev:.10}, current={illum:.10}"
);
prev = illum;
}
}
#[test]
fn conical_continuity_at_outer_tangent() {
let a = 0.01_f64;
let b = 0.02;
let eps = 1e-8;
let c_boundary = a + b;
let case_inside = GeometryCase::from_angles(a, b, c_boundary - eps, D_LIGHT, D_OCCULTER);
let case_outside = GeometryCase::from_angles(a, b, c_boundary + eps, D_LIGHT, D_OCCULTER);
let illum_inside = case_inside.illumination_conical();
let illum_outside = case_outside.illumination_conical();
let diff = (illum_inside - illum_outside).abs();
assert!(
diff < 1e-4,
"discontinuity at outer tangent: inside={illum_inside:.10}, outside={illum_outside:.10}, diff={diff:.3e}"
);
}
#[test]
fn conical_continuity_at_inner_tangent_umbra() {
let a = 0.005_f64;
let b = 0.02;
let eps = 1e-8;
let c_boundary = b - a;
let case_inside = GeometryCase::from_angles(a, b, c_boundary - eps, D_LIGHT, D_OCCULTER);
let case_outside = GeometryCase::from_angles(a, b, c_boundary + eps, D_LIGHT, D_OCCULTER);
let illum_inside = case_inside.illumination_conical();
let illum_outside = case_outside.illumination_conical();
let diff = (illum_inside - illum_outside).abs();
assert!(
diff < 1e-4,
"discontinuity at inner tangent (umbra): inside={illum_inside:.10}, outside={illum_outside:.10}, diff={diff:.3e}"
);
}
#[test]
fn conical_continuity_at_inner_tangent_annular() {
let a = 0.02_f64;
let b = 0.005;
let eps = 1e-8;
let c_boundary = a - b;
let case_inside = GeometryCase::from_angles(a, b, c_boundary - eps, D_LIGHT, D_OCCULTER);
let case_outside = GeometryCase::from_angles(a, b, c_boundary + eps, D_LIGHT, D_OCCULTER);
let illum_inside = case_inside.illumination_conical();
let illum_outside = case_outside.illumination_conical();
let diff = (illum_inside - illum_outside).abs();
assert!(
diff < 1e-4,
"discontinuity at inner tangent (annular): inside={illum_inside:.10}, outside={illum_outside:.10}, diff={diff:.3e}"
);
}
#[test]
fn conical_leo_full_shadow() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let earth_pos = ZERO;
let sat_pos = Vector3::new(-(6371.0 + 400.0), 0.0, 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&earth_pos,
695700.0,
6371.0,
ShadowModel::Conical,
);
assert_close(illum, 0.0, 1e-10, "LEO full shadow");
}
#[test]
fn conical_leo_sunlit() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let earth_pos = ZERO;
let sat_pos = Vector3::new(6371.0 + 400.0, 0.0, 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&earth_pos,
695700.0,
6371.0,
ShadowModel::Conical,
);
assert_close(illum, 1.0, 1e-12, "LEO sunlit");
}
#[test]
fn conical_geo_penumbra_transition_wider_than_leo() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let earth_pos = ZERO;
let r_earth = 6371.0;
let r_sun = 695700.0;
let penumbra_width = |alt: f64| -> f64 {
let x = -(r_earth + alt);
let mut y_low = 0.0_f64;
let mut y_high = 0.0_f64;
for i in 0..20000 {
let y = r_earth * 2.0 * (i as f64) / 20000.0;
let sat = Vector3::new(x, y, 0.0);
let illum = eclipse::illumination(
&sat,
&sun_pos,
&earth_pos,
r_sun,
r_earth,
ShadowModel::Conical,
);
if illum > 0.05 && y_low == 0.0 {
y_low = y;
}
if illum > 0.95 {
y_high = y;
break;
}
}
y_high - y_low
};
let leo_pw = penumbra_width(400.0);
let geo_pw = penumbra_width(35786.0);
assert!(
geo_pw > leo_pw,
"GEO penumbra transition should be wider: LEO={leo_pw:.1} km, GEO={geo_pw:.1} km"
);
}
#[test]
fn cylindrical_sunlit() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let sat_pos = Vector3::new(6371.0 + 400.0, 0.0, 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&ZERO,
695700.0,
6371.0,
ShadowModel::Cylindrical,
);
assert_close(illum, 1.0, 1e-12, "cylindrical sunlit");
}
#[test]
fn cylindrical_umbra() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let sat_pos = Vector3::new(-(6371.0 + 400.0), 0.0, 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&ZERO,
695700.0,
6371.0,
ShadowModel::Cylindrical,
);
assert_close(illum, 0.0, 1e-12, "cylindrical umbra");
}
#[test]
fn cylindrical_perpendicular() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let sat_pos = Vector3::new(0.0, 6371.0 + 400.0, 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&ZERO,
695700.0,
6371.0,
ShadowModel::Cylindrical,
);
assert_close(illum, 1.0, 1e-12, "cylindrical perpendicular");
}
#[test]
fn cylindrical_just_inside_shadow() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let sat_pos = Vector3::new(-(6371.0 + 400.0), 6371.0 * 0.5, 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&ZERO,
695700.0,
6371.0,
ShadowModel::Cylindrical,
);
assert_close(illum, 0.0, 1e-12, "cylindrical just inside shadow");
}
#[test]
fn cylindrical_just_outside_shadow() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let sat_pos = Vector3::new(-(6371.0 + 400.0), 6371.0 * 1.1, 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&ZERO,
695700.0,
6371.0,
ShadowModel::Cylindrical,
);
assert_close(illum, 1.0, 1e-12, "cylindrical just outside shadow");
}
#[test]
fn cylindrical_is_binary() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let r_earth = 6371.0;
for i in 0..360 {
let angle = (i as f64) * std::f64::consts::PI / 180.0;
let r = r_earth + 400.0;
let sat_pos = Vector3::new(r * angle.cos(), r * angle.sin(), 0.0);
let illum = eclipse::illumination(
&sat_pos,
&sun_pos,
&ZERO,
695700.0,
r_earth,
ShadowModel::Cylindrical,
);
assert!(
illum == 0.0 || illum == 1.0,
"cylindrical should be binary, got {illum} at angle {i}°"
);
}
}
#[test]
fn illumination_central_matches_full_api() {
let sun_pos = Vector3::new(149_597_870.7, 0.0, 0.0);
let sat_pos = Vector3::new(-(6371.0 + 400.0), 1000.0, 0.0);
let full = eclipse::illumination(
&sat_pos,
&sun_pos,
&ZERO,
695700.0,
6371.0,
ShadowModel::Conical,
);
let central =
eclipse::illumination_central(&sat_pos, &sun_pos, 6371.0, 695700.0, ShadowModel::Conical);
assert_close(
central,
full,
1e-15,
"illumination_central matches full API",
);
}
#[test]
fn conical_output_always_in_0_1() {
let a_values = [0.001, 0.005, 0.01, 0.02, 0.05];
let b_values = [0.001, 0.005, 0.01, 0.02, 0.05];
let c_values = [0.0, 0.001, 0.005, 0.01, 0.02, 0.03, 0.05, 0.1, 0.5];
for &a in &a_values {
for &b in &b_values {
for &c in &c_values {
let case = GeometryCase::from_angles(a, b, c, D_LIGHT, D_OCCULTER);
let illum = case.illumination_conical();
assert!(
(0.0..=1.0).contains(&illum),
"illumination out of [0,1]: a={a}, b={b}, c={c}, illum={illum}"
);
}
}
}
}