use serde::{Deserialize, Serialize};
use crate::classify::cascade::PeriodicVia;
use crate::classify::grow;
use crate::classify::heesch::count_coronas;
use crate::classify::lattice::{
basis_inverse, lattice_classes, lattice_from_orbit, signature_groups,
};
use crate::classify::torus::gold_check;
use crate::cyclotomic::IsRing;
use crate::cyclotomic::geometry::float::norm_f;
use crate::cyclotomic::geometry::{area_eq_k_area, covol_eq_m_areas};
use crate::geom::iso::{Iso, build_orbit, cryst_order, gluing_iso};
use crate::geom::matches::{PatchMatch, TileMatch};
use crate::geom::patch::{BasicPatch, boundary_vertices};
use crate::geom::rat::Rat;
const VERIFY_ORBIT_CAP: usize = 3_000;
const VERIFY_RADIUS_EXTENTS: f64 = 16.0;
const VERIFY_RADIUS_PAD: f64 = 30.0;
const REPLAY_CAP: usize = 100_000;
fn replay_build<T: IsRing>(build: &[PatchMatch], base: &Rat<T>) -> Option<BasicPatch<T>> {
if build.is_empty() || build.len() > REPLAY_CAP {
return None;
}
grow::replay_recipe(base, build, |_, _| true)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeriodicCert {
pub build: Vec<PatchMatch>,
pub glue: Vec<TileMatch>,
pub via: PeriodicVia,
}
impl PeriodicCert {
pub fn reconstruct<T: IsRing>(&self, base: &Rat<T>) -> Option<BasicPatch<T>> {
replay_build(&self.build, base)
}
pub fn boundary_arc_pairs<T: IsRing>(&self, base: &Rat<T>) -> Option<Vec<usize>> {
let n = self
.reconstruct(base)?
.boundary_positions()
.len()
.checked_sub(1)?;
if n == 0 {
return Some(Vec::new());
}
let mut partner = vec![usize::MAX; n];
for tm in &self.glue {
let (a, b) = (tm.a.range.start_offset, tm.b.range.start_offset);
if a < n && b < n {
partner[a] = b;
partner[b] = a;
}
}
let same_arc = |e: usize| -> bool {
let (p, q) = (partner[e], partner[(e + 1) % n]);
p != usize::MAX && q != usize::MAX && ((p + 1) % n == q || (q + 1) % n == p)
};
let brk = (0..n).find(|&e| !same_arc(e)).unwrap_or(0);
let mut arc_of = vec![0usize; n];
let mut cur = 0usize;
for k in 0..n {
let e = (brk + 1 + k) % n;
arc_of[e] = cur;
if !same_arc(e) {
cur += 1;
}
}
let mut arc_color = vec![usize::MAX; cur + 1];
let mut ncol = 0usize;
for e in 0..n {
let a = arc_of[e];
if arc_color[a] == usize::MAX && partner[e] != usize::MAX {
arc_color[a] = ncol;
arc_color[arc_of[partner[e]]] = ncol;
ncol += 1;
}
}
Some(
(0..n)
.map(|e| arc_color[arc_of[e]].min(ncol.saturating_sub(1)))
.collect(),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HeeschStatus {
Finite,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HeeschCert {
pub heesch: u8,
pub status: HeeschStatus,
pub build: Vec<PatchMatch>,
pub bound: u8,
pub budget: u32,
}
impl HeeschCert {
pub fn reconstruct<T: IsRing>(&self, base: &Rat<T>) -> Option<BasicPatch<T>> {
replay_build(&self.build, base)
}
pub fn verify_lower_bound<T: IsRing>(&self, base: &Rat<T>) -> bool {
count_coronas(base, &self.build) >= self.heesch as usize
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verdict {
Periodic(PeriodicCert),
CannotTile(HeeschCert),
}
pub(crate) fn base_verts<T: IsRing>(base: &Rat<T>) -> Vec<T> {
boundary_vertices::<T>(base.seq())
}
pub(crate) fn meta_of<T: IsRing>(
build: &[PatchMatch],
base: &Rat<T>,
) -> Option<(Vec<T>, Vec<i8>, usize)> {
if build.is_empty() {
return Some((base_verts(base), base.seq().to_vec(), 1));
}
let gp = replay_build(build, base)?;
let k = gp.next_tile_id(); let bp = gp.boundary_positions(); let verts = bp[..bp.len() - 1].to_vec();
let seq = gp.angles().to_vec();
Some((verts, seq, k))
}
impl PeriodicCert {
pub fn verify<T: IsRing>(&self, base: &Rat<T>) -> bool {
let Some((meta_verts, meta_seq, k)) = meta_of(&self.build, base) else {
return false;
};
let Some(gens) = glue_isometries(&self.glue, &meta_verts) else {
return false;
};
let orbit = build_orbit(
&meta_verts,
&gens,
orbit_radius(&meta_verts),
VERIFY_ORBIT_CAP,
);
let Some((v1, v2)) = lattice_from_orbit(&orbit) else {
return false;
};
let Some(inv) = basis_inverse(&v1, &v2) else {
return false;
};
let domain = coset_reps(&orbit, &meta_verts, v1, v2, &inv);
if !covol_eq_m_areas(v1, v2, &meta_verts, domain.len())
|| !area_eq_k_area(&meta_verts, &base_verts(base), k)
{
return false;
}
gold_check(&meta_verts, &meta_seq, &domain, v1, v2)
}
pub fn grow<T: IsRing>(&self, base: &Rat<T>, radius: f64, cap: usize) -> Option<Vec<Iso<T>>> {
let (meta_verts, _seq, _k) = meta_of(&self.build, base)?;
let gens = glue_isometries(&self.glue, &meta_verts)?;
Some(build_orbit(&meta_verts, &gens, radius, cap))
}
}
fn glue_isometries<T: IsRing>(glue: &[TileMatch], meta_verts: &[T]) -> Option<Vec<Iso<T>>> {
let mut gens = Vec::with_capacity(glue.len() * 2);
for tm in glue {
if tm.a.tile_id != 0 || tm.b.tile_id != 0 || tm.a.range.len != 1 || tm.b.range.len != 1 {
return None;
}
let g = gluing_iso(meta_verts, tm.a.range.start_offset, tm.b.range.start_offset)?;
cryst_order::<T>(g.rot)?;
gens.push(g);
gens.push(g.inv());
}
Some(gens)
}
fn coset_reps<T: IsRing>(
orbit: &[Iso<T>],
meta_verts: &[T],
v1: T,
v2: T,
inv: &[[f64; 2]; 2],
) -> Vec<Iso<T>> {
let groups = signature_groups(orbit, meta_verts);
lattice_classes(&groups, v1, v2, inv)
.into_iter()
.map(|(_, iso)| iso)
.collect()
}
fn orbit_radius<T: IsRing>(meta_verts: &[T]) -> f64 {
let maxn = meta_verts.iter().map(norm_f).fold(0.0_f64, f64::max);
maxn * VERIFY_RADIUS_EXTENTS + VERIFY_RADIUS_PAD
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Classified {
Decided(Verdict),
Undecided {
depth: usize,
corona: Vec<PatchMatch>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geom::matches::{EdgeRange, Segment};
use crate::geom::tiles;
use crate::geom::tileset::TileSet;
fn sample_periodic() -> PeriodicCert {
PeriodicCert {
build: vec![
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(1, EdgeRange::new(3, 1))),
PatchMatch::new(EdgeRange::new(2, 2), Segment::new(2, EdgeRange::new(5, 2))),
],
glue: vec![TileMatch::new(
Segment::new(0, EdgeRange::new(1, 2)),
Segment::new(1, EdgeRange::new(4, 2)),
)],
via: PeriodicVia::Anisohedral(3),
}
}
#[test]
fn periodic_cert_round_trips() {
let cert = sample_periodic();
let json = serde_json::to_string(&cert).unwrap();
let back: PeriodicCert = serde_json::from_str(&json).unwrap();
assert_eq!(back, cert);
}
#[test]
fn heesch_cert_round_trips() {
let cert = HeeschCert {
heesch: 1,
status: HeeschStatus::Finite,
build: vec![PatchMatch::new(
EdgeRange::new(0, 1),
Segment::new(1, EdgeRange::new(3, 1)),
)],
bound: 2,
budget: 20_000_000,
};
let json = serde_json::to_string(&cert).unwrap();
let back: HeeschCert = serde_json::from_str(&json).unwrap();
assert_eq!(back, cert);
}
#[test]
fn reconstruct_replays_a_hand_recipe() {
use crate::cyclotomic::ZZ12;
let base = Rat::<ZZ12>::from_snake_trusted(&tiles::triangle()); let ts = TileSet::single(base.clone());
let seed = BasicPatch::single_tile(ts, 0);
let m0 = *seed.get_all_matches().first().unwrap();
let mut gp = seed.with_tile(&m0).unwrap();
let mut build = vec![m0];
for _ in 0..2 {
let m = *gp.get_all_matches().first().expect("a glue is available");
assert!(gp.add_tile(&m).is_some());
build.push(m);
}
let direct_rat = gp.to_rat();
let cert = PeriodicCert {
build,
glue: vec![],
via: PeriodicVia::Conway,
};
let recon = cert.reconstruct(&base).expect("recipe replays");
assert_eq!(
recon.to_rat(),
direct_rat,
"reconstructed meta-tile matches the grown one"
);
let wrong = Rat::<ZZ12>::from_snake_trusted(&tiles::dodecagon());
assert!(
cert.reconstruct(&wrong).is_none(),
"recipe is base-specific"
);
}
#[test]
fn boundary_arc_pairs_labels_every_meta_edge() {
use crate::classify::cascade::{AcceptBounds, certify_periodic};
use crate::cyclotomic::ZZ12;
let seq: &[i8] = &[-3, 2, 2, 2, -2, 3, 1, 3, -3, 2, 2, 3];
let base = Rat::<ZZ12>::from_slice_trusted(seq);
let cert =
certify_periodic::<ZZ12>(seq, &AcceptBounds::default()).expect("tiles periodically");
let arcs = cert
.boundary_arc_pairs(&base)
.expect("k>=2 meta has a boundary");
let n = cert.reconstruct(&base).unwrap().boundary_positions().len() - 1;
assert_eq!(arcs.len(), n, "one arc-pair label per meta boundary edge");
let ncol = arcs.iter().max().map_or(0, |m| m + 1);
assert!(
ncol >= 2,
"a k=4 cell boundary splits into several arc-pairs"
);
assert!(arcs.iter().all(|&c| c < ncol), "labels dense in 0..ncol");
}
#[test]
fn verdict_round_trips_both_arms() {
for v in [
Verdict::Periodic(sample_periodic()),
Verdict::CannotTile(HeeschCert {
heesch: 0,
status: HeeschStatus::Finite,
build: vec![],
bound: 1,
budget: 100_000,
}),
] {
let json = serde_json::to_string(&v).unwrap();
let back: Verdict = serde_json::from_str(&json).unwrap();
assert_eq!(back, v);
}
}
}