use crate::classify::lattice::independent_pair;
use crate::classify::tiling::Tiling;
use crate::cyclotomic::IsRing;
use crate::geom::iso::{Iso, build_orbit};
use crate::geom::patch::{boundary_vertices, trace_boundary_positions};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConwayCert {
pub cuts: [usize; 6],
}
fn edge_vectors<T: IsRing>(seq: &[i8]) -> Vec<T> {
let pos = trace_boundary_positions::<T>(seq);
let n = seq.len();
(0..n).map(|i| pos[i + 1] - pos[i]).collect()
}
fn is_palindrome<T: IsRing>(edges: &[T], s: usize, len: usize) -> bool {
let n = edges.len();
(0..len / 2).all(|k| edges[(s + k) % n] == edges[(s + len - 1 - k) % n])
}
fn is_translate_pair<T: IsRing>(edges: &[T], sa: usize, sd: usize, len: usize) -> bool {
let n = edges.len();
(0..len).all(|k| edges[(sa + k) % n] == -edges[(sd + len - 1 - k) % n])
}
pub fn conway_criterion<T: IsRing>(seq: &[i8]) -> Option<ConwayCert> {
let edges = edge_vectors::<T>(seq);
let n = edges.len();
if n == 0 {
return None;
}
for sa in 0..n {
for la in 0..=(n / 2) {
let rem = n - 2 * la; for lb in 0..=rem {
for lc in 0..=(rem - lb) {
for le in 0..=(rem - lb - lc) {
let lf = rem - lb - lc - le;
let a0 = sa;
let b0 = (a0 + la) % n;
let c0 = (b0 + lb) % n;
let d0 = (c0 + lc) % n;
let e0 = (d0 + la) % n;
let f0 = (e0 + le) % n;
if is_translate_pair(&edges, a0, d0, la)
&& is_palindrome(&edges, b0, lb)
&& is_palindrome(&edges, c0, lc)
&& is_palindrome(&edges, e0, le)
&& is_palindrome(&edges, f0, lf)
{
return Some(ConwayCert {
cuts: [a0, b0, c0, d0, e0, f0],
});
}
}
}
}
}
}
None
}
pub fn bn_criterion<T: IsRing>(seq: &[i8]) -> Vec<(T, T)> {
let edges = edge_vectors::<T>(seq);
let n = edges.len();
if n == 0 || !n.is_multiple_of(2) {
return Vec::new();
}
let h = n / 2;
let pair_ok = |s: usize, off: usize, lo: usize, len: usize| {
(0..len).all(|i| edges[(s + h + off + i) % n] == -edges[(s + lo + len - 1 - i) % n])
};
let disp = |s: usize, lo: usize, len: usize| {
(0..len).fold(T::zero(), |acc, i| acc + edges[(s + lo + i) % n])
};
let mut out: Vec<(T, T)> = Vec::new();
for s in 0..n {
for a in 0..=h {
for b in 0..=(h - a) {
let c = h - a - b;
if pair_ok(s, 0, 0, a) && pair_ok(s, a, a, b) && pair_ok(s, a + b, a + b, c) {
let (da, db, dc) = (disp(s, 0, a), disp(s, a, b), disp(s, a + b, c));
let cand = (da + db, db + dc);
if !out.contains(&cand) {
out.push(cand);
}
}
}
}
}
out
}
fn arc_len(cuts: &[usize; 6], i: usize, n: usize) -> usize {
(cuts[(i + 1) % 6] + n - cuts[i]) % n
}
fn conway_isometries<T: IsRing>(verts: &[T], cert: &ConwayCert) -> (Vec<Iso<T>>, (T, T)) {
let n = verts.len();
let c = cert.cuts;
let mut gens: Vec<Iso<T>> = Vec::new();
let mut lat: Vec<T> = Vec::new();
let half = T::turn() / 2;
if arc_len(&c, 0, n) > 0 {
let t = verts[c[4]] - verts[c[0]];
gens.push(Iso { rot: 0, shift: t });
gens.push(Iso { rot: 0, shift: -t });
lat.push(t);
}
let arcs = [(1usize, 2usize), (2, 3), (4, 5), (5, 0)]; let mut centers: Vec<T> = Vec::new();
for (idx, &(s, _e)) in arcs.iter().enumerate() {
let arc_index = [1, 2, 4, 5][idx]; if arc_len(&c, arc_index, n) == 0 {
continue; }
let start = c[s];
let end = c[(s + 1) % 6];
let center2 = verts[start] + verts[end];
gens.push(Iso {
rot: half,
shift: center2,
});
centers.push(center2);
}
for i in 0..centers.len() {
for j in (i + 1)..centers.len() {
lat.push(centers[i] - centers[j]);
}
}
let lattice = independent_pair(lat).unwrap_or((T::zero(), T::zero()));
(gens, lattice)
}
pub fn build_tiling<T: IsRing>(seq: &[i8], radius: f64, cap: usize) -> Option<Tiling<T>> {
let cert = conway_criterion::<T>(seq)?;
let verts = boundary_vertices::<T>(seq);
let (gens, lattice) = conway_isometries::<T>(&verts, &cert);
let placements = build_orbit(&verts, &gens, radius, cap);
Some(Tiling {
verts,
seq: seq.to_vec(),
lattice,
placements,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::tiling::verify_tiling;
use crate::cyclotomic::ZZ12;
use crate::geom::rat::Rat;
use crate::geom::tiles;
fn rat_of(snake: crate::geom::snake::Snake<ZZ12>) -> Rat<ZZ12> {
Rat::from_snake_trusted(&snake)
}
#[test]
fn conway_separates_tilers_from_dodecagon() {
let square = rat_of(tiles::square::<ZZ12>());
let triangle = rat_of(tiles::triangle::<ZZ12>());
let hexagon = rat_of(tiles::hexagon::<ZZ12>());
for (name, rat) in [
("square", &square),
("triangle", &triangle),
("hexagon", &hexagon),
] {
let cert = conway_criterion::<ZZ12>(rat.seq());
eprintln!("{name}: {cert:?}");
assert!(cert.is_some(), "{name} must satisfy Conway (it tiles)");
}
let dodecagon = Rat::<ZZ12>::from_snake_trusted(&tiles::dodecagon());
let cert = conway_criterion::<ZZ12>(dodecagon.seq());
eprintln!("dodecagon: {cert:?}");
assert!(
cert.is_none(),
"regular dodecagon must fail Conway (it does not tile)"
);
}
#[test]
fn rectangles_pass() {
for (name, snake) in [
("tetromino_O", tiles::tetromino_O::<ZZ12>()),
("tetromino_I", tiles::tetromino_I::<ZZ12>()),
] {
let rat = rat_of(snake);
let cert = conway_criterion::<ZZ12>(rat.seq());
eprintln!("{name}: {cert:?}");
assert!(cert.is_some(), "{name} (a rectangle) must satisfy Conway");
}
}
#[test]
fn constructed_tilings_actually_tile() {
let square = rat_of(tiles::square::<ZZ12>());
let triangle = rat_of(tiles::triangle::<ZZ12>());
let hexagon = rat_of(tiles::hexagon::<ZZ12>());
for (name, rat) in [
("square", &square),
("triangle", &triangle),
("hexagon", &hexagon),
] {
let perim = rat.seq().len() as f64;
let tiling =
build_tiling::<ZZ12>(rat.seq(), 3.0 * perim, 4000).expect("conway-positive");
let chk = verify_tiling(&tiling);
eprintln!("{name}: {} tiles, check={chk:?}", tiling.placements.len());
assert!(chk.ok(), "{name} constructed tiling must verify: {chk:?}");
}
}
#[test]
fn cert_is_well_formed() {
let square = rat_of(tiles::square::<ZZ12>());
let edges = edge_vectors::<ZZ12>(square.seq());
let n = edges.len();
let ConwayCert { cuts } = conway_criterion::<ZZ12>(square.seq()).unwrap();
let len = |i: usize| (cuts[(i + 1) % 6] + n - cuts[i]) % n;
let total: usize = (0..6).map(len).sum();
assert_eq!(total, n, "the six arcs must partition the boundary");
assert_eq!(len(0), len(3), "translate pair arcs have equal length");
assert!(
is_translate_pair(&edges, cuts[0], cuts[3], len(0)),
"A/D translate pair"
);
for i in [1usize, 2, 4, 5] {
assert!(
is_palindrome(&edges, cuts[i], len(i)),
"arc {i} centrally symmetric"
);
}
}
#[test]
fn bn_catches_bricks_conway_misses() {
use crate::cyclotomic::geometry::float::cross_f;
let brick: [i8; 14] = [-4, 0, 4, 0, 2, 2, -2, 0, 4, 2, -2, 0, 4, 2];
assert!(
conway_criterion::<ZZ12>(&brick).is_none(),
"Conway misses the brick"
);
let cands = bn_criterion::<ZZ12>(&brick);
assert!(!cands.is_empty(), "BN factorizes the brick");
assert!(
cands.iter().any(|(v1, v2)| cross_f(v1, v2).abs() > 1e-9),
"a nondegenerate lattice proposal exists"
);
let dodec = rat_of(tiles::dodecagon::<ZZ12>());
assert!(
bn_criterion::<ZZ12>(dodec.seq()).is_empty(),
"dodecagon: no BN hexagon"
);
let spectre = rat_of(tiles::spectre::<ZZ12>());
assert!(
bn_criterion::<ZZ12>(spectre.seq()).is_empty(),
"spectre: no BN hexagon"
);
}
}