use serde::Serialize;
use crate::chart::Placement;
use crate::dignities::{traditional_ruler, Reception, SEVEN, SIGNS};
use crate::ephemeris::KernelSet;
use crate::horary::PTOLEMAIC;
use crate::planets::calculate_planets;
use crate::pyfloat;
const MAX_REFRANATION_DAYS: f64 = 400.0;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Perfection {
pub applying: &'static str,
pub to: &'static str,
pub aspect: &'static str,
pub degrees: f64,
pub days: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Prohibition {
pub by: &'static str,
pub significator: &'static str,
pub aspect: &'static str,
pub days: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Translation {
pub by: &'static str,
pub from: &'static str,
pub from_aspect: &'static str,
pub to: &'static str,
pub to_aspect: &'static str,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Collection {
pub by: &'static str,
pub querent_aspect: &'static str,
pub quesited_aspect: &'static str,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Refranation {
pub planet: &'static str,
pub aspect: &'static str,
pub days: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Judgment {
pub quesited_house: Option<u8>,
pub querent: &'static str,
pub co_significator: &'static str,
pub quesited: Option<&'static str>,
pub same_significator: bool,
pub perfection: Option<Perfection>,
pub moon_perfection: Option<Perfection>,
pub prohibition: Option<Prohibition>,
pub refranation: Option<Refranation>,
pub translation: Vec<Translation>,
pub collection: Vec<Collection>,
pub receptions: Vec<Reception>,
}
#[derive(Debug, Clone, Copy)]
struct Body {
name: &'static str,
lon: f64,
speed: f64,
}
impl Body {
fn days_in_sign(self) -> f64 {
let within = pyfloat::rem(self.lon, 30.0);
if self.speed > 0.0 {
(30.0 - within) / self.speed
} else if self.speed < 0.0 {
within / -self.speed
} else {
f64::INFINITY
}
}
}
fn next_aspect(a: Body, b: Body) -> Option<(&'static str, f64, f64)> {
let v = a.speed - b.speed;
if v.abs() < 1e-9 {
return None;
}
let d0 = a.lon - b.lon;
let horizon = a.days_in_sign().min(b.days_in_sign());
let mut best: Option<(&'static str, f64, f64)> = None;
for (angle, aspect) in PTOLEMAIC {
for target in [angle, -angle] {
let t = pyfloat::rem((target - d0) * v.signum(), 360.0) / v.abs();
if t > 1e-9 && t < horizon && best.is_none_or(|(_, bt, _)| t < bt) {
best = Some((aspect, t, (v * t).abs()));
}
}
}
best
}
fn last_aspect(a: Body, b: Body) -> Option<(&'static str, f64)> {
let back = |x: Body| Body {
speed: -x.speed,
..x
};
next_aspect(back(a), back(b)).map(|(aspect, t, _)| (aspect, t))
}
fn round2(x: f64) -> f64 {
pyfloat::round(x, 2)
}
fn perfection(a: Body, b: Body) -> Option<Perfection> {
next_aspect(a, b).map(|(aspect, days, degrees)| {
let (applying, to) = if a.speed.abs() >= b.speed.abs() {
(a.name, b.name)
} else {
(b.name, a.name)
};
Perfection {
applying,
to,
aspect,
degrees: round2(degrees),
days: round2(days),
}
})
}
fn station_within(
kernels: &KernelSet,
jd: f64,
shift: f64,
planet: Body,
days: f64,
) -> Option<f64> {
if matches!(planet.name, "Sun" | "Moon") || days <= 0.0 {
return None;
}
let span = days.min(MAX_REFRANATION_DAYS);
let step = (span / 60.0).clamp(0.25, 5.0);
let speed_at = |t: f64| -> Option<f64> {
calculate_planets(kernels, jd + t, shift)
.ok()?
.bodies
.into_iter()
.find(|b| b.name == planet.name)
.map(|b| b.speed)
};
let mut t = 0.0;
let mut previous = planet.speed;
while t < span {
t = (t + step).min(span);
let speed = speed_at(t)?;
if speed.signum() != previous.signum() {
return Some(t);
}
previous = speed;
}
None
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn judge(
kernels: &KernelSet,
jd: f64,
shift: f64,
planets: &[Placement],
cusp_signs: &[&'static str],
asc_sign: &str,
quesited_house: Option<u8>,
receptions: &[Reception],
) -> Judgment {
let body = |name: &str| {
planets.iter().find(|p| p.name == name).map(|p| Body {
name: p.name,
lon: p.ecliptic_longitude,
speed: p.speed,
})
};
let querent = traditional_ruler(asc_sign).unwrap_or("Mars");
let quesited = quesited_house
.filter(|h| (1..=12).contains(h))
.and_then(|h| cusp_signs.get(h as usize - 1))
.and_then(|sign| traditional_ruler(sign));
let same_significator = quesited == Some(querent);
let mut judgment = Judgment {
quesited_house,
querent,
co_significator: "Moon",
quesited,
same_significator,
perfection: None,
moon_perfection: None,
prohibition: None,
refranation: None,
translation: Vec::new(),
collection: Vec::new(),
receptions: Vec::new(),
};
let (Some(q), Some(a)) = (quesited.and_then(body), body(querent)) else {
return judgment;
};
if let Some(moon) = body("Moon").filter(|_| q.name != "Moon") {
judgment.moon_perfection = perfection(moon, q);
}
if same_significator {
return judgment;
}
judgment.receptions = receptions
.iter()
.filter(|r| {
(r.planet == a.name && r.receiver == q.name)
|| (r.planet == q.name && r.receiver == a.name)
})
.cloned()
.collect();
let direct = perfection(a, q);
let others: Vec<Body> = SEVEN
.iter()
.filter(|n| **n != a.name && **n != q.name)
.filter_map(|n| body(n))
.collect();
if let Some(p) = &direct {
judgment.prohibition = others
.iter()
.flat_map(|x| {
[a, q].into_iter().filter_map(move |sig| {
next_aspect(*x, sig).map(|(aspect, days, _)| (x.name, sig.name, aspect, days))
})
})
.filter(|(_, _, _, days)| *days < p.days)
.min_by(|l, r| l.3.total_cmp(&r.3))
.map(|(by, significator, aspect, days)| Prohibition {
by,
significator,
aspect,
days: round2(days),
});
judgment.refranation = [a, q]
.into_iter()
.filter_map(|sig| {
station_within(kernels, jd, shift, sig, p.days).map(|days| (sig.name, days))
})
.min_by(|l, r| l.1.total_cmp(&r.1))
.map(|(planet, days)| Refranation {
planet,
aspect: p.aspect,
days: pyfloat::round(days, 1),
});
} else {
for x in &others {
let lighter = x.speed.abs() > a.speed.abs().max(q.speed.abs());
let heavier = x.speed.abs() < a.speed.abs().min(q.speed.abs());
if lighter {
for (from, to) in [(a, q), (q, a)] {
if let (Some((from_aspect, _)), Some((to_aspect, _, _))) =
(last_aspect(*x, from), next_aspect(*x, to))
{
judgment.translation.push(Translation {
by: x.name,
from: from.name,
from_aspect,
to: to.name,
to_aspect,
});
}
}
}
if heavier {
if let (Some((qa, _, _)), Some((qq, _, _))) =
(next_aspect(a, *x), next_aspect(q, *x))
{
judgment.collection.push(Collection {
by: x.name,
querent_aspect: qa,
quesited_aspect: qq,
});
}
}
}
}
judgment.perfection = direct;
judgment
}
pub(crate) fn cusp_signs(cusps: &[f64]) -> Vec<&'static str> {
cusps
.iter()
.map(|lon| SIGNS[(pyfloat::floordiv(pyfloat::rem(*lon, 360.0), 30.0) as usize) % 12])
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn b(name: &'static str, lon: f64, speed: f64) -> Body {
Body { name, lon, speed }
}
#[test]
fn applying_trine_within_signs() {
let (aspect, days, degrees) =
next_aspect(b("Moon", 10.0, 13.0), b("Jupiter", 140.0, 0.1)).unwrap();
assert_eq!(aspect, "Trine");
assert!((days - 10.0 / 12.9).abs() < 1e-9);
assert!((degrees - 10.0).abs() < 1e-9);
}
#[test]
fn no_perfection_once_a_planet_leaves_its_sign() {
assert!(next_aspect(b("Moon", 28.0, 13.0), b("Jupiter", 130.0, 0.0)).is_none());
}
#[test]
fn separating_aspects_are_in_the_past() {
let (aspect, ago) = last_aspect(b("Mercury", 75.0, 1.5), b("Mars", 10.0, 0.5)).unwrap();
assert_eq!(aspect, "Sextile");
assert!((ago - 5.0).abs() < 1e-9);
}
#[test]
fn retrograde_planets_apply_backwards() {
let (aspect, days, _) =
next_aspect(b("Venus", 42.0, -0.5), b("Saturn", 130.0, 0.03)).unwrap();
assert_eq!(aspect, "Square");
assert!((days - 2.0 / 0.53).abs() < 1e-9);
}
#[test]
fn mercury_stations_retrograde_on_1_april_2024() {
use crate::ephemeris::{Kernel, Spk};
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../kernels/de440s.bsp");
if !path.exists() {
eprintln!("skipping: {} not found", path.display());
return;
}
let mut kernels = KernelSet::new();
kernels.push(Kernel::new("de440s.bsp", Spk::open(path).unwrap()).unwrap());
let jd = crate::UtcInstant::parse("2024-03-25T00:00:00Z")
.unwrap()
.julian_day();
let mercury = calculate_planets(&kernels, jd, 0.0)
.unwrap()
.bodies
.into_iter()
.find(|b| b.name == "Mercury")
.unwrap();
let body = b("Mercury", mercury.longitude, mercury.speed);
let days = station_within(&kernels, jd, 0.0, body, 30.0).unwrap();
assert!((days - 7.9).abs() <= 0.6, "{days}");
assert!(station_within(&kernels, jd, 0.0, body, 5.0).is_none());
}
}