use crate::classify::conway::build_tiling;
use crate::classify::tiling::{CLUSTER_ORBIT_CAP, ORBIT_RADIUS_FACTOR, Tiling, verify_tiling};
use crate::cyclotomic::IsRing;
use crate::geom::matches::PatchMatch;
use crate::geom::patch::BasicPatch;
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
use crate::stringmatch::canonical_rotation;
pub struct ClusterWitness<T> {
pub k: usize,
pub build: Vec<PatchMatch>,
pub tiling: Tiling<T>,
}
fn cluster_tiling<T: IsRing>(seq: &[i8]) -> Option<Tiling<T>> {
let r = ORBIT_RADIUS_FACTOR * seq.len() as f64;
build_tiling::<T>(seq, r, CLUSTER_ORBIT_CAP).filter(|t| verify_tiling(t).ok())
}
#[inline]
fn xorshift64(s: &mut u64) -> u64 {
let mut x = *s;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*s = x;
x
}
fn seeded_shuffle<X>(v: &mut [X], state: &mut u64) {
if *state == 0 || v.len() < 2 {
return;
}
for i in (1..v.len()).rev() {
let j = (xorshift64(state) % (i as u64 + 1)) as usize;
v.swap(i, j);
}
}
pub fn tiles_anisohedral_restart<T: IsRing>(
tile_seq: &[i8],
kmax: usize,
cluster_cap: usize,
per_budget: usize,
restarts: usize,
) -> Option<ClusterWitness<T>> {
if restarts <= 1 {
return tiles_anisohedral_seeded::<T>(tile_seq, kmax, cluster_cap, per_budget, 0);
}
for seed in 1..=restarts as u64 {
if let Some(hit) =
tiles_anisohedral_seeded::<T>(tile_seq, kmax, cluster_cap, per_budget, seed)
{
return Some(hit);
}
}
None
}
pub fn tiles_anisohedral_seeded<T: IsRing>(
tile_seq: &[i8],
kmax: usize,
cluster_cap: usize,
budget: usize,
seed: u64,
) -> Option<ClusterWitness<T>> {
use std::collections::HashSet;
let ts = TileSet::single(Rat::<T>::from_slice_trusted(tile_seq));
let pseed = BasicPatch::single_tile(ts, 0);
let mut seen: HashSet<Vec<i8>> = HashSet::new();
let mut examined = 0usize;
let over = |examined: usize| budget != 0 && examined >= budget;
let mut rng: u64 = seed;
let mut frontier: Vec<(BasicPatch<T>, Vec<PatchMatch>)> = Vec::new();
let mut k2: Vec<PatchMatch> = pseed.get_all_matches();
seeded_shuffle(&mut k2, &mut rng);
for pm in &k2 {
let Some(gp) = pseed.with_tile(pm) else {
continue;
};
let key = canonical_rotation(gp.angles());
if !seen.insert(key.clone()) {
continue;
}
examined += 1;
let recipe = vec![*pm];
if let Some(tiling) = cluster_tiling::<T>(&key) {
return Some(ClusterWitness {
k: 2,
build: recipe,
tiling,
});
}
if over(examined) {
return None;
}
frontier.push((gp, recipe));
}
for k in 3..=kmax {
let mut next: Vec<(BasicPatch<T>, Vec<PatchMatch>)> = Vec::new();
seeded_shuffle(&mut frontier, &mut rng);
for (gp, recipe) in &frontier {
let mut matches = gp.get_all_matches();
seeded_shuffle(&mut matches, &mut rng);
for pm in &matches {
let mut g2 = gp.clone();
if g2.add_tile(pm).is_none() {
continue;
}
let key = canonical_rotation(g2.angles());
if !seen.insert(key.clone()) {
continue;
}
examined += 1;
let mut r2 = recipe.clone();
r2.push(*pm);
if let Some(tiling) = cluster_tiling::<T>(&key) {
if crate::classify::trace::aniso() {
eprintln!(
"ANISO_TRACE: found k={k} cluster after examining {examined} clusters"
);
}
return Some(ClusterWitness {
k,
build: r2,
tiling,
});
}
if over(examined) {
return None;
}
next.push((g2, r2));
}
}
next.truncate(cluster_cap);
frontier = next;
if frontier.is_empty() {
break;
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::ZZ12;
#[test]
#[ignore = "aniso restart rescues the heavy-tail cluster (104492), ~30s"]
fn aniso_restart_rescues_heavy_tail() {
let seq = [-1i8, 0, -1, 0, 5, 1, 0, 1, 1, 1, 5]; let (kmax, cap, per) = (6, 4_000, 5_000);
assert!(
tiles_anisohedral_seeded::<ZZ12>(&seq, kmax, cap, per, 0).is_none(),
"seed 0 should miss the deep cluster at per_budget {per}",
);
let hit = tiles_anisohedral_restart::<ZZ12>(&seq, kmax, cap, per, 12);
assert_eq!(
hit.map(|w| w.k),
Some(4),
"restarts must find the k=4 cluster"
);
}
}