use serde::{Deserialize, Serialize};
use crate::classify::aniso::tiles_anisohedral_restart;
use crate::classify::cert::{Classified, PeriodicCert, Verdict};
use crate::classify::conway::build_tiling;
use crate::classify::heesch::{Heesch, heesch_number_witnessed};
use crate::classify::isohedral::isohedral_tiling;
use crate::classify::mint::{cert_from_cluster, cert_from_tiling, heesch_cert, torus_cert};
use crate::classify::tiling::{DETECT_ORBIT_CAP, ORBIT_RADIUS_FACTOR};
use crate::cyclotomic::IsRing;
use crate::geom::matches::PatchMatch;
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeriodicVia {
Conway,
Translation,
Isohedral,
Anisohedral(usize),
Torus(usize),
}
#[derive(Debug, Clone, Copy)]
pub struct AcceptBounds {
pub iso_builds: usize,
pub aniso_kmax: usize,
pub aniso_cap: usize,
pub aniso_budget: usize,
pub aniso_restarts: usize,
pub torus_kmax: usize,
pub torus_coronas: usize,
}
impl Default for AcceptBounds {
fn default() -> Self {
AcceptBounds {
iso_builds: 20_000,
aniso_kmax: 4,
aniso_cap: 4_000,
aniso_budget: 300_000,
aniso_restarts: 1,
torus_kmax: 12,
torus_coronas: 3,
}
}
}
fn periodic_verdict(pc: PeriodicCert) -> Classified {
Classified::Decided(Verdict::Periodic(pc))
}
fn bn_pc<T: IsRing>(seq: &[i8]) -> Option<PeriodicCert> {
let base = Rat::<T>::from_slice_trusted(seq);
crate::classify::conway::bn_criterion::<T>(seq)
.into_iter()
.find_map(|(v1, v2)| crate::classify::mint::translation_cert(&base, v1, v2))
}
fn conway_pc<T: IsRing>(seq: &[i8]) -> Option<PeriodicCert> {
let base = Rat::<T>::from_slice_trusted(seq);
let tiling = build_tiling::<T>(
seq,
ORBIT_RADIUS_FACTOR * seq.len() as f64,
DETECT_ORBIT_CAP,
)?;
cert_from_tiling(&base, &tiling, PeriodicVia::Conway)
}
fn torus_pc<T: IsRing>(seq: &[i8], kmax: usize, coronas: usize) -> Option<PeriodicCert> {
torus_cert::<T>(&Rat::<T>::from_slice_trusted(seq), kmax, coronas)
}
fn iso_pc<T: IsRing>(seq: &[i8], bounds: &AcceptBounds) -> Option<PeriodicCert> {
let base = Rat::<T>::from_slice_trusted(seq);
let tiling = isohedral_tiling::<T>(
seq,
ORBIT_RADIUS_FACTOR * seq.len() as f64,
DETECT_ORBIT_CAP,
bounds.iso_builds,
)?;
cert_from_tiling(&base, &tiling, PeriodicVia::Isohedral)
}
fn aniso_pc<T: IsRing>(seq: &[i8], bounds: &AcceptBounds) -> Option<PeriodicCert> {
let w = tiles_anisohedral_restart::<T>(
seq,
bounds.aniso_kmax,
bounds.aniso_cap,
bounds.aniso_budget,
bounds.aniso_restarts,
)?;
cert_from_cluster::<T>(
&Rat::<T>::from_slice_trusted(seq),
&w.build,
&w.tiling,
PeriodicVia::Anisohedral(w.k),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StageKind {
Accept,
Reject,
}
impl StageKind {
pub fn selected_by(self, mode: StageMode) -> bool {
match self {
StageKind::Accept => mode.runs_accepts(),
StageKind::Reject => mode.runs_rejects(),
}
}
}
pub enum StageOutcome {
Settled(Classified),
Pass,
PassWitnessed(Heesch, Vec<PatchMatch>),
}
pub type StageCheck = Box<dyn Fn(&[i8]) -> StageOutcome + Sync>;
pub struct Stage {
pub name: &'static str,
pub kind: StageKind,
pub check: StageCheck,
}
fn accept(name: &'static str, f: impl Fn(&[i8]) -> Option<PeriodicCert> + Sync + 'static) -> Stage {
let check = move |s: &[i8]| match f(s) {
Some(pc) => StageOutcome::Settled(periodic_verdict(pc)),
None => StageOutcome::Pass,
};
Stage {
name,
kind: StageKind::Accept,
check: Box::new(check),
}
}
fn reject<T: IsRing>(name: &'static str, bound: usize, budget: usize) -> Stage {
let check = move |s: &[i8]| {
let base = Rat::<T>::from_slice_trusted(s);
let (h, build) = heesch_number_witnessed(TileSet::single(base.clone()), 0, bound, budget);
if h.cannot_tile() {
StageOutcome::Settled(Classified::Decided(Verdict::CannotTile(heesch_cert(
&base, h, build, bound, budget,
))))
} else {
StageOutcome::PassWitnessed(h, build)
}
};
Stage {
name,
kind: StageKind::Reject,
check: Box::new(check),
}
}
fn witnessed_depth(h: Heesch) -> usize {
match h {
Heesch::AtLeast(b) => b,
Heesch::Unknown(k) | Heesch::Finite(k) => k,
}
}
pub fn run_stages<T: IsRing>(seq: &[i8], stages: &[Stage], mode: StageMode) -> Classified {
let mut bank: Option<(Heesch, Vec<PatchMatch>)> = None;
for stage in stages.iter().filter(|st| st.kind.selected_by(mode)) {
match (stage.check)(seq) {
StageOutcome::Settled(c) => return c,
StageOutcome::Pass => {}
StageOutcome::PassWitnessed(h, build) => {
if bank
.as_ref()
.is_none_or(|(bh, _)| witnessed_depth(h) >= witnessed_depth(*bh))
{
bank = Some((h, build));
}
}
}
}
match bank {
Some((h, build)) => undecided_witnessed(h, build),
None => undecided::<T>(seq, UNDECIDED_WITNESS_BOUND, UNDECIDED_WITNESS_BUDGET),
}
}
const H2_BUDGET: usize = 1_000_000; const H3_BUDGET: usize = 200_000;
pub fn fast_stages<T: IsRing>(
b: AcceptBounds,
deep_torus_kmax: usize,
deep_bound: usize,
deep_budget: usize,
) -> Vec<Stage> {
let deep = AcceptBounds {
torus_kmax: deep_torus_kmax,
torus_coronas: b.torus_coronas + 1,
..b
};
vec![
accept("conway", |s| conway_pc::<T>(s)),
accept("bn", |s| bn_pc::<T>(s)),
reject::<T>("heesch2", 2, H2_BUDGET),
accept("torus", move |s| {
torus_pc::<T>(s, b.torus_kmax, b.torus_coronas)
}),
accept("iso", move |s| iso_pc::<T>(s, &b)),
accept("aniso", move |s| aniso_pc::<T>(s, &b)),
accept("torus-deep", move |s| {
torus_pc::<T>(s, deep.torus_kmax, deep.torus_coronas)
}),
reject::<T>("heesch3", 3, H3_BUDGET),
reject::<T>("deep-heesch", deep_bound, deep_budget),
]
}
pub fn deep_stages<T: IsRing>(bounds: AcceptBounds, hbound: usize, hbudget: usize) -> Vec<Stage> {
vec![
accept("cheap-accepts", move |s| {
certify_periodic_cheap::<T>(s, &bounds)
}),
reject::<T>("heesch", hbound, hbudget),
accept("deep-accepts", move |s| {
certify_periodic_deep::<T>(s, &bounds)
}),
]
}
pub const UNDECIDED_WITNESS_BOUND: usize = 3;
pub const UNDECIDED_WITNESS_BUDGET: usize = 100_000;
pub fn undecided_witnessed(h: Heesch, corona: Vec<PatchMatch>) -> Classified {
debug_assert!(
!h.cannot_tile(),
"Finite is a settled reject, not Undecided"
);
Classified::Undecided {
depth: witnessed_depth(h),
corona,
}
}
pub fn undecided<T: IsRing>(seq: &[i8], target: usize, budget: usize) -> Classified {
let ts = TileSet::single(Rat::<T>::from_slice_trusted(seq));
match crate::classify::heesch::corona_witness(&ts, 0, target, budget) {
Some(corona) => Classified::Undecided {
depth: target,
corona,
},
None => Classified::Undecided {
depth: 0,
corona: Vec::new(),
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StageMode {
#[default]
Full,
PeriodicOnly,
HeeschOnly,
}
impl StageMode {
pub fn runs_accepts(self) -> bool {
matches!(self, StageMode::Full | StageMode::PeriodicOnly)
}
pub fn runs_rejects(self) -> bool {
matches!(self, StageMode::Full | StageMode::HeeschOnly)
}
}
pub fn classify<T: IsRing>(
seq: &[i8],
bounds: &AcceptBounds,
hbound: usize,
hbudget: usize,
mode: StageMode,
) -> Classified {
run_stages::<T>(seq, &deep_stages::<T>(*bounds, hbound, hbudget), mode)
}
pub fn certify_periodic<T: IsRing>(seq: &[i8], bounds: &AcceptBounds) -> Option<PeriodicCert> {
certify_periodic_cheap::<T>(seq, bounds).or_else(|| certify_periodic_deep::<T>(seq, bounds))
}
pub fn certify_periodic_cheap<T: IsRing>(
seq: &[i8],
bounds: &AcceptBounds,
) -> Option<PeriodicCert> {
if let Some(pc) = conway_pc::<T>(seq) {
return Some(pc);
}
if let Some(pc) = bn_pc::<T>(seq) {
return Some(pc);
}
for c in 2..=3.min(bounds.torus_coronas.max(2)) {
if let Some(pc) = torus_pc::<T>(seq, bounds.torus_kmax, c) {
return Some(pc);
}
}
None
}
pub fn certify_periodic_deep<T: IsRing>(seq: &[i8], bounds: &AcceptBounds) -> Option<PeriodicCert> {
if let Some(pc) = aniso_pc::<T>(seq, bounds) {
return Some(pc);
}
if let Some(pc) = iso_pc::<T>(seq, bounds) {
return Some(pc);
}
for c in 4..=bounds.torus_coronas {
if let Some(pc) = torus_pc::<T>(seq, bounds.torus_kmax, c) {
return Some(pc);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::cert::{HeeschStatus, Verdict};
use crate::cyclotomic::ZZ12;
use crate::geom::tiles;
#[test]
fn undecided_keeps_corona_witness() {
let c = undecided::<ZZ12>(&[-4, 0, 2, 0, 4, 0, 0, 2, 0, 2, 0, 4, 2], 3, 5_000_000);
match &c {
Classified::Undecided { depth, corona } => {
assert_eq!(*depth, 3);
assert!(!corona.is_empty(), "kept a non-empty corona witness");
}
other => panic!("expected Undecided, got {other:?}"),
}
let js = serde_json::to_string(&c).unwrap();
assert_eq!(
serde_json::from_str::<Classified>(&js).unwrap(),
c,
"serde round-trip"
);
}
#[test]
fn undecided_witnessed_reuses_search_witness() {
use crate::classify::heesch::count_coronas;
let tri = Rat::<ZZ12>::from_snake_trusted(&tiles::triangle());
let ts = TileSet::single(tri.clone());
let (h, build) = heesch_number_witnessed(ts, 0, 2, 5_000_000);
assert_eq!(h, Heesch::AtLeast(2));
match undecided_witnessed(h, build) {
Classified::Undecided { depth, corona } => {
assert_eq!(depth, 2);
assert_eq!(
count_coronas(&tri, &corona),
2,
"witness replays to the recorded depth"
);
}
other => panic!("expected Undecided, got {other:?}"),
}
}
#[test]
fn undecided_fallback_never_overclaims() {
let dodec = Rat::<ZZ12>::from_snake_trusted(&tiles::dodecagon());
let c = undecided::<ZZ12>(dodec.seq(), 2, 200_000);
assert_eq!(
c,
Classified::Undecided {
depth: 0,
corona: vec![]
}
);
}
#[test]
#[ignore = "integration: full classify across the three verdict kinds (~80s)"]
fn classify_routes_to_certified_verdicts() {
use crate::cyclotomic::ZZ12;
let b = AcceptBounds::default();
let tri = Rat::<ZZ12>::from_snake_trusted(&tiles::triangle());
match classify::<ZZ12>(tri.seq(), &b, 2, 20_000_000, StageMode::Full) {
Classified::Decided(Verdict::Periodic(pc)) => {
assert!(pc.verify(&tri));
}
other => panic!("triangle should be Periodic, got {other:?}"),
}
let dodec = Rat::<ZZ12>::from_snake_trusted(&tiles::dodecagon());
match classify::<ZZ12>(dodec.seq(), &b, 2, 200_000, StageMode::Full) {
Classified::Decided(Verdict::CannotTile(hc)) => {
assert_eq!(hc.heesch, 0);
assert!(hc.verify_lower_bound(&dodec));
}
other => panic!("dodecagon should be CannotTile(0), got {other:?}"),
}
let t1 = [-2i8, 1, 4, -1, 4, -1, 2, -1, 5, 1];
match classify::<ZZ12>(&t1, &b, 2, 20_000_000, StageMode::Full) {
Classified::Decided(Verdict::CannotTile(hc)) => {
assert_eq!((hc.heesch, hc.status), (1, HeeschStatus::Finite));
assert!(hc.verify_lower_bound(&Rat::<ZZ12>::from_slice_trusted(&t1)));
}
other => panic!("[-2,1,4,...] should be CannotTile(1), got {other:?}"),
}
}
#[test]
fn certify_periodic_is_deterministic() {
use crate::cyclotomic::ZZ12;
let bounds = AcceptBounds {
torus_coronas: 4,
..AcceptBounds::default()
};
let tri = Rat::<ZZ12>::from_snake_trusted(&tiles::triangle());
for seq in [&[-2i8, -1, 2, 5, -2, 1, 2, 1, 2, 4][..], tri.seq()] {
let first = certify_periodic::<ZZ12>(seq, &bounds).expect("mints");
let a = serde_json::to_string(&first).unwrap();
for _ in 0..3 {
let pc = certify_periodic::<ZZ12>(seq, &bounds).expect("mints");
assert_eq!(pc.via, first.via, "{seq:?}: via stable");
assert_eq!(
serde_json::to_string(&pc).unwrap(),
a,
"{seq:?}: cert byte-identical across runs"
);
}
}
}
#[test]
fn certify_periodic_tags_provenance() {
use crate::cyclotomic::ZZ12;
let bounds = AcceptBounds {
torus_coronas: 4,
..AcceptBounds::default()
};
let tri = Rat::<ZZ12>::from_snake_trusted(&tiles::triangle());
let cases: [(&[i8], PeriodicVia); 4] = [
(tri.seq(), PeriodicVia::Conway), (&[-2, -1, 2, 5, -2, 1, 2, 1, 2, 4], PeriodicVia::Torus(3)), (
&[-3, 2, 2, 2, -2, 3, 1, 3, -3, 2, 2, 3],
PeriodicVia::Torus(4),
), (
&[-4, 0, 4, 0, 2, 2, -2, 0, 4, 2, -2, 0, 4, 2],
PeriodicVia::Translation,
),
];
for (seq, want) in cases {
let pc =
certify_periodic::<ZZ12>(seq, &bounds).unwrap_or_else(|| panic!("{seq:?}: mints"));
assert_eq!(pc.via, want, "{seq:?}: cert carries its via");
assert!(
pc.verify(&Rat::<ZZ12>::from_slice_trusted(seq)),
"{seq:?}: verifies"
);
}
}
}