use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use crate::classify::grow::{capture_placement, replay_placements};
use crate::cyclotomic::IsRing;
use crate::cyclotomic::geometry::{
PointLocation, cmp_xy, intersect_unit_segments, point_in_polygon,
};
use crate::geom::iso::{Iso, dir_of_unit};
use crate::geom::matches::PatchMatch;
use crate::geom::patch::{BasicPatch, boundary_vertices};
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
use crate::stringmatch::canonical_rotation;
fn scaled_seq(seq: &[i8], k: usize) -> Vec<i8> {
let mut out = Vec::with_capacity(seq.len() * k);
for &t in seq {
out.push(t);
out.extend(std::iter::repeat_n(0, k - 1)); }
out
}
fn mate<T: IsRing>(base: &[T], f: usize, p: T, q: T) -> Option<Iso<T>> {
let n = base.len();
let src = dir_of_unit::<T>(base[(f + 1) % n] - base[f])?;
let dst = dir_of_unit::<T>(q - p)?;
Some(Iso::carrying(base[f], src, p, dst))
}
fn interiors_disjoint<T: IsRing>(a: &[T], b: &[T]) -> bool {
let (na, nb) = (a.len(), b.len());
for i in 0..na {
let ea = (a[i], a[(i + 1) % na]);
for j in 0..nb {
let eb = (b[j], b[(j + 1) % nb]);
if intersect_unit_segments(&ea, &eb) {
return false;
}
}
}
!a.iter()
.any(|v| point_in_polygon(v, b) == PointLocation::Inside)
&& !b
.iter()
.any(|v| point_in_polygon(v, a) == PointLocation::Inside)
}
fn add_edge<T: IsRing>(f: &mut HashMap<(T, T), i32>, x: T, y: T) {
if let Some(c) = f.get_mut(&(y, x)) {
*c -= 1;
if *c == 0 {
f.remove(&(y, x));
}
} else {
*f.entry((x, y)).or_insert(0) += 1;
}
}
fn contained_in<T: IsRing>(poly: &[T], region: &[T], redges: &[(T, T)]) -> bool {
if poly
.iter()
.any(|v| point_in_polygon(v, region) == PointLocation::Outside)
{
return false;
}
let m = poly.len();
(0..m).all(|i| {
let e = (poly[i], poly[(i + 1) % m]);
!redges.iter().any(|re| intersect_unit_segments(&e, re))
})
}
fn union_boundary_seq<T: IsRing>(polys: &[Vec<T>], expected: usize) -> Option<Vec<i8>> {
let mut boundary: HashMap<(T, T), i32> = HashMap::new();
for poly in polys {
let m = poly.len();
for i in 0..m {
add_edge(&mut boundary, poly[i], poly[(i + 1) % m]);
}
}
if boundary.len() != expected {
return None;
}
let mut next: HashMap<T, T> = HashMap::with_capacity(expected);
for (&(a, b), &c) in &boundary {
if c != 1 || next.insert(a, b).is_some() {
return None;
}
}
let &start = next.keys().next()?;
let mut verts = Vec::with_capacity(expected);
let mut cur = start;
loop {
verts.push(cur);
cur = *next.get(&cur)?;
if cur == start {
break;
}
if verts.len() > expected {
return None;
}
}
if verts.len() != expected {
return None; }
let h = T::turn() / 2;
let mut seq = Vec::with_capacity(expected);
for i in 0..expected {
let prev = dir_of_unit::<T>(verts[i] - verts[(i + expected - 1) % expected])?;
let cur = dir_of_unit::<T>(verts[(i + 1) % expected] - verts[i])?;
let raw = (cur - prev).rem_euclid(T::turn());
seq.push(if raw > h { raw - T::turn() } else { raw });
}
Some(seq)
}
fn cyclic_eq(a: &[i8], b: &[i8]) -> bool {
a.len() == b.len() && canonical_rotation(a) == canonical_rotation(b)
}
struct Filler<'a, T: IsRing> {
base: &'a [T],
n: usize,
region: &'a [T],
redges: &'a [(T, T)],
target: usize,
placed: Vec<Iso<T>>,
polys: Vec<Vec<T>>,
frontier: HashMap<(T, T), i32>,
budget: usize,
nodes: usize,
}
impl<T: IsRing> Filler<'_, T> {
fn search(&mut self) -> bool {
self.nodes += 1;
if self.nodes > self.budget {
return false; }
if self.frontier.is_empty() {
return self.placed.len() == self.target;
}
if self.placed.len() >= self.target {
return false; }
let &(p, q) = self
.frontier
.keys()
.min_by(|a, b| cmp_xy(&a.0, &b.0).then_with(|| cmp_xy(&a.1, &b.1)))
.unwrap();
for f in 0..self.n {
let Some(g) = mate(self.base, f, p, q) else {
continue;
};
if self
.placed
.iter()
.any(|h| h.rot == g.rot && h.shift == g.shift)
{
continue; }
let poly = g.tile(self.base);
if !contained_in(&poly, self.region, self.redges) {
continue;
}
if self.polys.iter().any(|pp| !interiors_disjoint(&poly, pp)) {
continue;
}
let saved = self.frontier.clone();
let m = poly.len();
for i in 0..m {
add_edge(&mut self.frontier, poly[(i + 1) % m], poly[i]);
}
self.placed.push(g);
self.polys.push(poly);
if self.search() {
return true;
}
self.polys.pop();
self.placed.pop();
self.frontier = saved;
}
false
}
}
fn fill<T: IsRing>(base: &Rat<T>, k: usize, budget: usize) -> (Option<Vec<Iso<T>>>, bool) {
if k < 2 {
return (None, false);
}
let bverts = boundary_vertices::<T>(base.seq());
let region = boundary_vertices::<T>(&scaled_seq(base.seq(), k));
let redges: Vec<(T, T)> = (0..region.len())
.map(|i| (region[i], region[(i + 1) % region.len()]))
.collect();
let mut frontier: HashMap<(T, T), i32> = HashMap::new();
for &(a, b) in &redges {
*frontier.entry((a, b)).or_insert(0) += 1;
}
let mut filler = Filler {
base: &bverts,
n: bverts.len(),
region: ®ion,
redges: &redges,
target: k * k,
placed: Vec::new(),
polys: Vec::new(),
frontier,
budget,
nodes: 0,
};
let found = filler.search();
let aborted = filler.nodes > budget;
(found.then_some(filler.placed), aborted)
}
pub fn reptile_at<T: IsRing>(base: &Rat<T>, k: usize) -> Option<Vec<Iso<T>>> {
fill(base, k, usize::MAX).0
}
pub fn reptile<T: IsRing>(base: &Rat<T>, kmax: usize) -> Option<(usize, Vec<Iso<T>>)> {
(2..=kmax).find_map(|k| reptile_at(base, k).map(|w| (k, w)))
}
pub(crate) fn build_from_placements<T: IsRing>(
base: &Rat<T>,
placements: &[Iso<T>],
) -> Option<Vec<PatchMatch>> {
if placements.len() <= 1 {
return Some(Vec::new());
}
let v = boundary_vertices::<T>(base.seq());
let root_inv = placements[0].inv();
let targets: HashSet<Iso<T>> = placements.iter().map(|g| root_inv.after(g)).collect();
let seed = BasicPatch::single_tile(TileSet::single(base.clone()), 0);
let mut chosen = None;
for first in seed.get_all_matches() {
let Some(g) = seed.with_tile(&first) else {
continue;
};
let (Some(q0), Some(i1)) = (capture_placement(&g, &v, 0), capture_placement(&g, &v, 1))
else {
continue;
};
let q0inv = q0.inv();
let t1 = q0inv.after(&i1);
if targets.contains(&t1) {
chosen = Some((g, q0inv, t1, vec![first]));
break;
}
}
let (mut gp, q0inv, t1, mut build) = chosen?;
let mut placed: HashSet<Iso<T>> = HashSet::from([Iso::id(), t1]);
while placed.len() < placements.len() {
let mut advanced = false;
'scan: for edge in 0..gp.len() {
for pm in gp.get_matches_in_edge_range(edge, edge) {
let new_id = gp.next_tile_id();
let mut trial = gp.clone();
if trial.add_tile(&pm).is_none() {
continue;
}
let Some(iso) = capture_placement(&trial, &v, new_id) else {
continue;
};
let norm = q0inv.after(&iso);
if targets.contains(&norm) && placed.insert(norm) {
build.push(pm);
gp = trial;
advanced = true;
break 'scan;
}
}
}
if !advanced {
return None;
}
}
Some(build)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepTileCert {
pub k: usize,
pub build: Vec<PatchMatch>,
}
impl RepTileCert {
pub fn verify<T: IsRing>(&self, base: &Rat<T>) -> bool {
let Some(placements) = replay_placements(base, &self.build) else {
return false;
};
if self.k < 2 || placements.len() != self.k * self.k {
return false;
}
let bverts = boundary_vertices::<T>(base.seq());
let polys: Vec<Vec<T>> = placements.iter().map(|g| g.tile(&bverts)).collect();
for i in 0..polys.len() {
for j in (i + 1)..polys.len() {
if !interiors_disjoint(&polys[i], &polys[j]) {
return false;
}
}
}
let Some(loop_seq) = union_boundary_seq(&polys, base.seq().len() * self.k) else {
return false;
};
cyclic_eq(&loop_seq, &scaled_seq(base.seq(), self.k))
}
}
fn mint_cert<T: IsRing>(base: &Rat<T>, k: usize, placements: &[Iso<T>]) -> Option<RepTileCert> {
let cert = RepTileCert {
k,
build: build_from_placements(base, placements)?,
};
cert.verify(base).then_some(cert)
}
pub fn reptile_cert<T: IsRing>(base: &Rat<T>, kmax: usize) -> Option<RepTileCert> {
let (k, placements) = reptile(base, kmax)?;
mint_cert(base, k, &placements)
}
pub enum RepScreen {
RepTile(RepTileCert),
No,
Inconclusive,
Anomaly(usize),
}
pub fn reptile_screen_one<T: IsRing>(base: &Rat<T>, kmax: usize, budget: usize) -> RepScreen {
let mut any_abort = false;
for k in 2..=kmax {
let (res, aborted) = fill(base, k, budget);
if let Some(placements) = res {
return match mint_cert(base, k, &placements) {
Some(cert) => RepScreen::RepTile(cert),
None => RepScreen::Anomaly(k),
};
}
any_abort |= aborted;
}
if any_abort {
RepScreen::Inconclusive
} else {
RepScreen::No
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::ZZ12;
fn rat(seq: &[i8]) -> Rat<ZZ12> {
Rat::from_slice_trusted(seq)
}
#[test]
fn triangle_is_rep4_and_rep9() {
let t = rat(&[4, 4, 4]);
assert_eq!(reptile_at(&t, 2).expect("triangle rep-4").len(), 4);
assert_eq!(reptile_at(&t, 3).expect("triangle rep-9").len(), 9);
}
#[test]
fn square_is_rep4_and_rep9() {
let s = rat(&[3, 3, 3, 3]);
assert_eq!(reptile_at(&s, 2).expect("square rep-4").len(), 4);
assert_eq!(reptile_at(&s, 3).expect("square rep-9").len(), 9);
}
#[test]
fn hexagon_is_not_a_reptile() {
let h = rat(&[2, 2, 2, 2, 2, 2]);
for k in 2..=4 {
assert!(
reptile_at(&h, k).is_none(),
"regular hexagon must not rep-tile at k={k}"
);
}
}
#[test]
fn reptile_reports_smallest_scale() {
let (k, w) = reptile(&rat(&[4, 4, 4]), 6).expect("triangle is a rep-tile");
assert_eq!(k, 2);
assert_eq!(w.len(), 4);
}
#[test]
fn l_tromino_is_rep4() {
let (k, w) = reptile(&rat(&[3, 0, 3, 3, -3, 3, 3, 0]), 4).expect("L-tromino rep-tile");
assert_eq!(k, 2);
assert_eq!(w.len(), 4);
}
#[test]
fn l_tetromino_is_rep4() {
let t = rat(&[3, 0, 3, 3, -3, 0, 3, 3, 0, 0]);
let cert = reptile_cert(&t, 6).expect("L-tetromino rep-tile");
assert_eq!(cert.k, 2);
assert!(cert.verify(&t), "rep-4 tiling verifies exactly");
}
#[test]
fn t_tetromino_is_rep16() {
let t = rat(&[3, 0, 0, 3, 3, -3, 3, 3, -3, 3]);
let cert = reptile_cert(&t, 6).expect("T-tetromino rep-tile");
assert_eq!(cert.k, 4);
assert_eq!(cert.build.len(), 15); assert!(
cert.verify(&t),
"the rep-16 tiling verifies exactly (no gap/overlap)"
);
}
#[test]
fn build_recipe_round_trips_the_witness() {
use crate::classify::grow::replay_placements;
for seq in [
&[4, 4, 4][..],
&[3, 3, 3, 3][..],
&[3, 0, 3, 3, -3, 3, 3, 0][..],
] {
let base = rat(seq);
let (_k, placements) = reptile(&base, 3).expect("rep-tile");
let build = build_from_placements(&base, &placements).expect("build recipe");
assert_eq!(
build.len(),
placements.len() - 1,
"one glue per non-root copy"
);
let root_inv = placements[0].inv();
let want: HashSet<Iso<ZZ12>> = placements.iter().map(|g| root_inv.after(g)).collect();
let got: HashSet<Iso<ZZ12>> = replay_placements(&base, &build)
.expect("replay")
.into_iter()
.collect();
assert_eq!(
got, want,
"replayed build == the witness placements (seq {seq:?})"
);
}
}
#[test]
fn zz10_penrose_rhombi_are_rep4() {
use crate::cyclotomic::ZZ10;
for seq in [&[4, 1, 4, 1][..], &[3, 2, 3, 2][..]] {
let base = Rat::<ZZ10>::from_slice_trusted(seq);
let cert = reptile_cert(&base, 4).expect("rhombus is a rep-tile");
assert_eq!(cert.k, 2, "a parallelogram is rep-4");
assert!(cert.verify(&base), "verifies exactly");
}
}
#[test]
fn reptile_cert_mints_verifies_and_serdes() {
let base = rat(&[4, 4, 4]);
let cert = reptile_cert(&base, 6).expect("triangle rep-tile cert");
assert_eq!(cert.k, 2);
assert_eq!(cert.build.len(), 3); assert!(cert.verify(&base), "cert self-verifies");
let json = serde_json::to_string(&cert).unwrap();
let back: RepTileCert = serde_json::from_str(&json).unwrap();
assert!(back.verify(&base), "cert survives serde round-trip");
assert!(
reptile_cert(&rat(&[2, 2, 2, 2, 2, 2]), 4).is_none(),
"hexagon has no cert"
);
let bad = RepTileCert {
k: 3,
build: cert.build.clone(),
};
assert!(!bad.verify(&base), "wrong-k cert rejected");
}
#[test]
fn verify_rejects_wrong_tile_count() {
let tri = rat(&[4, 4, 4]);
let cert = reptile_cert(&tri, 6).expect("triangle cert"); let short = RepTileCert {
k: cert.k,
build: cert.build[..cert.build.len() - 1].to_vec(), };
assert!(
!short.verify(&tri),
"3 copies cannot fill the k=2 region (needs 4)"
);
}
}