use std::collections::{HashMap, HashSet};
use crate::cyclotomic::{IsRing, Units};
use crate::geom::celltable::{FragId, FragmentAlphabet, Placement, StateAlphabet, StateId};
use crate::geom::snake::Snake;
pub trait Boundary<ZZ> {
fn add(&mut self, angle: i8) -> bool;
fn pop(&mut self);
fn angles(&self) -> &[i8];
fn is_closed(&self) -> bool;
fn offset(&self) -> ZZ;
fn direction(&self) -> i8;
fn paranoid_recheck(&self) {}
}
impl<ZZ: IsRing> Boundary<ZZ> for Snake<ZZ> {
fn add(&mut self, angle: i8) -> bool {
Snake::add(self, angle)
}
fn pop(&mut self) {
Snake::pop(self);
}
fn angles(&self) -> &[i8] {
Snake::angles(self)
}
fn is_closed(&self) -> bool {
Snake::is_closed(self)
}
fn offset(&self) -> ZZ {
Snake::offset(self)
}
fn direction(&self) -> i8 {
Snake::direction(self)
}
fn paranoid_recheck(&self) {
let angles = Snake::angles(self).to_vec();
let mut fresh: Snake<ZZ> = Snake::new();
for (i, &a) in angles.iter().enumerate() {
assert!(
fresh.add(a),
"stateful snake accepted full prefix {:?} but fresh snake \
rejected angle {a} at step {i}",
angles,
);
}
assert_eq!(
fresh.is_closed(),
Snake::is_closed(self),
"fresh snake disagrees on is_closed for {:?}",
angles
);
}
}
struct Frame<ZZ> {
cell: (i64, i64),
state: StateId,
pt: ZZ,
facing: i8,
places: (usize, usize),
vertex: Option<(i64, i64, StateId)>,
saved_angle0: Option<i8>,
}
pub struct DominoBoundary<'a, ZZ> {
st: &'a StateAlphabet<ZZ>,
fr: &'a FragmentAlphabet<ZZ>,
turn: i8,
cell: (i64, i64),
state: StateId,
pt: ZZ,
facing: i8,
angles: Vec<i8>,
occ: HashMap<(i64, i64), Vec<FragId>>,
visited: HashSet<(i64, i64, StateId)>,
frames: Vec<Frame<ZZ>>,
place_buf: Vec<Placement>,
}
impl<'a, ZZ: IsRing> DominoBoundary<'a, ZZ> {
pub fn new(st: &'a StateAlphabet<ZZ>, fr: &'a FragmentAlphabet<ZZ>) -> Self {
let origin = st.origin();
let mut visited = HashSet::new();
visited.insert((0, 0, origin));
DominoBoundary {
st,
fr,
turn: ZZ::turn(),
cell: (0, 0),
state: origin,
pt: ZZ::zero(),
facing: 0,
angles: Vec::new(),
occ: HashMap::new(),
visited,
frames: Vec::new(),
place_buf: Vec::new(),
}
}
}
impl<ZZ: IsRing> Boundary<ZZ> for DominoBoundary<'_, ZZ> {
fn add(&mut self, angle: i8) -> bool {
let abs_dir = (self.facing as i64 + angle as i64).rem_euclid(self.turn as i64);
let (next_state, delta) = self
.st
.step(self.state, abs_dir as usize)
.expect("domino alphabet radius too small (transition left the ball)");
let next_cell = (self.cell.0 + delta.0, self.cell.1 + delta.1);
let new_pt = self.pt + <ZZ as Units>::unit(abs_dir as i8);
let p0 = self.place_buf.len();
for &pl in self.fr.emit(self.state, abs_dir as usize) {
self.place_buf.push(pl);
}
let mut crossed = false;
for i in p0..self.place_buf.len() {
let (off, fid) = self.place_buf[i];
let c = (self.cell.0 + off.0, self.cell.1 + off.1);
if let Some(v) = self.occ.get(&c)
&& v.iter().any(|&g| self.fr.conflict(fid, g))
{
crossed = true;
break;
}
}
let is_origin = next_cell == (0, 0) && next_state == self.st.origin();
let revisit = !is_origin
&& self
.visited
.contains(&(next_cell.0, next_cell.1, next_state));
if crossed || revisit {
self.place_buf.truncate(p0);
return false;
}
let p1 = self.place_buf.len();
for i in p0..p1 {
let (off, fid) = self.place_buf[i];
let c = (self.cell.0 + off.0, self.cell.1 + off.1);
self.occ.entry(c).or_default().push(fid);
}
let vertex = if is_origin {
None
} else {
let v = (next_cell.0, next_cell.1, next_state);
self.visited.insert(v);
Some(v)
};
let mut frame = Frame {
cell: self.cell,
state: self.state,
pt: self.pt,
facing: self.facing,
places: (p0, p1),
vertex,
saved_angle0: None,
};
self.cell = next_cell;
self.state = next_state;
self.pt = new_pt;
self.facing = abs_dir as i8;
self.angles.push(angle);
if is_origin {
let ang_sum: i64 = self.angles.iter().map(|&a| a as i64).sum();
let target = self.turn as i64 * ang_sum.signum();
let orig = self.angles[0];
self.angles[0] = (target - (ang_sum - orig as i64)) as i8;
frame.saved_angle0 = Some(orig);
}
self.frames.push(frame);
true
}
fn pop(&mut self) {
let frame = self.frames.pop().expect("pop with no live edge");
self.angles.pop();
if let Some(orig) = frame.saved_angle0
&& !self.angles.is_empty()
{
self.angles[0] = orig;
}
let (p0, p1) = frame.places;
for i in p0..p1 {
let (off, fid) = self.place_buf[i];
let c = (frame.cell.0 + off.0, frame.cell.1 + off.1);
let v = self.occ.get_mut(&c).expect("occ cell vanished");
let pos = v.iter().rposition(|&g| g == fid).expect("frag vanished");
v.swap_remove(pos);
}
self.place_buf.truncate(p0);
if let Some(v) = frame.vertex {
self.visited.remove(&v);
}
self.cell = frame.cell;
self.state = frame.state;
self.pt = frame.pt;
self.facing = frame.facing;
}
fn angles(&self) -> &[i8] {
&self.angles
}
fn is_closed(&self) -> bool {
!self.angles.is_empty() && self.pt.is_zero()
}
fn offset(&self) -> ZZ {
self.pt
}
fn direction(&self) -> i8 {
self.facing
}
}
#[cfg(test)]
mod tests {
use crate::enumerate::run_rat_enum_seqs;
use std::collections::BTreeMap;
fn by_len(rats: &[Vec<i8>]) -> BTreeMap<usize, usize> {
let mut m = BTreeMap::new();
for s in rats {
*m.entry(s.len()).or_insert(0) += 1;
}
m
}
#[test]
fn automaton_equals_snake() {
for ring in [6u8, 10, 12] {
for n in [6usize, 8] {
let (a_rats, a) = run_rat_enum_seqs(ring, n, 1, 1, true, false, true);
let (s_rats, s) = run_rat_enum_seqs(ring, n, 1, 1, true, false, false);
assert_eq!(
a_rats.len(),
s_rats.len(),
"count mismatch ring={ring} n={n}"
);
assert_eq!(
by_len(&a_rats),
by_len(&s_rats),
"per-length mismatch ring={ring} n={n}"
);
assert_eq!(
(
a.closed,
a.intersected,
a.too_far,
a.recursed,
a.canonical_skip
),
(
s.closed,
s.intersected,
s.too_far,
s.recursed,
s.canonical_skip
),
"branch stats diverge ring={ring} n={n}"
);
}
}
}
#[test]
#[ignore = "heavy (~45 min, n=13); run with --ignored --nocapture"]
fn automaton_a316192_to_n13() {
const OEIS: &[(usize, usize)] = &[
(3, 1),
(4, 3),
(5, 4),
(6, 22),
(7, 69),
(8, 418),
(9, 2210),
(10, 14024),
(11, 89075),
(12, 597581),
(13, 4076855),
];
let (rats, _) = run_rat_enum_seqs(12, 13, 1, 1, true, false, true);
let bl = by_len(&rats);
let mut ok = true;
for &(n, want) in OEIS {
let got = bl.get(&n).copied().unwrap_or(0);
eprintln!(
" a({n}) = {got} (OEIS {want}){}",
if got == want { "" } else { " MISMATCH" }
);
ok &= got == want;
}
assert!(ok, "automaton per-length counts diverge from OEIS A316192");
assert_eq!(rats.len(), 4_780_262, "cumulative n<=13 free count");
}
}