use crate::positions::Position;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Occultation {
None,
Partial,
Full,
}
impl std::fmt::Display for Occultation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Occultation::None => write!(f, "None"),
Occultation::Partial => write!(f, "Partial"),
Occultation::Full => write!(f, "Full"),
}
}
}
pub fn occultation(
target: &Position,
occulter: &Position,
target_radius_km: f64,
occulter_radius_km: f64,
) -> Occultation {
let target_distance = target.distance();
let occulter_distance = occulter.distance();
if occulter_distance >= target_distance {
return Occultation::None;
}
let target_radius = target.angular_semi_diameter([target_radius_km; 3]);
let occulter_radius = occulter.angular_semi_diameter([occulter_radius_km; 3]);
let separation = target.separation_from(occulter);
if separation >= target_radius + occulter_radius {
Occultation::None
} else if separation <= occulter_radius - target_radius {
Occultation::Full
} else {
Occultation::Partial
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::{AU_KM, DAY_S};
use crate::jplephem::kernel::SpiceKernel;
use crate::jplephem_ext::SpiceKernelExt;
use crate::planetlib::Body;
use crate::positions::PositionKind;
use crate::searchlib::{find_discrete, DEFAULT_NUM, EPSILON_DISCRETE};
use crate::time::{Time, Timescale};
use nalgebra::Vector3;
fn seen_from_here(position: Vector3<f64>) -> Position {
Position {
position,
velocity: Vector3::zeros(),
kind: PositionKind::Astrometric,
center: 0,
target: 0,
light_time: 0.0,
observer_barycentric: None,
}
}
fn at_angle(distance_au: f64, angle: f64) -> Position {
seen_from_here(Vector3::new(angle.cos(), angle.sin(), 0.0) * distance_au)
}
#[test]
fn test_a_target_clear_of_the_occulter_is_not_occulted() {
let target = at_angle(1.0, 0.0);
let occulter = at_angle(0.5, 2f64.to_radians());
assert_eq!(
occultation(&target, &occulter, 6378.0, 3396.0),
Occultation::None
);
}
#[test]
fn test_a_grazing_target_is_partly_occulted() {
let target = at_angle(1.0, 0.0);
let target_radius = target.angular_semi_diameter([6378.0; 3]);
let occulter_distance = 0.5;
let occulter_radius = (3396.0 / (occulter_distance * AU_KM)).asin();
let occulter = at_angle(occulter_distance, occulter_radius);
assert!(target_radius < occulter_radius);
assert_eq!(
occultation(&target, &occulter, 6378.0, 3396.0),
Occultation::Partial
);
}
#[test]
fn test_a_target_behind_the_occulter_is_fully_occulted() {
let target = at_angle(1.0, 0.0);
let target_radius = target.angular_semi_diameter([6378.0; 3]);
let occulter_distance = 0.5;
let occulter_radius = (3396.0 / (occulter_distance * AU_KM)).asin();
let occulter = at_angle(occulter_distance, 0.5 * (occulter_radius - target_radius));
assert_eq!(
occultation(&target, &occulter, 6378.0, 3396.0),
Occultation::Full
);
}
#[test]
fn test_an_occulter_behind_the_target_occults_nothing() {
let target = at_angle(0.5, 0.0);
let occulter = at_angle(1.0, 0.0);
assert_eq!(
occultation(&target, &occulter, 3396.0, 6378.0),
Occultation::None
);
assert_eq!(
occultation(&target, &at_angle(0.5, 0.0), 3396.0, 6378.0),
Occultation::None
);
}
#[test]
fn test_an_annular_occultation_is_partial() {
let target = at_angle(1.0, 0.0);
let occulter = at_angle(0.5, 0.0);
let target_radius = target.angular_semi_diameter([70000.0; 3]);
let occulter_radius = occulter.angular_semi_diameter([1737.4; 3]);
assert!(occulter_radius < target_radius);
assert_eq!(
occultation(&target, &occulter, 70000.0, 1737.4),
Occultation::Partial
);
}
const GM_MARS: f64 = 42_828.375_214;
const ORBIT_RADIUS_KM: f64 = 17_000.0;
struct MarsOrbiter {
kernel: SpiceKernel,
toward_earth: Vector3<f64>,
across: Vector3<f64>,
epoch_tt: f64,
period_days: f64,
ts: Timescale,
}
impl MarsOrbiter {
fn new() -> Self {
let mut kernel = SpiceKernel::open("test_data/de421.bsp").expect("test_data/de421.bsp");
let ts = Timescale::default();
let t = ts.utc((2007, 10, 3, 8, 30, 0.0));
let mars = kernel.at("mars", &t).unwrap();
let earth = mars.observe("earth", &mut kernel, &t).unwrap();
let toward_earth = earth.position.normalize();
let across = toward_earth.cross(&Vector3::z()).normalize();
let seconds = std::f64::consts::TAU * (ORBIT_RADIUS_KM.powi(3) / GM_MARS).sqrt();
MarsOrbiter {
kernel,
toward_earth,
across,
epoch_tt: t.tt(),
period_days: seconds / DAY_S,
ts,
}
}
fn observer(&mut self, t: &Time) -> Position {
let phase = std::f64::consts::TAU * (t.tt() - self.epoch_tt) / self.period_days;
let radius_au = ORBIT_RADIUS_KM / AU_KM;
let speed = std::f64::consts::TAU * radius_au / self.period_days;
let (sin, cos) = phase.sin_cos();
let mars = self.kernel.at("mars", t).unwrap();
Position::barycentric(
mars.position + radius_au * (cos * self.toward_earth + sin * self.across),
mars.velocity + speed * (-sin * self.toward_earth + cos * self.across),
-1,
)
}
fn state(&mut self, t: &Time) -> Occultation {
let observer = self.observer(t);
let earth = observer.observe("earth", &mut self.kernel, t).unwrap();
let mars = observer.observe("mars", &mut self.kernel, t).unwrap();
occultation(
&earth,
&mars,
Body::Earth.radii_km()[0],
Body::Mars.radii_km()[0],
)
}
}
#[test]
fn test_mars_occults_the_earth_once_per_orbit() {
let mut orbiter = MarsOrbiter::new();
let start = orbiter.epoch_tt;
let end = start + orbiter.period_days;
let t0 = orbiter.ts.tt_jd(start, None);
assert_eq!(orbiter.state(&t0), Occultation::None);
let mut sample = |jd: &[f64]| -> Vec<i64> {
jd.iter()
.map(|&jd| {
let t = orbiter.ts.tt_jd(jd, None);
match orbiter.state(&t) {
Occultation::None => 0,
Occultation::Partial => 1,
Occultation::Full => 2,
}
})
.collect()
};
let events = find_discrete(
start,
end,
&mut sample,
0.005,
EPSILON_DISCRETE,
DEFAULT_NUM,
);
let values: Vec<i64> = events.iter().map(|&(_, v)| v).collect();
assert_eq!(
values,
vec![1, 2, 1, 0],
"one ingress and one egress per orbit, got {:?}",
events
);
let full_seconds = (events[2].0 - events[1].0) * DAY_S;
let mars_radius = (Body::Mars.radii_km()[0] / ORBIT_RADIUS_KM).asin();
let earth_radius = 18.50 / 2.0 / 206_264.806_247_096_36;
let expected = 2.0 * (mars_radius - earth_radius) / std::f64::consts::TAU
* orbiter.period_days
* DAY_S;
assert!(
(full_seconds - expected).abs() < 0.05 * expected,
"total occultation lasted {full_seconds} s, expected about {expected} s"
);
let middle = orbiter.ts.tt_jd(0.5 * (events[1].0 + events[2].0), None);
assert_eq!(orbiter.state(&middle), Occultation::Full);
}
}