use std::collections::HashMap;
use crate::cyclotomic::geometry::intersect;
use crate::cyclotomic::linalg::wedge_sign;
use crate::cyclotomic::{IsRing, Units};
use super::states::{StateAlphabet, StateId, cell_anchor, cell_basis, cell_of};
pub type FragId = u32;
pub type Placement = ((i64, i64), FragId);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FragShape {
VertexVertex,
VertexWall,
WallWall,
}
fn frag_shape<ZZ: IsRing>(a: &ZZ, b: &ZZ) -> FragShape {
let a_in = cell_of(a) == (0, 0);
let b_in = cell_of(b) == (0, 0);
match (a_in, b_in) {
(true, true) => FragShape::VertexVertex,
(true, false) | (false, true) => FragShape::VertexWall,
(false, false) => FragShape::WallWall,
}
}
fn diagonal_corner<ZZ: IsRing>(p: &ZZ, edge_vec: &ZZ, dx: i64, dy: i64) -> Option<(i64, i64)> {
let two = ZZ::one() + ZZ::one();
let (u, v) = cell_basis::<ZZ>();
let g2 = ZZ::from(dx) * u + ZZ::from(dy) * v; let w = g2 - two * *p;
let dsign = wedge_sign(&u, &v); let s = wedge_sign(&w, edge_vec) * if dx * dy > 0 { 1 } else { -1 } * dsign;
match s.cmp(&0) {
std::cmp::Ordering::Less => Some((dx, 0)),
std::cmp::Ordering::Greater => Some((0, dy)),
std::cmp::Ordering::Equal => None,
}
}
pub(crate) fn occupied_offsets<ZZ: IsRing>(
p: &ZZ,
edge_vec: &ZZ,
(dx, dy): (i64, i64),
) -> Vec<(i64, i64)> {
assert!(
dx.abs() <= 1 && dy.abs() <= 1,
"unit edge spans >1 cell (dx={dx}, dy={dy}): cell basis is not near-orthogonal, \
so the 3-cell decomposition is unsound -- restore a superset fallback"
);
if dx == 0 && dy == 0 {
vec![(0, 0)] } else if dx == 0 || dy == 0 {
vec![(0, 0), (dx, dy)] } else {
match diagonal_corner(p, edge_vec, dx, dy) {
Some(corner) => vec![(0, 0), corner, (dx, dy)],
None => vec![(0, 0), (dx, dy)],
}
}
}
#[derive(Clone, Debug)]
pub struct FragmentAlphabet<ZZ> {
edges: Vec<(ZZ, ZZ)>,
shape: Vec<FragShape>,
emit: Vec<Vec<Vec<Placement>>>,
conflict: Vec<bool>,
n: usize,
turn: usize,
}
impl<ZZ: IsRing> FragmentAlphabet<ZZ> {
pub fn build(states: &StateAlphabet<ZZ>) -> Self {
let turn = states.turn();
let mut edges: Vec<(ZZ, ZZ)> = Vec::new();
let mut shape: Vec<FragShape> = Vec::new();
let mut index: HashMap<(ZZ, ZZ), FragId> = HashMap::new();
let mut emit: Vec<Vec<Vec<Placement>>> = vec![vec![Vec::new(); turn]; states.len()];
for s in states.states() {
let p = states.rep(s);
for (d, cell) in emit[s as usize].iter_mut().enumerate() {
let Some((_, delta)) = states.step(s, d) else {
continue; };
let u = <ZZ as Units>::unit(d as i8);
let q = p + u;
for off in occupied_offsets(&p, &u, delta) {
let ov: ZZ = cell_anchor(off);
let rel = (p - ov, q - ov);
let fid = *index.entry(rel).or_insert_with(|| {
let id = edges.len() as FragId;
edges.push(rel);
shape.push(frag_shape(&rel.0, &rel.1));
id
});
cell.push((off, fid));
}
}
}
let n = edges.len();
let mut conflict = vec![false; n * n];
for i in 0..n {
for j in (i + 1)..n {
if intersect(&edges[i], &edges[j]) {
conflict[i * n + j] = true;
conflict[j * n + i] = true;
}
}
}
Self {
edges,
shape,
emit,
conflict,
n,
turn,
}
}
pub fn len(&self) -> usize {
self.n
}
pub fn is_empty(&self) -> bool {
self.n == 0
}
pub fn turn(&self) -> usize {
self.turn
}
pub fn edge(&self, f: FragId) -> (ZZ, ZZ) {
self.edges[f as usize]
}
pub fn shape(&self, f: FragId) -> FragShape {
self.shape[f as usize]
}
pub fn conflict(&self, a: FragId, b: FragId) -> bool {
self.conflict[a as usize * self.n + b as usize]
}
pub fn emit(&self, s: StateId, d: usize) -> &[Placement] {
&self.emit[s as usize][d]
}
pub fn num_conflicts(&self) -> usize {
self.conflict.iter().filter(|&&c| c).count() / 2
}
pub fn shape_counts(&self) -> (usize, usize, usize) {
let mut c = (0, 0, 0);
for &sh in &self.shape {
match sh {
FragShape::VertexVertex => c.0 += 1,
FragShape::VertexWall => c.1 += 1,
FragShape::WallWall => c.2 += 1,
}
}
c
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::{ZZ6, ZZ10, ZZ12};
fn build(radius: u32) -> (StateAlphabet<ZZ12>, FragmentAlphabet<ZZ12>) {
let states = StateAlphabet::<ZZ12>::build(radius);
let frags = FragmentAlphabet::build(&states);
(states, frags)
}
fn coverage_and_span<ZZ: IsRing>(radius: u32) -> (i64, i64) {
let st = StateAlphabet::<ZZ>::build(radius);
let (u, v) = cell_basis::<ZZ>();
let (ux, uy) = u.xy();
let (vx, vy) = v.xy();
let det = ux * vy - uy * vx; let eps = 1e-6;
let (mut mdx, mut mdy) = (0i64, 0i64);
for s in st.states() {
let (px, py) = st.rep(s).xy();
for d in 0..st.turn() {
let Some((_, (dx, dy))) = st.step(s, d) else {
continue;
};
mdx = mdx.max(dx.abs());
mdy = mdy.max(dy.abs());
let e = <ZZ as Units>::unit(d as i8);
let (ex, ey) = e.xy();
let listed: std::collections::HashSet<(i64, i64)> =
occupied_offsets(&st.rep(s), &e, (dx, dy))
.into_iter()
.collect();
let samples = 2048;
for i in 1..samples {
let t = i as f64 / samples as f64;
let (zx, zy) = (px + t * ex, py + t * ey);
let a = (zx * vy - zy * vx) / det; let b = (ux * zy - uy * zx) / det; if (a - a.round()).abs() > 0.5 - eps || (b - b.round()).abs() > 0.5 - eps {
continue; }
let cell = (a.round() as i64, b.round() as i64);
assert!(
listed.contains(&cell),
"turn={} s={s} d={d} delta=({dx},{dy}): swept cell {cell:?} \
at t={t:.4} not covered by {listed:?}",
ZZ::turn()
);
}
}
}
(mdx, mdy)
}
#[test]
fn occupied_offsets_covers_every_swept_cell() {
for (name, (mdx, mdy)) in [
("ZZ6", coverage_and_span::<ZZ6>(8)),
("ZZ10", coverage_and_span::<ZZ10>(8)),
("ZZ12", coverage_and_span::<ZZ12>(8)),
] {
eprintln!("{name}: coverage OK; max unit-edge span |dx|<={mdx} |dy|<={mdy}");
}
}
#[test]
#[ignore = "scaling probe; run with --ignored --nocapture"]
fn scaling_probe() {
eprintln!("-- state alphabet (cheap) --");
for r in [8u32, 16, 24, 32, 40, 50, 60] {
let st = StateAlphabet::<ZZ12>::build(r);
eprintln!(" R={r:>2} (perimeter n={:>3}): states={}", 2 * r, st.len());
}
eprintln!("-- fragment alphabet + conflicts (heavy: O(|F|^2)) --");
for r in [8u32, 12, 16, 20] {
let (st, fr) = build(r);
eprintln!(
" R={r:>2} (n={:>3}): states={} |F|={} |X|={}",
2 * r,
st.len(),
fr.len(),
fr.num_conflicts()
);
}
}
#[test]
fn all_three_shapes_present() {
let (_, frags) = build(8);
let (vv, vw, ww) = frags.shape_counts();
assert!(vv > 0, "expected VertexVertex fragments");
assert!(vw > 0, "expected VertexWall fragments");
assert!(ww > 0, "expected WallWall fragments");
}
#[test]
fn emit_reconstructs_the_edge() {
let (states, frags) = build(6);
let turn = states.turn();
for s in states.states() {
let p = states.rep(s);
for d in 0..turn {
if states.step(s, d).is_none() {
assert!(frags.emit(s, d).is_empty());
continue;
}
let q = p + <ZZ12 as Units>::unit(d as i8);
for &(off, fid) in frags.emit(s, d) {
let ov = ZZ12::from(off);
let (a, b) = frags.edge(fid);
assert_eq!((a + ov, b + ov), (p, q), "s={s} d={d} off={off:?}");
}
}
}
}
#[test]
fn conflict_symmetric_and_irreflexive() {
let (_, frags) = build(6);
for i in 0..frags.len() as FragId {
assert!(!frags.conflict(i, i), "self-conflict at {i}");
for j in 0..frags.len() as FragId {
assert_eq!(frags.conflict(i, j), frags.conflict(j, i), "asym {i},{j}");
}
}
}
#[test]
fn report_sizes() {
let (states, frags) = build(8);
let (vv, vw, ww) = frags.shape_counts();
eprintln!(
"ZZ12 r=8: states={} frags={} conflicts={} shapes(vv/vw/ww)={}/{}/{}",
states.len(),
frags.len(),
frags.num_conflicts(),
vv,
vw,
ww
);
assert_eq!(states.len(), 145);
assert!(!frags.is_empty());
}
}