use super::*;
pub(super) struct Sampler<'a> {
pub(super) kernels: &'a KernelSet,
pub(super) req: &'a ChartRequest<'a>,
pub(super) system: HouseSystem,
pub(super) sidereal: bool,
pub(super) ayanamsa: &'static str,
pub(super) origin: UtcInstant,
pub(super) anchors: Vec<(i64, Anchor)>,
}
pub(super) struct Anchor {
pub(super) bodies: Vec<BodyPosition>,
pub(super) gast_hours: f64,
}
pub(super) fn can_retrograde(name: &str) -> bool {
!matches!(name, "Sun" | "Moon" | "Lilith")
}
impl<'a> Sampler<'a> {
pub(super) fn new(kernels: &'a KernelSet, req: &'a ChartRequest<'a>) -> Self {
Sampler {
kernels,
req,
system: HouseSystem::from_code(req.house_system),
sidereal: req.zodiac_type.trim().eq_ignore_ascii_case("sidereal"),
ayanamsa: ayanamsa(req.ayanamsa).code,
origin: req.instant,
anchors: Vec::new(),
}
}
pub(super) fn anchor(&mut self, index: i64) -> Result<&Anchor, EngineError> {
if let Some(pos) = self.anchors.iter().position(|(i, _)| *i == index) {
return Ok(&self.anchors[pos].1);
}
let jd = self
.origin
.add_micros(index * 2 * 3_600_000_000)
.julian_day();
let shift = if self.sidereal {
ayanamsa_info(jd, self.ayanamsa).value
} else {
0.0
};
let (planets, gast_hours) = planets_and_sidereal_time(
self.kernels,
jd,
shift,
NodeType::from_code(self.req.node_type),
)?;
if self.anchors.len() == 3 {
self.anchors.remove(0);
}
self.anchors.push((
index,
Anchor {
bodies: planets.bodies,
gast_hours,
},
));
Ok(&self.anchors.last().unwrap().1)
}
pub(super) fn at(
&mut self,
instant: UtcInstant,
) -> Result<(Vec<Placement>, Vec<f64>), EngineError> {
let hours = instant.seconds_since(&self.origin) / 3600.0;
let index = (hours / 2.0).floor() as i64;
let into = hours - index as f64 * 2.0; let (first, gast0) = {
let anchor = self.anchor(index)?;
let bodies: Vec<(&'static str, f64, f64, bool)> = anchor
.bodies
.iter()
.map(|b| (b.name, b.longitude, b.speed, b.is_retrograde))
.collect();
(bodies, anchor.gast_hours)
};
let (next_bodies, gast1) = {
let next = self.anchor(index + 1)?;
(
next.bodies
.iter()
.map(|b| (b.name, b.longitude, b.speed))
.collect::<Vec<_>>(),
next.gast_hours,
)
};
let hour_later = |lon: f64, speed: f64| pyfloat::rem(lon + speed / 24.0, 360.0);
let mut raw = Vec::with_capacity(first.len());
for &(name, lon, speed, is_retrograde) in &first {
if into == 0.0 {
raw.push((name, lon, speed, is_retrograde));
continue;
}
let Some(&(_, next, next_speed)) = next_bodies.iter().find(|(n, ..)| *n == name) else {
continue;
};
let points = [
lon,
hour_later(lon, speed),
next,
hour_later(next, next_speed),
];
let track = |hours: f64| {
let i = (hours.floor() as usize).min(2);
let (a, b) = (points[i], points[i + 1]);
pyfloat::rem(a + pyfloat::wrap180(b - a) * (hours - i as f64), 360.0)
};
let (longitude, later) = (track(into), track(into + 1.0));
let speed = pyfloat::wrap180(later - longitude) * 24.0;
raw.push((name, longitude, speed, can_retrograde(name) && speed < 0.0));
}
let gast1 = if gast1 < gast0 { gast1 + 24.0 } else { gast1 };
let gast = if into == 0.0 {
gast0
} else {
pyfloat::rem(gast0 + (gast1 - gast0) * into / 2.0, 24.0)
};
let jd = instant.julian_day();
let shift = if self.sidereal {
ayanamsa_info(jd, self.ayanamsa).value
} else {
0.0
};
let houses = houses_at_sidereal_time(
jd,
gast,
self.req.latitude,
self.req.longitude,
self.system,
shift,
);
let cusps = houses.cusps.to_vec();
Ok((placements(raw, &houses), cusps))
}
}
pub(super) struct SolarDays<'a> {
pub(super) kernels: &'a KernelSet,
pub(super) latitude: f64,
pub(super) longitude: f64,
pub(super) events: Option<SunEvents>,
pub(super) last: Option<SolarDay>,
}
impl<'a> SolarDays<'a> {
pub(super) fn new(kernels: &'a KernelSet, req: &ChartRequest, end: UtcInstant) -> Self {
let events = SunEvents::find(
kernels,
jd_utc(req.instant) - 1.6,
jd_utc(end) + 1.6,
req.latitude,
req.longitude,
)
.ok()
.flatten();
SolarDays {
kernels,
latitude: req.latitude,
longitude: req.longitude,
events,
last: None,
}
}
pub(super) fn hours(&mut self, instant: UtcInstant) -> PlanetaryHours {
if let Some(times) = self
.events
.as_ref()
.and_then(|e| e.rise_set(jd_utc(instant)))
{
return hours_in(&SolarDay::from_julian_days(times), instant);
}
let reuse = self.latitude.abs() < SOLAR_DAY_CACHE_MAX_LAT;
let day = match self.last {
Some(d) if reuse && d.covers(instant) => d,
_ => solar_day(self.kernels, instant, self.latitude, self.longitude),
};
self.last = Some(day);
hours_in(&day, instant)
}
}
pub fn search_elections(
kernels: &KernelSet,
req: &ChartRequest,
end: UtcInstant,
criteria: &ElectionCriteria,
) -> Result<ElectionSearch, EngineError> {
req.check_place()?;
let resolved = criteria.resolve()?;
let start = req.instant;
let span_days = end.seconds_since(&start) / 86_400.0;
if span_days <= 0.0 {
return Err(EngineError::InvalidInput(
"the search must end after it starts".into(),
));
}
if span_days > MAX_SEARCH_DAYS {
return Err(EngineError::InvalidInput(format!(
"a search spans at most {MAX_SEARCH_DAYS} days"
)));
}
require_kernel(kernels, start.julian_day())?;
require_kernel(kernels, end.julian_day() + 3.0 / 24.0)?;
let step = i64::from(resolved.summary.step_minutes) * 60_000_000;
let mut sampler = Sampler::new(kernels, req);
let mut days = SolarDays::new(kernels, req, end);
let mut evaluated = 0;
let mut excluded = Exclusions::default();
let mut windows: Vec<(UtcInstant, ElectionWindow)> = Vec::new();
let mut open: Option<(UtcInstant, ElectionWindow)> = None;
let mut instant = start;
while instant <= end {
let kept = 'moment: {
if let Some(range) = resolved.summary.local_hours {
if !range.contains(resolved.local_minute_of_day(instant)) {
excluded.count("outside_hours");
break 'moment None;
}
}
let hours = days.hours(instant);
if resolved.summary.daytime_only && !hours.is_day {
excluded.count("night");
break 'moment None;
}
let (planets, cusps) = sampler.at(instant)?;
let data = assess(
&Moment {
instant,
planets: &planets,
cusps: &cusps,
hours: &hours,
},
&resolved,
)?;
evaluated += 1;
if let Some(reason) = data.excluded_by.first() {
excluded.count(reason);
break 'moment None;
}
(data.score >= resolved.summary.min_score).then_some(data)
};
match kept {
Some(data) => {
let at = instant.isoformat();
match open.as_mut() {
Some((best, w)) => {
w.end = at.clone();
if data.score > w.score {
*best = instant;
w.best = at;
w.score = data.score;
}
}
None => {
open = Some((
instant,
ElectionWindow {
start: at.clone(),
end: at.clone(),
best: at,
score: data.score,
verdict: data.verdict,
factors: Vec::new(),
},
))
}
}
}
None => windows.extend(open.take()),
}
instant = instant.add_micros(step);
}
windows.extend(open.take());
windows.sort_by(|a, b| b.1.score.total_cmp(&a.1.score));
let max_results = resolved.summary.max_results;
let mut confirmed = Vec::with_capacity(max_results.min(windows.len()));
for (best, mut window) in windows {
if confirmed.len() == max_results {
break;
}
let exact = sky(
kernels,
&ChartRequest {
instant: best,
..req.clone()
},
)?;
let data = assess(
&Moment {
instant: best,
planets: &exact.planets,
cusps: &exact.cusps,
hours: &days.hours(best),
},
&resolved,
)?;
if !data.excluded_by.is_empty() || data.score < resolved.summary.min_score {
continue;
}
window.score = data.score;
window.verdict = data.verdict;
window.factors = data.factors;
confirmed.push(window);
}
confirmed.sort_by(|a, b| b.score.total_cmp(&a.score));
Ok(ElectionSearch {
start: start.isoformat(),
end: end.isoformat(),
windows: confirmed,
evaluated,
excluded,
criteria: resolved.summary,
})
}