use super::{Aspect, AspectType, BodyPosition};
#[derive(Debug, Clone)]
pub struct Pattern {
pub pattern_type: PatternType,
pub body_indices: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PatternType {
GrandTrine,
TSquare,
Yod,
GrandCross,
Stellium,
}
#[must_use]
pub fn detect_patterns(aspects: &[Aspect], positions: &[BodyPosition]) -> Vec<Pattern> {
let mut patterns = Vec::new();
detect_grand_trines(aspects, &mut patterns);
detect_t_squares(aspects, &mut patterns);
detect_yods(aspects, &mut patterns);
detect_grand_crosses(aspects, &mut patterns);
detect_stelliums(positions, &mut patterns);
patterns
}
fn has_aspect(aspects: &[Aspect], i: usize, j: usize, kind: AspectType) -> bool {
aspects.iter().any(|a| {
a.aspect_type == kind
&& ((a.body1_index == i && a.body2_index == j)
|| (a.body1_index == j && a.body2_index == i))
})
}
fn detect_grand_trines(aspects: &[Aspect], out: &mut Vec<Pattern>) {
let trine_bodies: Vec<usize> = {
let mut v: Vec<usize> = aspects
.iter()
.filter(|a| a.aspect_type == AspectType::Trine)
.flat_map(|a| [a.body1_index, a.body2_index])
.collect();
v.sort_unstable();
v.dedup();
v
};
let n = trine_bodies.len();
for ai in 0..n {
for bi in (ai + 1)..n {
for ci in (bi + 1)..n {
let a = trine_bodies[ai];
let b = trine_bodies[bi];
let c = trine_bodies[ci];
if has_aspect(aspects, a, b, AspectType::Trine)
&& has_aspect(aspects, b, c, AspectType::Trine)
&& has_aspect(aspects, a, c, AspectType::Trine)
{
out.push(Pattern {
pattern_type: PatternType::GrandTrine,
body_indices: vec![a, b, c],
});
}
}
}
}
}
fn detect_t_squares(aspects: &[Aspect], out: &mut Vec<Pattern>) {
let oppositions: Vec<(usize, usize)> = aspects
.iter()
.filter(|a| a.aspect_type == AspectType::Opposition)
.map(|a| (a.body1_index, a.body2_index))
.collect();
let all_bodies: Vec<usize> = {
let mut v: Vec<usize> = aspects
.iter()
.flat_map(|a| [a.body1_index, a.body2_index])
.collect();
v.sort_unstable();
v.dedup();
v
};
for (a, b) in &oppositions {
for &c in &all_bodies {
if c == *a || c == *b {
continue;
}
if has_aspect(aspects, c, *a, AspectType::Square)
&& has_aspect(aspects, c, *b, AspectType::Square)
{
out.push(Pattern {
pattern_type: PatternType::TSquare,
body_indices: vec![*a, *b, c],
});
}
}
}
}
fn detect_yods(aspects: &[Aspect], out: &mut Vec<Pattern>) {
let sextiles: Vec<(usize, usize)> = aspects
.iter()
.filter(|a| a.aspect_type == AspectType::Sextile)
.map(|a| (a.body1_index, a.body2_index))
.collect();
let all_bodies: Vec<usize> = {
let mut v: Vec<usize> = aspects
.iter()
.flat_map(|a| [a.body1_index, a.body2_index])
.collect();
v.sort_unstable();
v.dedup();
v
};
for (a, b) in &sextiles {
for &c in &all_bodies {
if c == *a || c == *b {
continue;
}
if has_aspect(aspects, c, *a, AspectType::Quincunx)
&& has_aspect(aspects, c, *b, AspectType::Quincunx)
{
out.push(Pattern {
pattern_type: PatternType::Yod,
body_indices: vec![*a, *b, c],
});
}
}
}
}
fn detect_grand_crosses(aspects: &[Aspect], out: &mut Vec<Pattern>) {
let oppositions: Vec<(usize, usize)> = aspects
.iter()
.filter(|a| a.aspect_type == AspectType::Opposition)
.map(|a| (a.body1_index, a.body2_index))
.collect();
let opp_count = oppositions.len();
for i in 0..opp_count {
for j in (i + 1)..opp_count {
let (body_a, body_b) = oppositions[i];
let (body_c, body_d) = oppositions[j];
let mut indices = [body_a, body_b, body_c, body_d];
indices.sort_unstable();
if indices[0] == indices[1] || indices[1] == indices[2] || indices[2] == indices[3] {
continue;
}
if has_aspect(aspects, body_a, body_c, AspectType::Square)
&& has_aspect(aspects, body_a, body_d, AspectType::Square)
&& has_aspect(aspects, body_b, body_c, AspectType::Square)
&& has_aspect(aspects, body_b, body_d, AspectType::Square)
{
out.push(Pattern {
pattern_type: PatternType::GrandCross,
body_indices: vec![body_a, body_b, body_c, body_d],
});
}
}
}
}
fn detect_stelliums(positions: &[BodyPosition], out: &mut Vec<Pattern>) {
let orb = AspectType::Conjunction.default_orb();
let n = positions.len();
let mut adjacent = vec![vec![false; n]; n];
for i in 0..n {
for j in (i + 1)..n {
let sep = vedaksha_math::angle::angular_separation(
positions[i].longitude,
positions[j].longitude,
);
if sep <= orb {
adjacent[i][j] = true;
adjacent[j][i] = true;
}
}
}
for size in 3..=n {
find_cliques_of_size(&adjacent, n, size, out);
}
}
fn find_cliques_of_size(adjacent: &[Vec<bool>], n: usize, size: usize, out: &mut Vec<Pattern>) {
let mut combo: Vec<usize> = (0..size).collect();
loop {
let is_clique = combo.windows(2).all(|_| true) && {
let mut ok = true;
'outer: for ai in 0..combo.len() {
for bi in (ai + 1)..combo.len() {
if !adjacent[combo[ai]][combo[bi]] {
ok = false;
break 'outer;
}
}
}
ok
};
if is_clique {
let already_covered = out.iter().any(|p| {
p.pattern_type == PatternType::Stellium
&& combo.iter().all(|idx| p.body_indices.contains(idx))
});
if !already_covered {
out.push(Pattern {
pattern_type: PatternType::Stellium,
body_indices: combo.clone(),
});
}
}
if !next_combination(&mut combo, n) {
break;
}
}
}
fn next_combination(combo: &mut [usize], n: usize) -> bool {
let k = combo.len();
let mut i = k;
loop {
if i == 0 {
return false;
}
i -= 1;
if combo[i] < n - k + i {
combo[i] += 1;
for j in (i + 1)..k {
combo[j] = combo[j - 1] + 1;
}
return true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aspects::{BodyPosition, find_aspects};
fn pos(longitude: f64) -> BodyPosition {
BodyPosition {
longitude,
speed: 1.0,
}
}
#[test]
fn detects_grand_trine() {
let positions = [pos(0.0), pos(120.0), pos(240.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
assert!(
patterns
.iter()
.any(|p| p.pattern_type == PatternType::GrandTrine),
"Expected Grand Trine"
);
}
#[test]
fn detects_stellium_three_bodies() {
let positions = [pos(10.0), pos(12.0), pos(14.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
assert!(
patterns
.iter()
.any(|p| p.pattern_type == PatternType::Stellium),
"Expected Stellium"
);
}
#[test]
fn stellium_contains_correct_indices() {
let positions = [pos(10.0), pos(12.0), pos(14.0), pos(200.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
let stellium = patterns
.iter()
.find(|p| p.pattern_type == PatternType::Stellium)
.expect("Expected a Stellium");
assert!(
!stellium.body_indices.contains(&3),
"Body 3 should not be in the stellium"
);
assert!(stellium.body_indices.contains(&0));
assert!(stellium.body_indices.contains(&1));
assert!(stellium.body_indices.contains(&2));
}
#[test]
fn no_stellium_when_bodies_spread_out() {
let positions = [pos(0.0), pos(60.0), pos(120.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
assert!(
!patterns
.iter()
.any(|p| p.pattern_type == PatternType::Stellium),
"Should not find stellium when bodies are spread"
);
}
#[test]
fn detects_t_square() {
let positions = [pos(0.0), pos(180.0), pos(90.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
assert!(
patterns
.iter()
.any(|p| p.pattern_type == PatternType::TSquare),
"Expected T-Square"
);
}
#[test]
fn detects_yod() {
let positions = [pos(0.0), pos(60.0), pos(210.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
assert!(
patterns.iter().any(|p| p.pattern_type == PatternType::Yod),
"Expected Yod"
);
}
#[test]
fn detects_grand_cross() {
let positions = [pos(0.0), pos(90.0), pos(180.0), pos(270.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
assert!(
patterns
.iter()
.any(|p| p.pattern_type == PatternType::GrandCross),
"Expected Grand Cross"
);
}
#[test]
fn no_grand_trine_for_random_positions() {
let positions = [pos(0.0), pos(45.0), pos(200.0)];
let aspects = find_aspects(&positions, AspectType::ALL, 1.0);
let patterns = detect_patterns(&aspects, &positions);
assert!(
!patterns
.iter()
.any(|p| p.pattern_type == PatternType::GrandTrine),
"Should not find Grand Trine for random positions"
);
}
}