use std::sync::Arc;
use crate::cyclotomic::IsRing;
use crate::geom::matches::{EdgeRange, PatchMatch, Segment, TileMatch};
use crate::geom::patch::{BasicPatch, GlueDelta, Patch};
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
use crate::geom::vertices::EdgeInfo;
#[inline]
pub(crate) fn peer_offset(s: &EdgeRange, i: usize, m: usize) -> usize {
(s.start_offset + (s.len - 1 - i)) % m
}
#[inline]
pub(crate) fn partner_offset(
owner: &EdgeRange,
peer: &EdgeRange,
x: usize,
m: usize,
) -> Option<usize> {
let i = (x + m - owner.start_offset % m) % m;
(i < owner.len).then(|| peer_offset(peer, i, m))
}
#[inline]
fn glue_new_range(b_start: usize, p: usize, q: usize, m: usize) -> EdgeRange {
EdgeRange::new((b_start + 2 * m - 1 - p - (q - 1)) % m, q)
}
#[derive(Clone)]
pub struct WithAdjacency<P> {
inner: P,
shape: Vec<usize>,
adj: Vec<Vec<TileMatch>>,
}
impl<P> WithAdjacency<P> {
pub fn num_tiles(&self) -> usize {
self.shape.len()
}
pub fn neighbors(&self, id: usize) -> &[TileMatch] {
&self.adj[id]
}
pub fn degree(&self, id: usize) -> usize {
self.adj[id].len()
}
pub fn adj(&self) -> &[Vec<TileMatch>] {
&self.adj
}
pub fn shape(&self) -> &[usize] {
&self.shape
}
fn push_node(&mut self, shape: usize) -> usize {
self.shape.push(shape);
self.adj.push(Vec::new());
self.shape.len() - 1
}
fn push_edge(&mut self, tm: TileMatch) {
self.adj[tm.a.tile_id].push(tm);
self.adj[tm.b.tile_id].push(TileMatch::new(tm.b, tm.a));
}
}
impl<T: IsRing, P: Patch<T>> Patch<T> for WithAdjacency<P> {
fn angles(&self) -> &[i8] {
self.inner.angles()
}
fn edges(&self) -> &[EdgeInfo] {
self.inner.edges()
}
fn patch_tile_ids(&self) -> &[usize] {
self.inner.patch_tile_ids()
}
fn boundary_positions(&self) -> &[T] {
self.inner.boundary_positions()
}
fn next_tile_id(&self) -> usize {
self.inner.next_tile_id()
}
fn tileset(&self) -> &Arc<TileSet<T>> {
self.inner.tileset()
}
fn get_matches_in_edge_range(&self, start_edge: usize, end_edge: usize) -> Vec<PatchMatch> {
self.inner.get_matches_in_edge_range(start_edge, end_edge)
}
fn get_matches_touching_vertex(&self, vertex_index: usize) -> Vec<PatchMatch> {
self.inner.get_matches_touching_vertex(vertex_index)
}
fn add_tile(&mut self, pm: &PatchMatch) -> Option<GlueDelta> {
let new_id = self.inner.next_tile_id();
let m_new = self.inner.tileset().rat(pm.b.tile_id).len();
let edges = tile_match_runs(
self.inner.edges(),
self.inner.patch_tile_ids(),
pm,
m_new,
new_id,
);
let delta = self.inner.add_tile(pm)?; debug_assert_eq!(new_id, self.num_tiles(), "node ids track patch tile ids");
self.push_node(pm.b.tile_id);
for tm in edges {
self.push_edge(tm);
}
Some(delta)
}
}
impl<T: IsRing> WithAdjacency<BasicPatch<T>> {
pub fn single_tile(tileset: Arc<TileSet<T>>, tile_id: usize) -> Self {
let inner = BasicPatch::single_tile(tileset, tile_id);
let mut wa = WithAdjacency {
inner,
shape: Vec::new(),
adj: Vec::new(),
};
wa.push_node(tile_id); wa
}
pub fn get_all_matches(&self) -> Vec<PatchMatch> {
self.inner.get_all_matches()
}
pub fn to_rat(&self) -> Rat<T> {
self.inner.to_rat()
}
pub fn to_recipe(&self) -> Option<Vec<PatchMatch>> {
let nodes: Vec<usize> = (0..self.num_tiles()).collect();
let base = self.tileset().rat(0).clone();
assemble(self.shape(), self.adj(), &nodes, &base).map(|(build, _, _)| build)
}
}
pub type IPatch<T> = WithAdjacency<BasicPatch<T>>;
pub(crate) fn seed_boundary(shape: usize, m: usize) -> (Vec<EdgeInfo>, Vec<usize>) {
let edges = (0..m)
.map(|i| EdgeInfo {
tile_type_id: shape,
canon_offset: i,
})
.collect();
(edges, vec![0usize; m])
}
pub(crate) fn tile_match_runs(
old_edges: &[crate::geom::vertices::EdgeInfo],
old_ptids: &[usize],
pm: &PatchMatch,
m_new: usize,
new_id: usize,
) -> Vec<TileMatch> {
let n = old_edges.len();
let mlen = pm.len();
let a_start = pm.a_range.start_offset;
let b_start = pm.b.range.start_offset;
let mut out = Vec::new();
let mut p = 0;
while p < mlen {
let owner = old_ptids[(a_start + p) % n];
let mut q = 1;
while p + q < mlen && old_ptids[(a_start + p + q) % n] == owner {
q += 1;
}
let owner_off0 = old_edges[(a_start + p) % n].canon_offset;
debug_assert!(
(0..q)
.all(|t| old_edges[(a_start + p + t) % n].canon_offset == (owner_off0 + t) % m_new),
"owner run is ascending-contiguous",
);
out.push(TileMatch::new(
Segment::new(new_id, glue_new_range(b_start, p, q, m_new)),
Segment::new(owner, EdgeRange::new(owner_off0, q)),
));
p += q;
}
out
}
pub(crate) fn assemble<T: IsRing>(
shape: &[usize],
adj: &[Vec<TileMatch>],
nodes: &[usize],
base: &Rat<T>,
) -> Option<(Vec<PatchMatch>, BasicPatch<T>, Vec<usize>)> {
debug_assert!(
nodes.iter().all(|&n| shape.get(n) == Some(&0)),
"assemble: monotile pipeline expects every node to be shape 0"
);
let m = base.len();
let ts = TileSet::single(base.clone());
let seed = BasicPatch::single_tile(ts, 0);
let (synth_edges, synth_ptids) = seed_boundary(0, m);
let hits = |created: &[TileMatch], parent_tile: usize, x: usize, y: usize| {
created.iter().any(|c| {
c.b.tile_id == parent_tile && partner_offset(&c.b.range, &c.a.range, x, m) == Some(y)
})
};
let mut order = vec![nodes[0]]; let mut placed: std::collections::HashSet<usize> = std::collections::HashSet::from([nodes[0]]);
let mut build: Vec<PatchMatch> = Vec::new();
let mut gp: Option<BasicPatch<T>> = None;
while placed.len() < nodes.len() {
let mut next: Option<(usize, BasicPatch<T>, PatchMatch)> = None;
'search: for &child in nodes {
if placed.contains(&child) {
continue;
}
for e in &adj[child] {
let parent = e.b.tile_id; if !placed.contains(&parent) {
continue;
}
let parent_tile = order.iter().position(|&n| n == parent)?;
let x = e.b.range.start_offset;
let y = peer_offset(&e.a.range, 0, m);
let cands: Vec<PatchMatch> = match &gp {
None => seed
.get_all_matches()
.iter()
.filter(|pm| pm.a_range.start_offset == x)
.cloned()
.collect(),
Some(g) => match (0..g.len()).find(|&i| {
g.patch_tile_ids()[i] == parent_tile && g.edges()[i].canon_offset == x
}) {
Some(pos) => g.get_matches_in_edge_range(pos, pos),
None => continue, },
};
for pm in &cands {
let created = match &gp {
None => tile_match_runs(&synth_edges, &synth_ptids, pm, m, 1),
Some(g) => {
tile_match_runs(g.edges(), g.patch_tile_ids(), pm, m, g.next_tile_id())
}
};
if !hits(&created, parent_tile, x, y) {
continue;
}
let trial = match &gp {
None => seed.with_tile(pm),
Some(g) => {
let mut g2 = g.clone();
g2.add_tile(pm).is_some().then_some(g2)
}
};
if let Some(g2) = trial {
next = Some((child, g2, *pm));
break 'search;
}
}
}
}
let Some((child, g2, pm)) = next else {
return None; };
gp = Some(g2);
build.push(pm);
order.push(child);
placed.insert(child);
}
Some((build, gp?, order))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::grow::capture_placement;
use crate::classify::mint::connected_transversal;
use crate::cyclotomic::ZZ12;
use crate::cyclotomic::traits::SymNum;
use crate::geom::iso::Iso;
use crate::geom::patch::{BasicPatch, trace_boundary_positions};
use crate::geom::rat::Rat;
use crate::geom::tiles;
use crate::geom::tileset::TileSet;
use std::collections::HashSet;
use std::collections::VecDeque;
#[test]
fn to_recipe_round_trips_a_grown_graph() {
use crate::classify::grow::{grow_coronas_build, replay_recipe};
let seq: &[i8] = &[3, 2, 0, 2, -3, 2, 3, 2, -3, 2, 3, -2, 3, -2]; let (_pls, ig) = grow_coronas_build::<ZZ12>(seq, 2).expect("grew a patch");
let base = Rat::<ZZ12>::from_slice_trusted(seq);
let recipe = ig.to_recipe().expect("graph assembles into a recipe");
assert_eq!(
recipe.len(),
ig.num_tiles() - 1,
"one glue per non-seed tile"
);
let recon = replay_recipe(&base, &recipe, |_, _| true).expect("recipe replays");
assert_eq!(
recon.to_rat(),
ig.to_rat(),
"round-trip rebuilds the identical patch"
);
}
fn bfs_from(adj: &[Vec<TileMatch>], root: usize) -> Vec<usize> {
let mut seen = vec![false; adj.len()];
let mut order = Vec::new();
let mut q = VecDeque::from([root]);
seen[root] = true;
while let Some(u) = q.pop_front() {
order.push(u);
for e in &adj[u] {
let v = e.b.tile_id;
if !seen[v] {
seen[v] = true;
q.push_back(v);
}
}
}
order
}
fn components(adj: &[Vec<TileMatch>]) -> Vec<usize> {
let mut comp = vec![usize::MAX; adj.len()];
let mut c = 0;
for s in 0..adj.len() {
if comp[s] != usize::MAX {
continue;
}
let mut q = VecDeque::from([s]);
comp[s] = c;
while let Some(u) = q.pop_front() {
for e in &adj[u] {
let v = e.b.tile_id;
if comp[v] == usize::MAX {
comp[v] = c;
q.push_back(v);
}
}
}
c += 1;
}
comp
}
fn grow_full(
seq: &[i8],
target: usize,
) -> (WithAdjacency<BasicPatch<ZZ12>>, Vec<Iso<ZZ12>>, Vec<ZZ12>) {
let ts = TileSet::single(Rat::<ZZ12>::from_slice_trusted(seq));
let verts: Vec<ZZ12> = trace_boundary_positions::<ZZ12>(seq)[..seq.len()].to_vec();
let mut wa = WithAdjacency::single_tile(ts.clone(), 0);
let first = wa
.get_all_matches()
.first()
.cloned()
.expect("a first match");
wa.add_tile(&first).expect("seed grows");
let mut placements = vec![
capture_placement(&wa, &verts, 0).expect("seed placement"),
capture_placement(&wa, &verts, 1).expect("tile 1 placement"),
];
while wa.next_tile_id() < target {
let angles = wa.angles().to_vec();
let n = angles.len();
let mut matches = wa.get_all_matches();
if matches.is_empty() {
break;
}
matches.sort_by_key(|pm| angles[pm.a_range.start_offset % n]);
let mut glued = false;
for pm in &matches {
let new_id = wa.next_tile_id();
if wa.add_tile(pm).is_some() {
placements.push(capture_placement(&wa, &verts, new_id).expect("placement"));
glued = true;
break;
}
}
if !glued {
break;
}
}
(wa, placements, verts)
}
fn assert_edges_geometric(
g: &WithAdjacency<BasicPatch<ZZ12>>,
pls: &[Iso<ZZ12>],
base: &[ZZ12],
) {
let m = base.len();
let canon = |p: ZZ12, q: ZZ12| {
if format!("{:?}", p.xy()) < format!("{:?}", q.xy()) {
(p, q)
} else {
(q, p)
}
};
for a in 0..g.num_tiles() {
for tm in g.neighbors(a) {
assert_eq!(tm.a.tile_id, a, "edge oriented a = node");
assert_eq!(tm.a.range.len, tm.b.range.len, "equal match length");
let pa = pls[tm.a.tile_id].tile(base);
let pb = pls[tm.b.tile_id].tile(base);
let len = tm.a.range.len;
for i in 0..len {
let ao = (tm.a.range.start_offset + i) % m;
let bo = peer_offset(&tm.b.range, i, m);
let ae = canon(pa[ao], pa[(ao + 1) % m]);
let be = canon(pb[bo], pb[(bo + 1) % m]);
assert_eq!(
ae, be,
"edge {}-{}: within-run position {i} mismatch (a off {ao}, b off {bo})",
tm.a.tile_id, tm.b.tile_id
);
}
}
}
}
const ASYM: [i8; 7] = [-1, 2, 3, 1, 2, 1, 4];
fn from_edges(n: usize, edges: &[(usize, usize)]) -> Vec<Vec<TileMatch>> {
let mut adj: Vec<Vec<TileMatch>> = vec![Vec::new(); n];
for &(a, b) in edges {
let r = EdgeRange::new(0, 1);
adj[a].push(TileMatch::new(Segment::new(a, r), Segment::new(b, r)));
adj[b].push(TileMatch::new(Segment::new(b, r), Segment::new(a, r)));
}
adj
}
fn grow_keep(seq: &[i8], target: usize) -> WithAdjacency<BasicPatch<ZZ12>> {
grow_full(seq, target).0
}
fn grow_snowflake() -> WithAdjacency<BasicPatch<ZZ12>> {
let hex = Rat::<ZZ12>::from_snake_trusted(&tiles::hexagon());
let m = 6;
let ts = TileSet::single(hex);
let mut wa = WithAdjacency::single_tile(ts.clone(), 0);
let first = wa.get_all_matches().first().cloned().unwrap();
wa.add_tile(&first).unwrap();
while wa.degree(0) < 6 {
let ptids = wa.patch_tile_ids().to_vec();
let n = ptids.len();
let matches = wa.get_all_matches();
let pick = matches
.iter()
.find(|pm| {
(0..pm.a_range.len).any(|t| ptids[(pm.a_range.start_offset + t) % n] == 0)
})
.or_else(|| matches.first())
.cloned()
.unwrap();
assert!(wa.add_tile(&pick).is_some(), "flower glue");
}
assert_eq!(wa.num_tiles(), 7, "flower is 7 tiles");
let mut done: HashSet<usize> = HashSet::new();
while wa.num_tiles() < 13 {
let cands = wa.get_all_matches();
let mut glued = false;
for pm in &cands {
let created =
tile_match_runs(wa.edges(), wa.patch_tile_ids(), pm, m, wa.next_tile_id());
let owners: HashSet<usize> = created.iter().map(|c| c.b.tile_id).collect();
if owners.len() == 1 {
let owner = *owners.iter().next().unwrap();
if (1..=6).contains(&owner)
&& !done.contains(&owner)
&& wa.add_tile(pm).is_some()
{
done.insert(owner);
glued = true;
break;
}
}
}
if !glued {
break;
}
}
wa
}
fn graph_of_build(build: &[PatchMatch], base: &Rat<ZZ12>) -> WithAdjacency<BasicPatch<ZZ12>> {
let ts = TileSet::single(base.clone());
let mut wa = WithAdjacency::single_tile(ts, 0);
wa.add_tile(&build[0]).expect("seed");
for pm in &build[1..] {
assert!(wa.add_tile(pm).is_some(), "replay glue");
}
wa
}
fn check_subset_of(g: &WithAdjacency<BasicPatch<ZZ12>>, seq: &[i8], subset: &[usize]) {
let base = Rat::<ZZ12>::from_slice_trusted(seq);
let (build, gp2, order) = assemble(g.shape(), g.adj(), subset, &base)
.unwrap_or_else(|| panic!("assemble {subset:?} must succeed"));
assert_eq!(
gp2.next_tile_id(),
subset.len(),
"{subset:?}: all tiles placed"
);
assert_eq!(build.len(), subset.len() - 1);
let sub: HashSet<usize> = subset.iter().copied().collect();
let pair = |a: usize, b: usize| if a < b { (a, b) } else { (b, a) };
let mut want: HashSet<(usize, usize)> = HashSet::new();
for &a in subset {
for e in g.neighbors(a) {
let b = e.b.tile_id;
if sub.contains(&b) {
want.insert(pair(a, b));
}
}
}
let g2 = graph_of_build(&build, &base);
let mut got: HashSet<(usize, usize)> = HashSet::new();
for u in 0..g2.num_tiles() {
for e in g2.neighbors(u) {
got.insert(pair(order[u], order[e.b.tile_id]));
}
}
assert_eq!(
got, want,
"{subset:?}: reconstructed topology matches induced subgraph"
);
}
fn check_subset(g: &WithAdjacency<BasicPatch<ZZ12>>, subset: &[usize]) {
check_subset_of(
g,
Rat::<ZZ12>::from_snake_trusted(&tiles::hexagon()).seq(),
subset,
);
}
fn tetro() -> Vec<i8> {
Rat::<ZZ12>::try_from(&tiles::tetromino_L::<ZZ12>())
.unwrap()
.seq()
.to_vec()
}
#[test]
fn tetromino_graph_edges_geometric() {
let seq = tetro();
let (g, pls, base) = grow_full(&seq, 16);
assert!(
g.num_tiles() >= 10,
"grew a dense patch (got {})",
g.num_tiles()
);
assert_edges_geometric(&g, &pls, &base);
}
#[test]
fn tetromino_subsets_assemble() {
let seq = tetro();
let wa = grow_keep(&seq, 16);
let n = wa.num_tiles();
check_subset_of(&wa, &seq, &(0..n).collect::<Vec<_>>()); check_subset_of(&wa, &seq, &[0, 1, 2]); let line: Vec<usize> = bfs_from(wa.adj(), 0).into_iter().take(5).collect();
check_subset_of(&wa, &seq, &line);
}
#[test]
fn snowflake_structure() {
let wa = grow_snowflake();
assert_eq!(wa.num_tiles(), 13, "center + 6 petals + 6 outer");
assert_eq!(wa.degree(0), 6, "center touches 6 petals");
for o in 7..13 {
let nbrs: HashSet<usize> = wa.neighbors(o).iter().map(|e| e.b.tile_id).collect();
assert_eq!(nbrs.len(), 1, "outer hex {o} touches one tile");
assert!(
(1..=6).contains(nbrs.iter().next().unwrap()),
"outer hex on a petal"
);
}
}
#[test]
fn snowflake_subsets_assemble() {
let wa = grow_snowflake();
check_subset(&wa, &[0, 1, 2, 3, 4, 5, 6]);
check_subset(&wa, &[0, 1, 2]);
let p1_outer = (7..13)
.find(|&o| wa.neighbors(o)[0].b.tile_id == 1)
.unwrap();
check_subset(&wa, &[1, p1_outer]);
let p4_outer = (7..13)
.find(|&o| wa.neighbors(o)[0].b.tile_id == 4)
.unwrap();
check_subset(&wa, &[0, 1, 4, p1_outer, p4_outer]);
}
#[test]
fn assemble_reconstructs_patch() {
let hex = Rat::<ZZ12>::from_snake_trusted(&tiles::hexagon());
let tri = Rat::<ZZ12>::from_snake_trusted(&tiles::triangle());
for seq in [&ASYM[..], hex.seq(), tri.seq()] {
let wa = grow_keep(seq, 12);
let n = wa.num_tiles();
assert!(n >= 6, "grew a real patch (got {n})");
let base = Rat::<ZZ12>::from_slice_trusted(seq);
let nodes: Vec<usize> = (0..n).collect();
let (build, gp2, _order) = assemble(wa.shape(), wa.adj(), &nodes, &base)
.unwrap_or_else(|| panic!("{seq:?}: assemble must succeed"));
eprintln!(
"{seq:?}: n={n} build={} wa.next_tile_id={} gp2.next_tile_id={}",
build.len(),
wa.next_tile_id(),
gp2.next_tile_id()
);
assert_eq!(build.len(), n - 1, "{seq:?}: one glue per non-seed tile");
assert_eq!(gp2.next_tile_id(), n, "{seq:?}: assembled all tiles");
assert_eq!(
gp2.to_rat(),
wa.to_rat(),
"{seq:?}: assembled patch identical to original"
);
}
}
#[test]
fn geometric_edges_asymmetric() {
let (g, pls, base) = grow_full(&ASYM, 14);
assert!(
g.num_tiles() >= 8,
"grew a real patch (got {})",
g.num_tiles()
);
assert_edges_geometric(&g, &pls, &base);
assert!(components(g.adj()).iter().all(|&c| c == 0), "connected");
}
#[test]
fn geometric_edges_real_tiles() {
for seq in [
&[-2i8, -1, 2, 5, -2, 1, 2, 1, 2, 4][..],
&[-3i8, 1, 3, -2, 4, 3, -2, 1, 2, 5][..],
] {
let (g, pls, base) = grow_full(seq, 16);
assert!(
g.num_tiles() >= 10,
"{seq:?}: grew a real patch (got {})",
g.num_tiles()
);
assert_edges_geometric(&g, &pls, &base);
}
}
fn grow_flower() -> WithAdjacency<BasicPatch<ZZ12>> {
let ts = TileSet::single(Rat::<ZZ12>::from_snake_trusted(&tiles::hexagon()));
let mut wa = WithAdjacency::single_tile(ts.clone(), 0);
let first = wa.get_all_matches().first().cloned().unwrap();
wa.add_tile(&first).unwrap();
while wa.next_tile_id() < 7 {
let ptids = wa.patch_tile_ids().to_vec();
let n = ptids.len();
let touches_center = |pm: &PatchMatch| {
(0..pm.a_range.len).any(|t| ptids[(pm.a_range.start_offset + t) % n] == 0)
};
let matches = wa.get_all_matches();
let pick = matches
.iter()
.find(|pm| touches_center(pm))
.or_else(|| matches.first())
.cloned()
.expect("a match to extend the flower");
assert!(wa.add_tile(&pick).is_some(), "flower glue succeeds");
}
wa
}
#[test]
fn hexagon_flower_graph() {
let g = grow_flower();
assert_eq!(g.num_tiles(), 7, "center + 6 petals");
assert!(components(g.adj()).iter().all(|&c| c == 0), "connected");
let petals: HashSet<usize> = g.neighbors(0).iter().map(|e| e.b.tile_id).collect();
assert_eq!(
petals,
HashSet::from([1, 2, 3, 4, 5, 6]),
"center touches all 6 petals"
);
let petal_nbrs = |p: usize| -> HashSet<usize> {
g.neighbors(p)
.iter()
.map(|e| e.b.tile_id)
.filter(|&v| v != 0)
.collect::<HashSet<_>>()
};
for p in 1..=6 {
assert_eq!(petal_nbrs(p).len(), 2, "petal {p} has two petal-neighbours");
}
let mut order = vec![1usize];
let mut prev = 0usize; while order.len() < 6 {
let cur = *order.last().unwrap();
let next = petal_nbrs(cur)
.into_iter()
.find(|&v| v != prev)
.expect("cycle continues");
prev = cur;
order.push(next);
}
assert!(
petal_nbrs(order[5]).contains(&order[0]),
"petals close into a cycle"
);
let uniq: HashSet<usize> = order.iter().copied().collect();
assert_eq!(uniq.len(), 6, "the six petals form one clean cycle");
}
#[test]
fn edges_recorded_both_ways() {
let (g, _pls, _base) = grow_full(&ASYM, 12);
assert!(
g.num_tiles() >= 8,
"grew a real patch (got {})",
g.num_tiles()
);
for a in 0..g.num_tiles() {
for tm in g.neighbors(a) {
assert_eq!(tm.a.tile_id, a, "edge oriented a = node");
assert_eq!(tm.a.range.len, tm.b.range.len, "equal match length");
let fwd = g
.neighbors(a)
.iter()
.filter(|u| u.b.tile_id == tm.b.tile_id)
.count();
let rev = g
.neighbors(tm.b.tile_id)
.iter()
.filter(|u| u.b.tile_id == a)
.count();
assert_eq!(
fwd, rev,
"edges {}-{} recorded equally both ways",
a, tm.b.tile_id
);
}
}
}
#[test]
fn add_tile_forwards_and_leaves_graph_clean_on_failure() {
let ts = TileSet::single(Rat::<ZZ12>::from_snake_trusted(&tiles::hexagon()));
let mut wa = WithAdjacency::single_tile(ts.clone(), 0);
let first = wa.get_all_matches().first().cloned().unwrap();
wa.add_tile(&first).expect("seed grows");
let (nodes_before, before_tiles) = (wa.num_tiles(), wa.next_tile_id());
let bogus = PatchMatch::new(
EdgeRange::new(0, 99),
Segment::new(0, EdgeRange::new(0, 99)),
);
let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
wa.add_tile(&bogus).is_some()
}))
.unwrap_or(false);
assert!(!ok, "bogus glue rejected");
assert_eq!(
wa.num_tiles(),
nodes_before,
"graph unchanged on failed glue"
);
assert_eq!(
wa.next_tile_id(),
before_tiles,
"patch unchanged on failed glue"
);
}
#[test]
fn multi_owner_glue_creates_multiple_edges() {
let (g, _pls, _base) =
grow_full(Rat::<ZZ12>::from_snake_trusted(&tiles::hexagon()).seq(), 7);
let high = (1..g.num_tiles()).filter(|&i| g.degree(i) >= 3).count();
assert!(
high >= 1,
"closing the flower must produce multi-owner glues"
);
}
#[test]
fn connected_transversal_synthetic() {
let adj = from_edges(6, &[(0, 1), (1, 2), (3, 4), (4, 5), (0, 3), (1, 4), (2, 5)]);
let class = [0usize, 1, 2, 0, 1, 2];
let dom = connected_transversal(&adj, &class, 3, 0).expect("transversal exists");
assert_eq!(dom.len(), 3);
assert_eq!(dom[0], 0, "root first");
let classes: HashSet<usize> = dom.iter().map(|&i| class[i]).collect();
assert_eq!(classes, HashSet::from([0, 1, 2]), "one node per class");
let dset: HashSet<usize> = dom.iter().copied().collect();
let mut seen = HashSet::from([dom[0]]);
let mut stack = vec![dom[0]];
while let Some(u) = stack.pop() {
for e in &adj[u] {
if dset.contains(&e.b.tile_id) && seen.insert(e.b.tile_id) {
stack.push(e.b.tile_id);
}
}
}
assert_eq!(seen.len(), 3, "transversal induces a connected subgraph");
}
#[test]
fn connected_transversal_needs_bridge_class() {
let adj = from_edges(3, &[(0, 1), (1, 2)]);
let class = [0usize, 1, 2];
let dom = connected_transversal(&adj, &class, 3, 0).expect("path transversal");
assert_eq!(dom.len(), 3);
assert_eq!(
HashSet::<usize>::from_iter(dom.iter().map(|&i| class[i])),
HashSet::from([0, 1, 2])
);
}
#[test]
fn connected_transversal_missing_class_is_none() {
let adj = from_edges(3, &[(0, 1), (1, 2)]);
let class = [0usize, 1, 0];
assert!(connected_transversal(&adj, &class, 3, 0).is_none());
}
#[test]
fn components_splits_disjoint_graphs() {
let adj = from_edges(5, &[(0, 1), (1, 2), (3, 4)]);
let comp = components(&adj);
assert_eq!(comp[0], comp[1]);
assert_eq!(comp[1], comp[2]);
assert_eq!(comp[3], comp[4]);
assert_ne!(comp[0], comp[3], "two components");
}
#[test]
fn bfs_covers_component() {
let (g, _pls, _base) =
grow_full(Rat::<ZZ12>::from_snake_trusted(&tiles::hexagon()).seq(), 7);
let order = bfs_from(g.adj(), 0);
assert_eq!(
order.len(),
g.num_tiles(),
"BFS reaches every node (connected)"
);
assert_eq!(order[0], 0);
}
}