use rustc_hash::FxHashSet;
use std::sync::Arc;
use crate::analysis::matchtypes::MatchTypeIndex;
use crate::cyclotomic::IsRing;
use crate::cyclotomic::geometry::intersect_unit_segments;
use crate::geom::angles;
use crate::geom::cyclic::{cyclic_arcs_overlap, cyclic_range_contains};
use crate::geom::glue;
use crate::geom::glue::junctions_glueable;
use crate::geom::grid::UnitSquareGrid;
#[cfg(test)]
use crate::geom::matches::PatchSegment;
use crate::geom::matches::{EdgeRange, Segment};
use crate::geom::rat::Rat;
use crate::geom::snake::Snake;
use crate::geom::tileset::TileSet;
use crate::geom::vertices::{
CoarseJunction, EdgeInfo, OpenVertexType, compute_junctions, compute_segments, is_junction_at,
};
use crate::stringmatch::forward_match_length;
pub use crate::geom::matches::PatchMatch;
#[derive(Clone)]
pub struct PatchSeed<T: IsRing> {
match_index: Arc<MatchTypeIndex<T>>,
tile_id: usize,
cached_matches: std::sync::OnceLock<Vec<PatchMatch>>,
}
#[derive(Clone, Default)]
struct BoundaryGrid<T: IsRing> {
positions: Vec<T>,
grid: UnitSquareGrid,
edge_data: Vec<(T, T)>,
boundary_edge_ids: Vec<usize>,
next_edge_id: usize,
}
#[derive(Clone)]
struct BoundaryAndCache {
angles: Vec<i8>,
edges: Vec<EdgeInfo>,
candidates: Option<Vec<Vec<PatchMatch>>>,
}
impl BoundaryAndCache {
fn new(angles: Vec<i8>, edges: Vec<EdgeInfo>) -> Self {
debug_assert_eq!(
angles.len(),
edges.len(),
"boundary angles/edges length mismatch"
);
Self {
angles,
edges,
candidates: None,
}
}
fn len(&self) -> usize {
self.angles.len()
}
fn angles(&self) -> &[i8] {
&self.angles
}
fn edges(&self) -> &[EdgeInfo] {
&self.edges
}
fn candidates(&self) -> Option<&[Vec<PatchMatch>]> {
self.candidates.as_deref()
}
#[allow(dead_code)]
fn fill_candidates(&mut self, c: Vec<Vec<PatchMatch>>) {
debug_assert_eq!(
c.len(),
self.angles.len(),
"candidate cache must be bucketed by boundary position"
);
self.candidates = Some(c);
}
fn replace(&mut self, angles: Vec<i8>, edges: Vec<EdgeInfo>) {
debug_assert_eq!(
angles.len(),
edges.len(),
"boundary angles/edges length mismatch"
);
self.angles = angles;
self.edges = edges;
self.candidates = None;
}
fn rotate_left(&mut self, n: usize) {
self.angles.rotate_left(n);
self.edges.rotate_left(n);
self.candidates = None;
}
}
#[derive(Clone)]
pub struct GrowingPatch<T: IsRing> {
match_index: Arc<MatchTypeIndex<T>>,
boundary_cache: BoundaryAndCache,
inner_chains: Vec<Vec<EdgeInfo>>,
boundary: BoundaryGrid<T>,
patch_tile_ids: Vec<usize>,
next_tile_id: usize,
}
fn update_inner_chains(
old_inner: &[Vec<EdgeInfo>],
old_edges: &[EdgeInfo],
pm: &PatchMatch,
new_n: usize,
old_ptids: &[usize],
) -> Vec<Vec<EdgeInfo>> {
let n = old_edges.len();
let seg_len_old = n - pm.len();
let ccw_pos = (pm.a_range.start_offset + pm.len()) % n;
let cw_end_matched = (pm.a_range.start_offset + pm.len() - 1) % n;
let mut new_inner = vec![Vec::new(); new_n];
for i in 1..seg_len_old {
new_inner[i] = old_inner[(ccw_pos + i) % n].clone();
}
let consumed_cw_ptid = old_ptids[cw_end_matched];
let ccw_survivor_ptid = old_ptids[ccw_pos];
let mut chain_ccw: Vec<EdgeInfo> = old_inner[ccw_pos].clone();
if consumed_cw_ptid != ccw_survivor_ptid {
chain_ccw.push(old_edges[cw_end_matched]);
}
let consumed_ccw_ptid = old_ptids[pm.a_range.start_offset];
let cw_survivor_ptid = old_ptids[(pm.a_range.start_offset + n - 1) % n];
let mut chain_cw: Vec<EdgeInfo> = old_inner[pm.a_range.start_offset].clone();
if consumed_ccw_ptid != cw_survivor_ptid {
chain_cw.push(old_edges[pm.a_range.start_offset]);
}
if new_n == seg_len_old {
let mut merged = chain_cw;
merged.extend(chain_ccw);
new_inner[0] = merged;
} else {
new_inner[0] = chain_ccw;
new_inner[seg_len_old] = chain_cw;
}
new_inner
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RawBoundary {
angles: Vec<i8>,
edges: Vec<EdgeInfo>,
inner_chains: Vec<Vec<EdgeInfo>>,
patch_tile_ids: Vec<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RawGlueResult {
boundary: RawBoundary,
new_junc_pos: usize,
match_len: usize,
old_survivor_len: usize,
new_tile_survivor_len: usize,
}
fn glue_tile_to_raw_boundary<T: IsRing>(
boundary: &RawBoundary,
junc_pos: usize,
target: EdgeInfo,
tileset: &TileSet<T>,
first_step: bool,
new_tile_id: usize,
) -> Option<RawGlueResult> {
let tile_seq = tileset.rat(target.tile_id).seq();
let tile_junc = target.tile_offset;
let mlen = forward_match_length(&boundary.angles, junc_pos, tile_seq, tile_junc);
glue_match_to_raw_boundary::<T>(
boundary,
&PatchMatch::new(
EdgeRange::new(junc_pos, mlen),
Segment::new(target.tile_id, EdgeRange::new(tile_junc, mlen)),
),
tileset,
first_step,
new_tile_id,
)
}
fn glue_match_to_raw_boundary<T: IsRing>(
boundary: &RawBoundary,
pm: &PatchMatch,
tileset: &TileSet<T>,
first_step: bool,
new_tile_id: usize,
) -> Option<RawGlueResult> {
let tile_seq = tileset.rat(pm.b.tile_id).seq();
let m = tile_seq.len();
let n = boundary.angles.len();
let mlen = pm.len();
if mlen == 0 || mlen > n || mlen > m {
return None;
}
debug_assert!(
forward_match_length(
&boundary.angles,
pm.a_range.start_offset,
tile_seq,
pm.b.range.start_offset,
) >= mlen,
"glue_match_to_raw_boundary: pm does not describe a real match \
(a_range.start_offset={}, b.range.start_offset={}, mlen={})",
pm.a_range.start_offset,
pm.b.range.start_offset,
mlen,
);
let seg_len_old = n - mlen;
let seg_len_new = m - mlen;
if seg_len_new == 0 {
return None;
}
let new_n = seg_len_old + seg_len_new;
let (new_edges, new_patch_tile_ids) = build_glued_edges(
&boundary.edges,
&boundary.patch_tile_ids,
pm,
m,
new_tile_id,
);
let new_inner = if first_step {
vec![vec![]; new_n]
} else {
update_inner_chains(
&boundary.inner_chains,
&boundary.edges,
pm,
new_n,
&boundary.patch_tile_ids,
)
};
let gr = glue::glue_raw_angles::<T>(
&boundary.angles,
tile_seq,
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
)?;
Some(RawGlueResult {
boundary: RawBoundary {
angles: gr.angles,
edges: new_edges,
inner_chains: new_inner,
patch_tile_ids: new_patch_tile_ids,
},
new_junc_pos: seg_len_old,
match_len: mlen,
old_survivor_len: seg_len_old,
new_tile_survivor_len: seg_len_new,
})
}
fn junction_angle_sequence<T: IsRing>(vtype: &OpenVertexType, tileset: &TileSet<T>) -> Vec<i8> {
let seed_seq = tileset.rat(vtype.cw.tile_id).seq();
let n_seed = seed_seq.len();
let junc_vertex = (vtype.cw.tile_offset + 1) % n_seed;
let mut result = vec![seed_seq[junc_vertex]];
let mut petals = vtype.inner.clone();
petals.push(vtype.ccw);
for petal in &petals {
let petal_seq = tileset.rat(petal.tile_id).seq();
let petal_angle = petal_seq[petal.tile_offset];
let prev = *result.last().unwrap();
let next = angles::normalize_angle::<T>(petal_angle + prev - T::hturn());
result.push(next);
}
result
}
fn flower_petal_glue<T: IsRing>(
mut boundary: RawBoundary,
mut junc_pos: usize,
targets: &[EdgeInfo],
tileset: &TileSet<T>,
mut first_step: bool,
next_tile_id: &mut usize,
prior_juncs: &mut [usize],
) -> Option<(RawBoundary, usize)> {
for target in targets {
let tile_id = *next_tile_id;
*next_tile_id += 1;
let n_old = boundary.angles.len();
let result = glue_tile_to_raw_boundary::<T>(
&boundary, junc_pos, *target, tileset, first_step, tile_id,
)?;
let ccw_pos = (junc_pos + result.match_len) % n_old;
for j in prior_juncs.iter_mut() {
*j = (*j + n_old - ccw_pos) % n_old;
}
first_step = false;
boundary = result.boundary;
junc_pos = result.new_junc_pos;
}
Some((boundary, junc_pos))
}
impl<T: IsRing> PatchSeed<T> {
pub fn new(tileset: Arc<TileSet<T>>, seed_tile_id: usize) -> Self {
let match_index = Arc::new(MatchTypeIndex::new(Arc::clone(&tileset)));
PatchSeed {
match_index,
tile_id: seed_tile_id,
cached_matches: std::sync::OnceLock::new(),
}
}
pub fn tile_id(&self) -> usize {
self.tile_id
}
pub fn tileset(&self) -> &Arc<TileSet<T>> {
self.match_index.tileset()
}
pub fn match_index(&self) -> &Arc<MatchTypeIndex<T>> {
&self.match_index
}
pub fn to_rat(&self) -> Rat<T> {
self.match_index.tileset().rat(self.tile_id).clone()
}
pub fn candidate_matches(&self) -> &[PatchMatch] {
self.cached_matches.get_or_init(|| {
compute_seed_matches(&self.match_index, self.match_index.tileset(), self.tile_id)
})
}
pub fn grow(self, pm: &PatchMatch) -> Option<GrowingPatch<T>> {
GrowingPatch::init_from_seed(self.match_index, self.tile_id, pm)
}
}
impl<T: IsRing> GrowingPatch<T> {
pub fn from_parts(
match_index: Arc<MatchTypeIndex<T>>,
angles: Vec<i8>,
edges: Vec<EdgeInfo>,
inner_chains: Vec<Vec<EdgeInfo>>,
patch_tile_ids: Vec<usize>,
next_tile_id: usize,
) -> Option<Self> {
if angles.is_empty()
|| angles.len() != edges.len()
|| inner_chains.len() != angles.len()
|| patch_tile_ids.len() != angles.len()
{
return None;
}
let boundary = BoundaryGrid::<T>::from_angles(&angles);
Some(GrowingPatch {
match_index,
boundary_cache: BoundaryAndCache::new(angles, edges),
inner_chains,
boundary,
patch_tile_ids,
next_tile_id,
})
}
pub fn construct_minimal_witness(
vtype: &OpenVertexType,
match_index: Arc<MatchTypeIndex<T>>,
) -> Option<(Self, usize)> {
let (patch, juncs) =
Self::construct_witness_from_vt_sequence(std::slice::from_ref(vtype), match_index)?;
Some((patch, juncs[0]))
}
pub fn construct_witness_from_vt_sequence(
vtypes: &[OpenVertexType],
match_index: Arc<MatchTypeIndex<T>>,
) -> Option<(Self, Vec<usize>)> {
if vtypes.is_empty() {
return None;
}
Self::construct_witness_from_vt_sequence_inner(vtypes, 0, &match_index)
}
fn construct_witness_from_vt_sequence_inner(
vtypes: &[OpenVertexType],
start: usize,
match_index: &Arc<MatchTypeIndex<T>>,
) -> Option<(Self, Vec<usize>)> {
let tileset = match_index.tileset();
let n_vts = vtypes.len();
let seed_id = vtypes[start].cw.tile_id;
let seed_seq = tileset.rat(seed_id).seq();
let n_seed = seed_seq.len();
let mut raw = RawBoundary {
angles: seed_seq.to_vec(),
edges: (0..n_seed)
.map(|i| EdgeInfo {
tile_id: seed_id,
tile_offset: i,
})
.collect(),
inner_chains: vec![vec![]; n_seed],
patch_tile_ids: vec![0; n_seed],
};
let mut next_tile_id = 1usize;
let mut junc_positions = Vec::new();
let vt0 = &vtypes[start];
let junc_pos = (vt0.cw.tile_offset + 1) % n_seed;
let mut all_targets = vt0.inner.clone();
all_targets.push(vt0.ccw);
let (new_raw, new_junc) = flower_petal_glue::<T>(
raw,
junc_pos,
&all_targets,
tileset.as_ref(),
true,
&mut next_tile_id,
&mut junc_positions,
)?;
if new_raw.angles[new_junc].abs() == T::hturn() {
return None;
}
raw = new_raw;
junc_positions.push(new_junc);
for step in 1..n_vts {
let k_prev = (start + step - 1) % n_vts;
let k = (start + step) % n_vts;
let vt = &vtypes[k];
let vt_prev = &vtypes[k_prev];
let tile_size = tileset.rat(vt_prev.ccw.tile_id).seq().len();
let offset = ((vt.cw.tile_offset as isize - vt_prev.ccw.tile_offset as isize
+ tile_size as isize) as usize)
% tile_size
+ 1;
let prev_junc = junc_positions[step - 1];
let n = raw.edges.len();
let junc_k = (prev_junc + offset) % n;
let angle_seq = junction_angle_sequence(vt, tileset.as_ref());
let mut all_targets = vt.inner.clone();
all_targets.push(vt.ccw);
let boundary_angle = raw.angles[junc_k];
let skip = angle_seq
.iter()
.position(|&a| a == boundary_angle)
.unwrap_or(0);
let targets = &all_targets[skip..];
if targets.is_empty() {
junc_positions.push(junc_k);
continue;
}
let (new_raw, new_junc) = flower_petal_glue::<T>(
raw,
junc_k,
targets,
tileset.as_ref(),
false,
&mut next_tile_id,
&mut junc_positions,
)?;
if new_raw.angles[new_junc].abs() == T::hturn() {
return None;
}
raw = new_raw;
junc_positions.push(new_junc);
}
let patch = GrowingPatch::from_parts(
Arc::clone(match_index),
raw.angles,
raw.edges,
raw.inner_chains,
raw.patch_tile_ids,
next_tile_id,
)?;
Some((patch, junc_positions))
}
pub fn compute_candidates_covering_position(
match_index: &Arc<MatchTypeIndex<T>>,
angles: &[i8],
edges: &[EdgeInfo],
target: usize,
) -> Vec<PatchMatch> {
let n = angles.len();
let tileset = match_index.tileset();
let max_tile_len = tileset.rats().iter().map(|r| r.len()).max().unwrap_or(0);
let mut result: Vec<PatchMatch> = Vec::new();
let segments = compute_segments(angles, edges, tileset);
let junctions_set: FxHashSet<usize> = compute_junctions(angles, edges, tileset)
.into_iter()
.collect();
let enumr = CandidateEnumerator::new(angles, match_index);
let mut seen: CandidateSeen = FxHashSet::default();
for offset in 0..max_tile_len.min(n) {
let pos = (target + n - offset) % n;
if let Some(segment) = segments
.iter()
.find(|s| pos >= s.range.start_offset && pos < s.range.start_offset + s.range.len)
{
let tile_id = segment.tile_seg.tile_id;
let tile_offset = (segment.tile_seg.range.start_offset
+ (pos - segment.range.start_offset))
% tileset.rat(tile_id).len();
let mut tmp_by_start: Vec<Vec<PatchMatch>> = vec![Vec::new(); n];
enumr.enumerate_at_position(
pos,
tile_id,
tile_offset,
&mut seen,
&mut tmp_by_start,
);
for pm in tmp_by_start.into_iter().flatten() {
if cyclic_range_contains(pm.a_range.start_offset, pm.len(), target, n) {
result.push(pm);
}
}
}
if junctions_set.contains(&pos) {
enumr.enumerate_at_junction(
pos,
&mut seen,
|start_a, len| cyclic_range_contains(start_a, len, target, n),
|pm| result.push(pm),
);
}
}
result
}
pub fn compute_all_candidates(
match_index: &Arc<MatchTypeIndex<T>>,
angles: &[i8],
edges: &[EdgeInfo],
) -> Vec<Vec<PatchMatch>> {
let n = angles.len();
let tileset = match_index.tileset();
let mut result = vec![Vec::new(); n];
let segments = compute_segments(angles, edges, tileset);
let junctions = compute_junctions(angles, edges, tileset);
let enumr = CandidateEnumerator::new(angles, match_index);
let mut seen: CandidateSeen = FxHashSet::default();
for segment in &segments {
let tile_id = segment.tile_seg.tile_id;
let seg_len = segment.range.len;
for local_k in 0..seg_len {
let tile_offset =
(segment.tile_seg.range.start_offset + local_k) % tileset.rat(tile_id).len();
let patch_pos = segment.range.start_offset + local_k;
enumr.enumerate_at_position(
patch_pos,
tile_id,
tile_offset,
&mut seen,
&mut result,
);
}
}
for &junc_idx in &junctions {
enumr.enumerate_at_junction(
junc_idx,
&mut seen,
|_, _| true,
|pm| result[pm.a_range.start_offset].push(pm),
);
}
result
}
pub fn get_all_matches(&self) -> Vec<PatchMatch> {
match self.boundary_cache.candidates() {
Some(cbs) => cbs.iter().flatten().cloned().collect(),
None => Self::compute_all_candidates(
&self.match_index,
self.boundary_cache.angles(),
self.boundary_cache.edges(),
)
.into_iter()
.flatten()
.collect(),
}
}
pub fn get_matches_touching_vertex(&self, vertex_index: usize) -> Vec<PatchMatch> {
let n = self.boundary_cache.len();
let computed: Vec<Vec<PatchMatch>>;
let source: &[Vec<PatchMatch>] = match self.boundary_cache.candidates() {
Some(cbs) => cbs,
None => {
computed = Self::compute_all_candidates(
&self.match_index,
self.boundary_cache.angles(),
self.boundary_cache.edges(),
);
&computed
}
};
let k = self
.match_index
.tileset()
.rats()
.iter()
.map(|r| r.len())
.max()
.unwrap_or(0)
.min(n);
let mut result = Vec::new();
for offset in 0..=k {
let start = (vertex_index + n - offset) % n;
for pm in &source[start] {
if cyclic_range_contains(pm.a_range.start_offset, pm.len(), vertex_index, n) {
result.push(*pm);
}
}
}
result
}
pub fn get_matches_in_edge_range(&self, start_edge: usize, end_edge: usize) -> Vec<PatchMatch> {
let n = self.boundary_len();
if n == 0 {
return Vec::new();
}
let range_len = (end_edge + n - start_edge) % n + 1;
self.get_all_matches()
.into_iter()
.filter(|pm| {
cyclic_arcs_overlap(start_edge, range_len, pm.a_range.start_offset, pm.len(), n)
})
.collect()
}
#[cfg(test)]
pub(crate) fn ensure_candidates_materialized(&mut self) {
if self.boundary_cache.candidates().is_none() {
let c = Self::compute_all_candidates(
&self.match_index,
self.boundary_cache.angles(),
self.boundary_cache.edges(),
);
self.boundary_cache.fill_candidates(c);
}
}
pub fn num_tiles(&self) -> usize {
self.match_index.tileset().num_tiles()
}
pub fn boundary_len(&self) -> usize {
self.boundary_cache.len()
}
pub fn angles(&self) -> &[i8] {
self.boundary_cache.angles()
}
pub fn to_rat(&self) -> Rat<T> {
Rat::from_slice_unchecked(self.boundary_cache.angles())
}
pub fn tileset(&self) -> &Arc<TileSet<T>> {
self.match_index.tileset()
}
pub fn match_index(&self) -> &Arc<MatchTypeIndex<T>> {
&self.match_index
}
pub fn edges(&self) -> &[EdgeInfo] {
self.boundary_cache.edges()
}
pub fn inner_chains(&self) -> &[Vec<EdgeInfo>] {
&self.inner_chains
}
pub fn patch_tile_ids(&self) -> &[usize] {
&self.patch_tile_ids
}
pub fn next_tile_id(&self) -> usize {
self.next_tile_id
}
pub fn normalize(&mut self) -> usize {
let n = self.boundary_cache.len();
if n == 0 {
return 0;
}
let rot = crate::geom::rat::lex_min_rot(self.boundary_cache.angles());
if rot != 0 {
self.boundary_cache.rotate_left(rot);
self.inner_chains.rotate_left(rot);
self.patch_tile_ids.rotate_left(rot);
self.boundary.boundary_edge_ids.rotate_left(rot);
let last_idx = self.boundary.positions.len() - 1;
self.boundary.positions.truncate(last_idx);
self.boundary.positions.rotate_left(rot);
let first = self.boundary.positions[0];
self.boundary.positions.push(first);
}
let mut remap: rustc_hash::FxHashMap<usize, usize> = rustc_hash::FxHashMap::default();
let mut next = 0usize;
for id in &mut self.patch_tile_ids {
let new_id = *remap.entry(*id).or_insert_with(|| {
let v = next;
next += 1;
v
});
*id = new_id;
}
self.next_tile_id = next;
rot
}
pub fn junction_vertex_type_at(&self, i: usize) -> Option<OpenVertexType> {
let n = self.boundary_cache.len();
if n == 0 || i >= n || !self.is_junction(i) {
return None;
}
Some(OpenVertexType {
cw: self.boundary_cache.edges()[(i + n - 1) % n],
inner: self.inner_chains[i].clone(),
ccw: self.boundary_cache.edges()[i],
})
}
pub fn coarse_junction_at(&self, i: usize) -> Option<CoarseJunction> {
let n = self.boundary_cache.len();
if n == 0 || i >= n || !self.is_junction(i) {
return None;
}
Some(CoarseJunction {
cw_edge: self.boundary_cache.edges()[(i + n - 1) % n],
ccw_edge: self.boundary_cache.edges()[i],
angle: self.boundary_cache.angles()[i],
})
}
pub fn is_junction(&self, i: usize) -> bool {
if self.boundary_cache.edges().is_empty() {
return false;
}
is_junction_at(
self.boundary_cache.angles(),
self.boundary_cache.edges(),
self.match_index.tileset(),
i,
)
}
pub fn neighbor_junction_offsets(&self, pos: usize) -> Option<(usize, usize)> {
let n = self.boundary_cache.edges().len();
if n == 0 || pos >= n {
return None;
}
let mut j_cw = (pos + n - 1) % n;
while j_cw != pos && !self.is_junction(j_cw) {
j_cw = (j_cw + n - 1) % n;
}
let mut j_ccw = (pos + 1) % n;
while j_ccw != pos && !self.is_junction(j_ccw) {
j_ccw = (j_ccw + 1) % n;
}
let cw_offset = self.boundary_cache.edges()[j_cw].tile_offset;
let ccw_prev = (j_ccw + n - 1) % n;
let ccw_edge = self.boundary_cache.edges()[ccw_prev];
let tile_len = self.match_index.tileset().rat(ccw_edge.tile_id).len();
let ccw_offset = (ccw_edge.tile_offset + 1) % tile_len;
Some((cw_offset, ccw_offset))
}
#[cfg(test)]
pub(crate) fn tile_segments(&self) -> Vec<PatchSegment> {
if self.boundary_cache.edges().is_empty() {
return vec![];
}
compute_segments(
self.boundary_cache.angles(),
self.boundary_cache.edges(),
self.match_index.tileset(),
)
}
pub fn add_tile(&mut self, pm: &PatchMatch) -> bool {
self.add_tile_growing(pm)
}
fn init_from_seed(
match_index: Arc<MatchTypeIndex<T>>,
seed_id: usize,
pm: &PatchMatch,
) -> Option<Self> {
let tileset = match_index.tileset();
let seed_rat = tileset.rat(seed_id);
let n = seed_rat.seq().len();
let m = tileset.rat(pm.b.tile_id).seq().len();
let mlen = pm.len();
if mlen == 0 || mlen > n || mlen > m {
return None;
}
let (_ns, seed_len, _ne) = seed_rat.get_match(
(
pm.a_range.start_offset as i64,
pm.b.range.start_offset as i64,
),
tileset.rat(pm.b.tile_id),
);
if seed_len == 0 {
return None;
}
let seed_angles = seed_rat.seq().to_vec();
let new_angles = compute_glue_angles::<T>(&seed_angles, pm, tileset).ok()?;
let seg_len_old = n - mlen;
let seg_len_new = m - mlen;
let new_len = seg_len_old + seg_len_new;
if new_len == 0 {
return None;
}
if Snake::<T>::try_from(new_angles.as_slice()).is_err() {
return None;
}
let boundary = BoundaryGrid::<T>::from_angles(&new_angles);
let seed_old_edges: Vec<EdgeInfo> = (0..n)
.map(|i| EdgeInfo {
tile_id: seed_id,
tile_offset: i,
})
.collect();
let seed_old_ptids = vec![0usize; n];
let (edges, patch_tile_ids) = build_glued_edges(&seed_old_edges, &seed_old_ptids, pm, m, 1);
debug_assert_eq!(edges.len(), new_len);
let inner_chains = vec![vec![]; new_len];
Some(GrowingPatch {
match_index,
boundary_cache: BoundaryAndCache::new(new_angles, edges),
inner_chains,
boundary,
patch_tile_ids,
next_tile_id: 2,
})
}
fn add_tile_growing(&mut self, pm: &PatchMatch) -> bool {
let n = self.boundary_len();
let mlen = pm.len();
let m = self.match_index.tileset().rat(pm.b.tile_id).seq().len();
debug_assert!(
mlen > 0 && mlen <= n && mlen <= m,
"add_tile_growing: invalid mlen={mlen} (n={n}, m={m}); \
get_all_matches() should never produce this"
);
if mlen == 0 || mlen > n || mlen > m {
return false;
}
let new_angles = match compute_glue_angles::<T>(
self.boundary_cache.angles(),
pm,
self.match_index.tileset(),
) {
Ok(a) => a,
Err(why) => {
debug_assert!(
false,
"compute_glue_angles rejected ({why:?}); get_all_matches() \
should already filter +/-hturn glues via try_glue_precomputed"
);
return false;
}
};
let seg_len_old = n - mlen;
let seg_len_new = m - mlen;
let new_len = seg_len_old + seg_len_new;
debug_assert!(
new_len > 0,
"add_tile_growing: new_len == 0 is geometrically impossible -- \
would require placing the new tile entirely inside the existing patch"
);
if new_len == 0 {
return false;
}
let ccw_pos = (pm.a_range.start_offset + mlen) % n;
let removed_ids: Vec<usize> = (0..mlen)
.map(|i| self.boundary.boundary_edge_ids[(pm.a_range.start_offset + i) % n])
.collect();
for &id in &removed_ids {
self.boundary.unregister_edge(id);
}
let cw_junction = self.boundary.positions[pm.a_range.start_offset];
let ccw_junction = self.boundary.positions[ccw_pos];
let initial_dir = {
let prev = (pm.a_range.start_offset + n - 1) % n;
dir_of_edge::<T>(
self.boundary.positions[prev],
self.boundary.positions[pm.a_range.start_offset],
)
};
let new_tile_positions =
trace_polyline_from::<T>(cw_junction, initial_dir, &new_angles[seg_len_old..]);
debug_assert_eq!(
*new_tile_positions.last().unwrap(),
ccw_junction,
"new tile trace should end at CCW junction"
);
if !self
.boundary
.polyline_all_clear(&new_tile_positions, ccw_junction)
{
for &id in &removed_ids {
let (p1, p2) = self.boundary.edge_data[id];
self.boundary.register_edge(p1, p2, id);
}
return false;
}
let mut new_positions = Vec::with_capacity(new_len + 1);
for i in 0..=seg_len_old {
new_positions.push(self.boundary.positions[(ccw_pos + i) % n]);
}
for p in new_tile_positions.iter().take(seg_len_new + 1).skip(1) {
new_positions.push(*p);
}
let mut new_boundary_edge_ids = Vec::with_capacity(new_len);
for i in 0..seg_len_old {
new_boundary_edge_ids.push(self.boundary.boundary_edge_ids[(ccw_pos + i) % n]);
}
for i in 0..seg_len_new {
let id = self.boundary.next_edge_id;
self.boundary.next_edge_id += 1;
new_boundary_edge_ids.push(id);
let p1 = new_tile_positions[i];
let p2 = new_tile_positions[i + 1];
self.boundary.edge_data.push((p1, p2));
self.boundary.register_edge(p1, p2, id);
}
let (new_edges, new_patch_tile_ids) = build_glued_edges(
self.boundary_cache.edges(),
&self.patch_tile_ids,
pm,
m,
self.next_tile_id,
);
debug_assert_eq!(new_edges.len(), new_len);
let new_inner = update_inner_chains(
&self.inner_chains,
self.boundary_cache.edges(),
pm,
new_len,
&self.patch_tile_ids,
);
self.boundary_cache.replace(new_angles, new_edges);
self.inner_chains = new_inner;
self.boundary.positions = new_positions;
self.boundary.boundary_edge_ids = new_boundary_edge_ids;
self.patch_tile_ids = new_patch_tile_ids;
self.next_tile_id += 1;
true
}
}
fn compute_seed_matches<T: IsRing>(
match_index: &MatchTypeIndex<T>,
tileset: &TileSet<T>,
seed_tile_id: usize,
) -> Vec<PatchMatch> {
let seed = tileset.rat(seed_tile_id);
let seed_seq = seed.seq();
let n = seed_seq.len();
let mut seen: FxHashSet<(usize, usize, usize, usize)> = FxHashSet::default();
let mut matches = Vec::new();
for offset in 0..n {
for cand in match_index.candidates_starting_at(seed_tile_id, offset) {
let tile_b = tileset.rat(cand.tile_id);
let (ns, len, ne) =
seed.get_match((offset as i64, cand.range.start_offset as i64), tile_b);
if len == 0 {
continue;
}
let ns_u = ns.rem_euclid(n as i64) as usize;
let ne_u = ne.rem_euclid(tile_b.len() as i64) as usize;
if !junctions_glueable(seed_seq, ns_u, len, tile_b.seq(), ne_u) {
continue;
}
if let Ok(_glued) = seed.try_glue_precomputed((ns, len, ne), tile_b, true) {
let key = (ns_u, len, ne_u, cand.tile_id);
if seen.insert(key) {
matches.push(PatchMatch::new(
EdgeRange::new(ns_u, len),
Segment::new(cand.tile_id, EdgeRange::new(ne_u, len)),
));
}
}
}
}
matches
}
fn build_glued_edges(
old_edges: &[EdgeInfo],
old_patch_tile_ids: &[usize],
pm: &PatchMatch,
m_tile: usize,
new_tile_id: usize,
) -> (Vec<EdgeInfo>, Vec<usize>) {
let n = old_edges.len();
let mlen = pm.len();
let seg_len_old = n - mlen;
let seg_len_new = m_tile - mlen;
let ccw_pos = (pm.a_range.start_offset + mlen) % n;
let new_len = seg_len_old + seg_len_new;
let mut new_edges = Vec::with_capacity(new_len);
let mut new_ptids = Vec::with_capacity(new_len);
for i in 0..seg_len_old {
new_edges.push(old_edges[(ccw_pos + i) % n]);
new_ptids.push(old_patch_tile_ids[(ccw_pos + i) % n]);
}
if seg_len_new > 0 {
new_edges.push(EdgeInfo {
tile_id: pm.b.tile_id,
tile_offset: pm.b.range.start_offset,
});
new_ptids.push(new_tile_id);
for k in 1..seg_len_new {
new_edges.push(EdgeInfo {
tile_id: pm.b.tile_id,
tile_offset: (pm.b.range.start_offset + k) % m_tile,
});
new_ptids.push(new_tile_id);
}
}
(new_edges, new_ptids)
}
type CandidateSeen = FxHashSet<(usize, usize, usize, usize)>;
struct CandidateEnumerator<'a, T: IsRing> {
angles: &'a [i8],
rat: Rat<T>,
match_index: &'a MatchTypeIndex<T>,
n: usize,
}
impl<'a, T: IsRing> CandidateEnumerator<'a, T> {
fn new(angles: &'a [i8], match_index: &'a MatchTypeIndex<T>) -> Self {
let rat = Rat::from_slice_unchecked(angles);
let n = angles.len();
Self {
angles,
rat,
match_index,
n,
}
}
fn tileset(&self) -> &TileSet<T> {
self.match_index.tileset()
}
fn enumerate_at_position(
&self,
pos: usize,
tile_id: usize,
tile_offset: usize,
seen: &mut CandidateSeen,
result: &mut [Vec<PatchMatch>],
) {
for cand in self
.match_index
.candidates_starting_at(tile_id, tile_offset)
{
self.try_candidate(pos, cand, seen, result);
}
}
fn try_candidate(
&self,
pos: usize,
cand: &Segment,
seen: &mut CandidateSeen,
result: &mut [Vec<PatchMatch>],
) {
let tile_b = self.tileset().rat(cand.tile_id);
let (ns, len, ne) = self
.rat
.get_match((pos as i64, cand.range.start_offset as i64), tile_b);
if len == 0 {
return;
}
let ns_u = ns.rem_euclid(self.n as i64) as usize;
let ne_u = ne.rem_euclid(tile_b.len() as i64) as usize;
if !junctions_glueable(self.angles, ns_u, len, tile_b.seq(), ne_u) {
return;
}
if !seen.insert((ns_u, len, ne_u, cand.tile_id)) {
return;
}
if self
.rat
.try_glue_precomputed((ns, len, ne), tile_b, true)
.is_ok()
{
result[ns_u].push(PatchMatch::new(
EdgeRange::new(ns_u, len),
Segment::new(cand.tile_id, EdgeRange::new(ne_u, len)),
));
}
}
fn enumerate_at_junction(
&self,
pos: usize,
seen: &mut CandidateSeen,
keep: impl Fn(usize, usize) -> bool,
mut emit: impl FnMut(PatchMatch),
) {
let tileset = self.tileset();
for tile_id_b in 0..tileset.num_tiles() {
let tile_b = tileset.rat(tile_id_b);
let b_seq = tile_b.seq();
let m = b_seq.len();
for ib in 0..m {
if !junctions_glueable(self.angles, pos, 1, b_seq, ib) {
continue;
}
let (ns, len, ne) = self.rat.get_match((pos as i64, ib as i64), tile_b);
if len != 1 {
continue;
}
let ns_u = ns.rem_euclid(self.n as i64) as usize;
let ne_u = ne.rem_euclid(m as i64) as usize;
if !seen.insert((ns_u, len, ne_u, tile_id_b)) {
continue;
}
if !keep(ns_u, len) {
continue;
}
if self
.rat
.try_glue_precomputed((ns, len, ne), tile_b, true)
.is_ok()
{
emit(PatchMatch::new(
EdgeRange::new(ns_u, len),
Segment::new(tile_id_b, EdgeRange::new(ne_u, len)),
));
}
}
}
}
}
fn compute_glue_angles<T: IsRing>(
angles: &[i8],
pm: &PatchMatch,
tileset: &Arc<TileSet<T>>,
) -> Result<Vec<i8>, DegenerateGlue> {
let other_seq = tileset.rat(pm.b.tile_id).seq();
let gr = glue::glue_raw_angles::<T>(
angles,
other_seq,
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
)
.ok_or(DegenerateGlue::KeystoneHturn)?;
if let (Some(a_yx), Some(a_xy)) = (gr.a_yx, gr.a_xy)
&& (a_yx.abs() == T::hturn() || a_xy.abs() == T::hturn())
{
return Err(DegenerateGlue::JunctionHturn);
}
Ok(gr.angles)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DegenerateGlue {
KeystoneHturn,
JunctionHturn,
}
fn trace_polyline_from<T: IsRing>(start: T, initial_dir: i8, angles: &[i8]) -> Vec<T> {
let mut positions = Vec::with_capacity(angles.len() + 1);
positions.push(start);
let mut dir = initial_dir;
for &a in angles {
dir = (dir as i64 + a as i64).rem_euclid(T::turn() as i64) as i8;
let last = *positions.last().unwrap();
positions.push(last + T::unit(dir));
}
positions
}
fn trace_boundary_positions<T: IsRing>(angles: &[i8]) -> Vec<T> {
trace_polyline_from(T::zero(), 0, angles)
}
impl<T: IsRing> BoundaryGrid<T> {
fn from_angles(angles: &[i8]) -> Self {
let positions = trace_boundary_positions::<T>(angles);
let n = angles.len();
let mut grid = UnitSquareGrid::new();
let mut edge_data = Vec::with_capacity(n);
let boundary_edge_ids: Vec<usize> = (0..n).collect();
for i in 0..n {
let p1 = positions[i];
let p2 = positions[i + 1];
edge_data.push((p1, p2));
for cell in UnitSquareGrid::edge_neighborhood_of(p1, p2) {
grid.add(cell, i);
}
}
Self {
positions,
grid,
edge_data,
boundary_edge_ids,
next_edge_id: n,
}
}
fn register_edge(&mut self, p1: T, p2: T, id: usize) {
for cell in UnitSquareGrid::edge_neighborhood_of(p1, p2) {
self.grid.add(cell, id);
}
}
fn unregister_edge(&mut self, id: usize) {
let (p1, p2) = self.edge_data[id];
for cell in UnitSquareGrid::edge_neighborhood_of(p1, p2) {
self.grid.remove(cell, id);
}
}
fn check_edge_clear(&self, p1: T, p2: T, allowed_endpoint: Option<T>) -> bool {
let is_allowed = allowed_endpoint == Some(p2);
for cell in UnitSquareGrid::edge_neighborhood_of(p1, p2) {
for &id in self.grid.get(cell) {
let (x, y) = self.edge_data[id];
if !is_allowed && (p2 == x || p2 == y) {
return false;
}
if intersect_unit_segments(&(p1, p2), &(x, y)) {
return false;
}
}
}
true
}
fn polyline_all_clear(&self, positions: &[T], allowed_last_endpoint: T) -> bool {
let n_edges = positions.len().saturating_sub(1);
for i in 0..n_edges {
let allowed = (i == n_edges - 1).then_some(allowed_last_endpoint);
if !self.check_edge_clear(positions[i], positions[i + 1], allowed) {
return false;
}
}
true
}
}
fn dir_of_edge<T: IsRing>(from: T, to: T) -> i8 {
let d = to - from;
for dir in 0..T::turn() {
if T::unit(dir) == d {
return dir;
}
}
unreachable!("dir_of_edge: ({from:?} -> {to:?}) is not a unit vector");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::matchtypes::MatchTypeIndex;
use crate::cyclotomic::{ZZ4, ZZ12};
use crate::geom::snake::Snake;
use crate::geom::tiles;
use crate::geom::vertices::{ClosedVertexType, vertex_type_raw_from};
use std::collections::BTreeMap;
fn ei(tile_id: usize, tile_offset: usize) -> EdgeInfo {
EdgeInfo {
tile_id,
tile_offset,
}
}
#[test]
fn closed_vertex_type_canonicalises_to_lex_min_rotation() {
let petals = [ei(2, 0), ei(0, 1), ei(1, 2), ei(0, 3)];
let canonical = ClosedVertexType::from_cyclic(&petals);
for shift in 0..petals.len() {
let rotated: Vec<EdgeInfo> = (0..petals.len())
.map(|i| petals[(shift + i) % petals.len()])
.collect();
assert_eq!(
ClosedVertexType::from_cyclic(&rotated),
canonical,
"rotation by {shift} should canonicalise to the same VT"
);
}
let edges = canonical.edges();
for k in 1..edges.len() {
assert!(edges[0] <= edges[k]);
}
}
#[test]
fn closed_vertex_type_distinguishes_non_rotation_orderings() {
let a = ClosedVertexType::from_cyclic(&[ei(0, 0), ei(0, 1), ei(0, 2)]);
let b = ClosedVertexType::from_cyclic(&[ei(0, 0), ei(0, 2), ei(0, 1)]);
assert_ne!(a, b, "different cyclic orderings must be distinct");
}
#[test]
fn closed_vertex_type_from_open_via_closure() {
let open = OpenVertexType {
cw: ei(1, 5),
inner: vec![ei(0, 0), ei(2, 3)],
ccw: ei(1, 4),
};
let closed = ClosedVertexType::from_open_via_closure(&open);
let expected = ClosedVertexType::from_cyclic(&[ei(0, 0), ei(2, 3), ei(1, 4), ei(1, 5)]);
assert_eq!(closed, expected);
assert_eq!(closed.len(), 4);
}
fn raw_is_junction<T: IsRing>(
boundary: &RawBoundary,
tileset: &TileSet<T>,
pos: usize,
) -> bool {
is_junction_at(&boundary.angles, &boundary.edges, tileset, pos)
}
fn next_junction_on_raw_boundary<T: IsRing>(
boundary: &RawBoundary,
tileset: &TileSet<T>,
from_pos: usize,
) -> Option<usize> {
let n = boundary.angles.len();
for step in 1..=n {
let pos = (from_pos + step) % n;
if raw_is_junction(boundary, tileset, pos) {
return Some(pos);
}
}
None
}
fn square_seed() -> PatchSeed<ZZ4> {
let sq: Snake<ZZ4> = tiles::square();
let rat = Rat::try_from(&sq).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
PatchSeed::new(ts, 0)
}
fn hex_seed() -> PatchSeed<ZZ12> {
let hex: Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
PatchSeed::new(ts, 0)
}
fn grow_first<T: IsRing>(ts: Arc<TileSet<T>>) -> GrowingPatch<T> {
let seed = PatchSeed::new(ts, 0);
let pm = *seed.candidate_matches().first().expect("seed has matches");
seed.grow(&pm).expect("first glue succeeds")
}
fn build_from_glues<T: IsRing>(
seed: PatchSeed<T>,
glues: &[PatchMatch],
label: &str,
) -> GrowingPatch<T> {
let mut gp = seed
.grow(&glues[0])
.unwrap_or_else(|| panic!("{label} glue 0 failed: pm={:?}", glues[0]));
for (i, pm) in glues.iter().enumerate().skip(1) {
assert!(gp.add_tile(pm), "{label} glue {} failed: pm={:?}", i, pm);
}
gp
}
#[test]
fn hollow_hex_ring_closure_rejected() {
let first = PatchMatch::new(EdgeRange::new(1, 1), Segment::new(0, EdgeRange::new(0, 1)));
let mut gp = hex_seed().grow(&first).expect("first glue should succeed");
for step in 2..=4 {
let start_a = gp.boundary_len() - 4;
let pm = PatchMatch::new(
EdgeRange::new(start_a, 1),
Segment::new(0, EdgeRange::new(0, 1)),
);
assert!(
gp.add_tile(&pm),
"step {} glue (pm={:?}) should succeed",
step,
pm
);
}
assert_eq!(
gp.boundary_len(),
22,
"after 4 glues = 5 hexes in a C, boundary should be 22 edges"
);
let start_a = gp.boundary_len() - 4;
let closing_pm = PatchMatch::new(
EdgeRange::new(start_a, 1),
Segment::new(0, EdgeRange::new(0, 1)),
);
let ok = gp.add_tile(&closing_pm);
assert!(
!ok,
"GrowingPatch::add_tile must refuse the closing glue \
(= would build a 6-hex ring with a hex-shaped hole at \
the center, which is non-simply-connected). \
pm={:?}, current boundary_len={}",
closing_pm,
gp.boundary_len()
);
assert_eq!(
gp.boundary_len(),
22,
"rejected glue must leave state unchanged"
);
}
fn seven_hex_full_corona() -> GrowingPatch<ZZ12> {
let first = PatchMatch::new(EdgeRange::new(1, 1), Segment::new(0, EdgeRange::new(0, 1)));
let mut gp = hex_seed().grow(&first).expect("chain glue 1");
for _ in 2..=4 {
let start_a = gp.boundary_len() - 4;
let pm = PatchMatch::new(
EdgeRange::new(start_a, 1),
Segment::new(0, EdgeRange::new(0, 1)),
);
assert!(gp.add_tile(&pm), "chain glue {:?}", pm);
}
let central = gp
.get_all_matches()
.into_iter()
.find(|pm| {
pm.len() == 5 && {
let mut trial = gp.clone();
trial.add_tile(pm)
}
})
.unwrap_or_else(|| panic!("no mlen=5 candidate to fill central"));
assert!(gp.add_tile(¢ral), "central fill");
let closer = gp
.get_all_matches()
.into_iter()
.find(|pm| {
pm.len() == 3 && {
let mut trial = gp.clone();
trial.add_tile(pm) && trial.boundary_len() == 18
}
})
.unwrap_or_else(|| panic!("no mlen=3 candidate closing the corona"));
assert!(gp.add_tile(&closer), "ring closure");
gp
}
#[test]
fn construct_witness_self_intersects_with_fully_interior_tile() {
let gp = seven_hex_full_corona();
assert_eq!(gp.boundary_len(), 18);
let mut vt_seq: Vec<OpenVertexType> = Vec::new();
for i in 0..gp.boundary_len() {
if let Some(vt) = gp.junction_vertex_type_at(i) {
vt_seq.push(vt);
}
}
assert_eq!(vt_seq.len(), 6, "7-hex corona has 6 outer junctions");
for vt in &vt_seq {
assert!(
vt.inner.is_empty(),
"outer-corner junctions have empty inner -- central not captured"
);
}
let mi = Arc::clone(gp.match_index());
let (rebuilt, _) =
GrowingPatch::construct_witness_from_vt_sequence(&vt_seq, mi).expect("returns Some");
assert_eq!(
rebuilt.boundary_len(),
30,
"reconstruction places 7 tiles in a spiral (wrong) instead of 6 \
corona tiles around the central -- the central's constraint is \
missing from the vt_seq."
);
assert!(
Snake::<ZZ12>::try_from(rebuilt.angles()).is_err(),
"the spiral self-intersects -- Snake rejects, but \
construct_witness_from_vt_sequence does not validate."
);
}
#[test]
fn cyclic_range_contains_unit() {
fn brute(start: usize, len: usize, n: usize) -> std::collections::BTreeSet<usize> {
if len == 0 || n == 0 {
return std::collections::BTreeSet::new();
}
(0..=len).map(|i| (start + i) % n).collect()
}
assert!(
cyclic_range_contains(25, 1, 0, 26),
"regression: end-at-n-mod-n=0 wrap"
);
assert!(cyclic_range_contains(25, 1, 25, 26), "CW endpoint");
for n in [1, 2, 5, 13, 26] {
for start in 0..n {
for len in 0..=(n + 1) {
let want = brute(start, len, n);
for index in 0..n {
let got = cyclic_range_contains(start, len, index, n);
let expected = want.contains(&index);
assert_eq!(got, expected, "n={n} start={start} len={len} index={index}");
}
}
}
}
assert!(!cyclic_range_contains(0, 0, 0, 10), "len=0 -> false");
assert!(!cyclic_range_contains(0, 5, 0, 0), "n=0 -> false");
}
#[test]
fn cyclic_arcs_overlap_unit() {
fn brute_edges(start: usize, len: usize, n: usize) -> std::collections::BTreeSet<usize> {
if len == 0 || n == 0 {
return std::collections::BTreeSet::new();
}
(0..len).map(|i| (start + i) % n).collect()
}
fn brute_overlap(a: usize, l_a: usize, b: usize, l_b: usize, n: usize) -> bool {
let arc_a = brute_edges(a, l_a, n);
let arc_b = brute_edges(b, l_b, n);
!arc_a.is_disjoint(&arc_b)
}
for n in [1, 2, 5, 8, 13] {
for a in 0..n {
for l_a in 0..=(n + 1) {
for b in 0..n {
for l_b in 0..=(n + 1) {
let got = cyclic_arcs_overlap(a, l_a, b, l_b, n);
let want = brute_overlap(a, l_a, b, l_b, n);
assert_eq!(
got, want,
"mismatch: a={a} l_a={l_a} b={b} l_b={l_b} n={n}"
);
}
}
}
}
}
assert!(
!cyclic_arcs_overlap(0, 0, 0, 5, 10),
"empty arc never overlaps"
);
assert!(
!cyclic_arcs_overlap(0, 5, 0, 0, 10),
"empty arc never overlaps (other side)"
);
assert!(!cyclic_arcs_overlap(0, 5, 0, 5, 0), "n=0 -> false");
assert!(
cyclic_arcs_overlap(0, 10, 5, 1, 10),
"full-cycle A vs any non-empty B"
);
assert!(
cyclic_arcs_overlap(7, 5, 1, 2, 10),
"wraparound A vs interior B"
);
assert!(!cyclic_arcs_overlap(0, 3, 5, 3, 10), "disjoint interiors");
assert!(cyclic_arcs_overlap(0, 3, 2, 3, 10), "edge 2 shared");
}
#[test]
fn get_matches_in_edge_range_matches_brute_force() {
for ts in [
Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::hexagon::<ZZ12>()).unwrap(),
])),
Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre::<ZZ12>()).unwrap(),
])),
] {
let seed = PatchSeed::new(Arc::clone(&ts), 0);
let first = *seed.candidate_matches().first().expect("seed match");
let gp = seed.grow(&first).expect("seed add");
let n = gp.boundary_len();
assert!(n > 0);
let all = gp.get_all_matches();
for start in 0..n {
for end in 0..n {
let range_len = (end + n - start) % n + 1;
let want: std::collections::BTreeSet<(usize, usize, usize, usize)> = all
.iter()
.filter(|pm| {
cyclic_arcs_overlap(
start,
range_len,
pm.a_range.start_offset,
pm.len(),
n,
)
})
.map(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
})
.collect();
let got: std::collections::BTreeSet<(usize, usize, usize, usize)> = gp
.get_matches_in_edge_range(start, end)
.into_iter()
.map(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
})
.collect();
assert_eq!(
got, want,
"mismatch on n={n} start={start} end={end} range_len={range_len}"
);
}
}
}
}
#[test]
fn get_matches_in_edge_range_full_boundary_equals_all() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre::<ZZ12>()).unwrap(),
]));
let seed = PatchSeed::new(Arc::clone(&ts), 0);
let first = *seed.candidate_matches().first().unwrap();
let gp = seed.grow(&first).unwrap();
let n = gp.boundary_len();
let mut all: Vec<_> = gp.get_all_matches();
all.sort_by_key(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
});
for start in 0..n {
let end = (start + n - 1) % n;
let mut got = gp.get_matches_in_edge_range(start, end);
got.sort_by_key(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
});
assert_eq!(got, all, "full-boundary range from start={start}");
}
}
#[test]
fn junction_pair_set_is_normalize_invariant() {
for ts in [
Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::hexagon::<ZZ12>()).unwrap(),
])),
Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre::<ZZ12>()).unwrap(),
])),
] {
let seed = PatchSeed::new(Arc::clone(&ts), 0);
let first = *seed.candidate_matches().first().unwrap();
let mut gp = seed.grow(&first).unwrap();
for _step in 0..4 {
let pre_pairs = collect_pair_set(&gp);
let mut normed = gp.clone();
normed.normalize();
let post_pairs = collect_pair_set(&normed);
assert_eq!(
pre_pairs, post_pairs,
"junction pair set differs across normalize"
);
if let Some(pm) = gp.get_all_matches().into_iter().next() {
if !gp.add_tile(&pm) {
break;
}
} else {
break;
}
}
}
}
fn collect_pair_set(
patch: &GrowingPatch<ZZ12>,
) -> std::collections::BTreeSet<(OpenVertexType, OpenVertexType)> {
let n = patch.boundary_len();
let juncs: Vec<OpenVertexType> = (0..n)
.filter_map(|i| patch.junction_vertex_type_at(i))
.collect();
let k = juncs.len();
let mut out = std::collections::BTreeSet::new();
if k < 2 {
return out;
}
for j in 0..k {
out.insert((juncs[j].clone(), juncs[(j + 1) % k].clone()));
}
out
}
fn square_grid_3x3_minus_top_left_corner() -> GrowingPatch<ZZ4> {
let glues = [
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(0, 1))),
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(1, 1))),
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(1, 1))),
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(1, 1))),
PatchMatch::new(EdgeRange::new(2, 2), Segment::new(0, EdgeRange::new(1, 2))),
PatchMatch::new(EdgeRange::new(1, 2), Segment::new(0, EdgeRange::new(1, 2))),
PatchMatch::new(EdgeRange::new(1, 2), Segment::new(0, EdgeRange::new(1, 2))),
];
build_from_glues(square_seed(), &glues, "3x3-minus-corner fixture")
}
#[test]
fn reconstruct_3x3_minus_corner_from_vt_seq() {
let gp = square_grid_3x3_minus_top_left_corner();
assert_eq!(
gp.boundary_len(),
12,
"fixture: 3x3-minus-corner has 12 boundary edges"
);
let n = gp.boundary_len();
let mut corona_vt_seq: Vec<OpenVertexType> = Vec::new();
for i in 0..n {
if let Some(vt) = gp.junction_vertex_type_at(i) {
corona_vt_seq.push(vt);
}
}
assert_eq!(
corona_vt_seq.len(),
7,
"fixture: 6 tile-tile straight junctions + 1 concave notch"
);
let mi = Arc::clone(gp.match_index());
let (rebuilt, _junc_positions) =
GrowingPatch::construct_witness_from_vt_sequence(&corona_vt_seq, mi)
.expect("3x3-minus-corner vt_seq should reconstruct");
assert_eq!(
rebuilt.boundary_len(),
gp.boundary_len(),
"reconstructed boundary length should match original",
);
}
#[test]
fn build_glued_edges_keystone_len() {
let old_edges: Vec<EdgeInfo> = (0..8)
.map(|i| EdgeInfo {
tile_id: 0,
tile_offset: i,
})
.collect();
let old_ptids = vec![0usize; 8];
let pm = PatchMatch::new(EdgeRange::new(2, 4), Segment::new(1, EdgeRange::new(0, 4)));
let m_tile = 4;
let (new_edges, new_ptids) = build_glued_edges(&old_edges, &old_ptids, &pm, m_tile, 99);
assert_eq!(new_edges.len(), 4, "keystone glue: new_len == seg_len_old");
assert_eq!(new_ptids.len(), 4);
for e in &new_edges {
assert_ne!(e.tile_id, pm.b.tile_id, "no surviving petal edges");
}
}
#[test]
fn update_inner_chains_keystone_no_oob() {
let old_edges: Vec<EdgeInfo> = (0..8)
.map(|i| EdgeInfo {
tile_id: 0,
tile_offset: i,
})
.collect();
let old_inner: Vec<Vec<EdgeInfo>> = vec![Vec::new(); 8];
let old_ptids = vec![0usize; 8];
let pm = PatchMatch::new(EdgeRange::new(2, 4), Segment::new(1, EdgeRange::new(0, 4)));
let new_n = 4; let new_inner = update_inner_chains(&old_inner, &old_edges, &pm, new_n, &old_ptids);
assert_eq!(new_inner.len(), new_n);
}
fn shape_edges(tile_id: usize, n: usize) -> Vec<EdgeInfo> {
(0..n).map(|i| ei(tile_id, i)).collect()
}
#[test]
fn update_inner_chains_normal_no_crossings_passes_old_through() {
let n = 6;
let old_edges = shape_edges(0, n);
let old_inner: Vec<Vec<EdgeInfo>> = (0..n).map(|i| vec![ei(99, i)]).collect();
let old_ptids = vec![7usize; n];
let pm = PatchMatch::new(EdgeRange::new(2, 2), Segment::new(1, EdgeRange::new(0, 2)));
let m_tile = 4;
let seg_len_old = n - pm.len();
let new_n = seg_len_old + (m_tile - pm.len());
let got = update_inner_chains(&old_inner, &old_edges, &pm, new_n, &old_ptids);
assert_eq!(got.len(), new_n);
assert_eq!(got[0], vec![ei(99, 4)]);
assert_eq!(got[1], vec![ei(99, 5)]);
assert_eq!(got[2], vec![ei(99, 0)]);
assert_eq!(got[3], vec![ei(99, 1)]);
assert_eq!(got[4], vec![ei(99, 2)]);
assert_eq!(got[5], Vec::<EdgeInfo>::new());
}
#[test]
fn update_inner_chains_normal_with_crossings_pushes_matched_edges() {
let n = 6;
let old_edges = shape_edges(0, n);
let old_inner: Vec<Vec<EdgeInfo>> = vec![Vec::new(); n];
let old_ptids = vec![0, 0, 1, 1, 0, 0];
let pm = PatchMatch::new(EdgeRange::new(2, 2), Segment::new(1, EdgeRange::new(0, 2)));
let m_tile = 4;
let seg_len_old = n - pm.len();
let new_n = seg_len_old + (m_tile - pm.len());
let got = update_inner_chains(&old_inner, &old_edges, &pm, new_n, &old_ptids);
assert_eq!(got[0], vec![ei(0, 3)]);
for (i, chain) in got.iter().enumerate().take(seg_len_old).skip(1) {
assert!(chain.is_empty(), "interior position {i}");
}
assert_eq!(got[seg_len_old], vec![ei(0, 2)]);
}
#[test]
fn update_inner_chains_keystone_merges_cw_then_ccw() {
let n = 6;
let old_edges = shape_edges(0, n);
let old_inner: Vec<Vec<EdgeInfo>> = vec![Vec::new(); n];
let old_ptids = vec![0, 0, 1, 1, 0, 0];
let pm = PatchMatch::new(EdgeRange::new(2, 2), Segment::new(1, EdgeRange::new(0, 2)));
let m_tile = 2;
let seg_len_old = n - pm.len();
let new_n = seg_len_old + (m_tile - pm.len());
assert_eq!(new_n, seg_len_old, "keystone precondition");
let got = update_inner_chains(&old_inner, &old_edges, &pm, new_n, &old_ptids);
assert_eq!(got[0], vec![ei(0, 2), ei(0, 3)]);
for (i, chain) in got.iter().enumerate().skip(1) {
assert!(chain.is_empty(), "interior position {i}");
}
}
#[test]
fn update_inner_chains_wraps_array_seam() {
let n = 6;
let old_edges = shape_edges(0, n);
let old_inner: Vec<Vec<EdgeInfo>> = vec![Vec::new(); n];
let old_ptids = vec![1, 0, 0, 0, 0, 1];
let pm = PatchMatch::new(EdgeRange::new(5, 2), Segment::new(1, EdgeRange::new(0, 2)));
let m_tile = 3;
let seg_len_old = n - pm.len();
let new_n = seg_len_old + (m_tile - pm.len());
let got = update_inner_chains(&old_inner, &old_edges, &pm, new_n, &old_ptids);
assert_eq!(got[0], vec![ei(0, 0)]);
assert_eq!(got[seg_len_old], vec![ei(0, 5)]);
for (i, chain) in got.iter().enumerate().take(seg_len_old).skip(1) {
assert!(chain.is_empty(), "interior position {i}");
}
}
#[test]
fn glue_raw_angles_keystone_returns_adjusted_result() {
use crate::geom::glue::glue_raw_angles;
let self_angles = vec![3, 3, 3, -1, -1, -1, -1, 3];
let other_angles = vec![1, 1, 1, 1];
let gr = glue_raw_angles::<ZZ12>(&self_angles, &other_angles, 3, 4, 0)
.expect("glue should succeed on keystone");
assert_eq!(
gr.angles.len(),
4,
"keystone result length = seg_len_old = 8 - 4 = 4"
);
assert!(
gr.a_yx.is_some() && gr.a_xy.is_some(),
"keystone junction angle should be recorded"
);
assert_eq!(
gr.angles[0], 3,
"merged junction angle at result[0]: normalize(3 + (-1) + 1 - 12) = 3"
);
}
#[test]
fn clone_preserves_seed_matches() {
let seed = hex_seed();
let original = seed.candidate_matches().to_vec();
assert!(
!original.is_empty(),
"fixture: seed should have non-empty matches"
);
let clone = seed.clone();
let cloned_matches = clone.candidate_matches().to_vec();
assert_eq!(
cloned_matches, original,
"cloned Seed must report the same matches as the original"
);
}
#[test]
fn first_add_produces_growing() {
let seed = hex_seed();
let pm = seed.candidate_matches()[0];
let gp = seed.grow(&pm).expect("first add");
assert_eq!(gp.boundary_len(), 12 - 2 * pm.len());
assert_eq!(gp.edges().len(), gp.boundary_len());
assert_eq!(gp.angles().len(), gp.boundary_len());
}
#[test]
fn junction_vertex_ids_nonempty_after_each_add() {
let seed = hex_seed();
let first = seed.candidate_matches()[0];
let mut gp = seed.grow(&first).expect("first glue");
let mut step = 0;
assert!(
!gp.edges().is_empty(),
"step {step}: edges should not be empty"
);
assert!(
(0..gp.boundary_len()).any(|i| gp.is_junction(i)),
"step {step}: should have junction vertices"
);
step += 1;
while step < 3 {
let candidates = gp.get_all_matches();
let pm = match candidates.first() {
Some(pm) => *pm,
None => break,
};
if !gp.add_tile(&pm) {
break;
}
assert!(
!gp.edges().is_empty(),
"step {step}: edges should not be empty"
);
assert!(
(0..gp.boundary_len()).any(|i| gp.is_junction(i)),
"step {step}: should have junction vertices"
);
step += 1;
}
assert!(step > 0, "expected at least one successful add");
}
#[test]
fn hexagon_all_36_matches_produce_valid_bi_hexes() {
let seed = hex_seed();
let matches = seed.candidate_matches().to_vec();
assert_eq!(matches.len(), 36, "hex self-matches = 36");
for pm in &matches {
let gp2 = seed
.clone()
.grow(pm)
.unwrap_or_else(|| panic!("first add should succeed for pm {:?}", pm));
assert_eq!(gp2.boundary_len(), 12 - 2 * pm.len());
assert_eq!(gp2.edges().len(), gp2.boundary_len());
let rat = gp2.to_rat();
assert!(
Snake::<ZZ12>::try_from(rat.seq()).is_ok(),
"valid snake for pm {:?}",
pm
);
}
}
#[test]
fn square_all_16_matches_produce_valid_bi_squares() {
let seed = square_seed();
let matches = seed.candidate_matches().to_vec();
assert_eq!(matches.len(), 16, "square self-matches = 16");
for pm in &matches {
let gp2 = seed
.clone()
.grow(pm)
.unwrap_or_else(|| panic!("first add should succeed for pm {:?}", pm));
assert_eq!(gp2.boundary_len(), 8 - 2 * pm.len());
let rat = gp2.to_rat();
assert!(
Snake::<ZZ4>::try_from(rat.seq()).is_ok(),
"valid snake for pm {:?}",
pm
);
}
}
#[test]
fn to_rat_matches_direct_glue_for_all_matches() {
let seed = hex_seed();
let matches = seed.candidate_matches().to_vec();
let ts = seed.tileset().clone();
for pm in &matches {
let gp2 = PatchSeed::<ZZ12>::new(Arc::clone(&ts), 0)
.grow(pm)
.expect("first add");
let rat = gp2.to_rat();
let seed_rat = ts.rat(0);
let new_rat = ts.rat(pm.b.tile_id);
let glued = seed_rat.try_glue(
(
pm.a_range.start_offset as i64,
pm.b.range.start_offset as i64,
),
new_rat,
);
match glued {
Ok(g) => assert_eq!(rat.seq(), g.seq(), "mismatch for pm {:?}", pm),
Err(e) => panic!("glue failed for pm {:?}: {}", pm, e),
}
}
}
#[test]
fn edges_self_consistent() {
let seed_sq: PatchSeed<ZZ4> = square_seed();
for pm in seed_sq.candidate_matches() {
let gp2 = match seed_sq.clone().grow(pm) {
Some(g) => g,
None => continue,
};
verify_edges_consistency(&gp2, gp2.tileset(), &format!("bi-sq pm {:?}", pm));
}
let seed_hex: PatchSeed<ZZ12> = hex_seed();
for pm in seed_hex.candidate_matches() {
let gp2 = match seed_hex.clone().grow(pm) {
Some(g) => g,
None => continue,
};
verify_edges_consistency(&gp2, gp2.tileset(), &format!("bi-hex pm {:?}", pm));
for pm2 in gp2.get_all_matches() {
let mut gp3 = gp2.clone();
if gp3.add_tile(&pm2) {
verify_edges_consistency(&gp3, gp3.tileset(), "3-hex");
}
}
}
}
fn assert_same_cyclic_shape(a: &[i8], b: &[i8], label: &str) {
assert_eq!(
a.len(),
b.len(),
"{label}: angle sequences have different lengths ({} vs {})",
a.len(),
b.len(),
);
if a.is_empty() {
return;
}
let mut a_canon = a.to_vec();
let a_rot = crate::geom::rat::lex_min_rot(&a_canon);
a_canon.rotate_left(a_rot);
let mut b_canon = b.to_vec();
let b_rot = crate::geom::rat::lex_min_rot(&b_canon);
b_canon.rotate_left(b_rot);
assert_eq!(
a_canon, b_canon,
"{label}: angle sequences are not cyclic rotations of each other"
);
}
fn verify_edges_consistency<T: IsRing>(
gp: &GrowingPatch<T>,
ts: &Arc<TileSet<T>>,
label: &str,
) {
let n = gp.boundary_len();
assert!(n > 0, "[{}] patch should be growing", label);
let edges = gp.edges();
assert_eq!(edges.len(), n, "[{}] edges length", label);
for (i, edge) in edges.iter().enumerate().take(n) {
assert!(
edge.tile_id < ts.num_tiles(),
"[{}] pos {}: invalid tile_id {}",
label,
i,
edge.tile_id
);
let tile_len = ts.rat(edge.tile_id).len();
assert!(
edge.tile_offset < tile_len,
"[{}] pos {}: invalid offset {} for tile {} (len {})",
label,
i,
edge.tile_offset,
edge.tile_id,
tile_len
);
}
for i in 0..n {
let j = (i + 1) % n;
if edges[i].tile_id == edges[j].tile_id && !gp.is_junction(i) && !gp.is_junction(j) {
let tile_len = ts.rat(edges[i].tile_id).len();
let expected_next = (edges[i].tile_offset + 1) % tile_len;
assert_eq!(
edges[j].tile_offset, expected_next,
"[{}] pos {}->{}: same-tile continuation expected offset {} got {}",
label, i, j, expected_next, edges[j].tile_offset
);
}
}
let angles = gp.angles();
assert_eq!(angles.len(), n, "[{}] angles length", label);
}
fn assert_minimal_witness_roundtrips_for<T: IsRing>(
glued: &GrowingPatch<T>,
mi: &Arc<MatchTypeIndex<T>>,
label: &str,
) {
let mut checked = 0;
for pos in 0..glued.boundary_len() {
let vt = match glued.junction_vertex_type_at(pos) {
Some(vt) => vt,
None => continue,
};
let (witness, wpos) = GrowingPatch::construct_minimal_witness(&vt, Arc::clone(mi))
.unwrap_or_else(|| {
panic!("{label}: construct_minimal_witness failed at pos={pos} vt={vt:?}")
});
let reconstructed = vertex_type_raw_from(witness.edges(), witness.inner_chains(), wpos);
assert_eq!(
reconstructed, vt,
"{label}: roundtrip failed at pos={pos} for vt={vt:?}",
);
checked += 1;
}
assert!(checked > 0, "{label}: expected at least one junction");
}
fn assert_witness_matches_brute_force<T: IsRing>(
brute: &GrowingPatch<T>,
mi: &Arc<MatchTypeIndex<T>>,
label: &str,
) {
let brute_angles = brute.angles().to_vec();
let brute_edges = brute.edges().to_vec();
let brute_inner = brute.inner_chains().to_vec();
for pos in 0..brute.boundary_len() {
let vt = match brute.junction_vertex_type_at(pos) {
Some(vt) => vt,
None => continue,
};
let (witness, _wpos) = GrowingPatch::construct_minimal_witness(&vt, Arc::clone(mi))
.unwrap_or_else(|| panic!("{label}: witness construction failed at pos={pos}"));
let w_edges = witness.edges();
let w_inner = witness.inner_chains();
let mut found = false;
for wpos in 0..witness.boundary_len() {
let wvt = vertex_type_raw_from(w_edges, w_inner, wpos);
if wvt == vt {
let brute_vt = vertex_type_raw_from(&brute_edges, &brute_inner, pos);
assert_eq!(
wvt, brute_vt,
"{label}: witness VT != brute-force VT at pos={pos}"
);
found = true;
break;
}
}
assert!(
found,
"{label}: no matching position in witness for vt={vt:?} at pos={pos}"
);
assert_same_cyclic_shape(
witness.angles(),
&brute_angles,
&format!("{label}: witness vs brute"),
);
}
}
fn assert_junction_angle_sequence_valid<T: IsRing>(
glued: &GrowingPatch<T>,
mi: &Arc<MatchTypeIndex<T>>,
label: &str,
) {
let tileset = mi.tileset();
let mut checked = 0;
for pos in 0..glued.boundary_len() {
let vt = match glued.junction_vertex_type_at(pos) {
Some(vt) => vt,
None => continue,
};
let angles = junction_angle_sequence::<T>(&vt, tileset.as_ref());
let (witness, wpos) =
GrowingPatch::construct_minimal_witness(&vt, Arc::clone(mi)).expect("witness");
assert_eq!(
*angles.last().unwrap(),
witness.angles()[wpos],
"{label}: last angle should match witness junction angle for vt={vt:?}",
);
assert!(
angles[0] > 0,
"{label}: seed angle should be positive for vt={vt:?} (convex-tile invariant)",
);
for i in 1..angles.len() {
assert!(
angles[i] <= angles[i - 1],
"{label}: angles should be monotone decreasing at i={i} for vt={vt:?}: {angles:?}",
);
}
checked += 1;
}
assert!(checked > 0, "{label}: expected at least one junction");
}
#[test]
fn edges_mixed_consistency() {
let hex_snake: Snake<ZZ12> = tiles::hexagon();
let sq_snake: Snake<ZZ12> = tiles::square();
let hex_rat = Rat::try_from(&hex_snake).unwrap();
let sq_rat = Rat::try_from(&sq_snake).unwrap();
let ts = Arc::new(TileSet::new(vec![hex_rat, sq_rat]));
for seed_id in 0..ts.num_tiles() {
let seed = PatchSeed::<ZZ12>::new(Arc::clone(&ts), seed_id);
for pm in seed.candidate_matches() {
if let Some(gp2) = seed.clone().grow(pm) {
verify_edges_consistency(
&gp2,
&ts,
&format!("mixed seed={} pm {:?}", seed_id, pm),
);
}
}
}
}
#[test]
fn brute_force_squares_up_to_4_tiles() {
let sq: Snake<ZZ4> = tiles::square();
let rat = Rat::try_from(&sq).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let patches = brute_force_patches(&ts, 4);
let mut by_tiles: BTreeMap<usize, (usize, usize)> = BTreeMap::new();
for ways in patches.values() {
let n = ways[0].len() + 1;
let e = by_tiles.entry(n).or_insert((0, 0));
e.0 += 1;
e.1 += ways.len();
}
assert_eq!(
by_tiles.get(&1).map(|(s, _)| *s).unwrap_or(0),
1,
"1 mono-square"
);
assert_eq!(by_tiles.get(&2), Some(&(1, 16)), "1 bi-square, 16 ways");
assert!(
by_tiles.get(&3).map(|(s, _)| *s).unwrap_or(0) >= 2,
"at least 2 tri-squares"
);
}
#[test]
fn brute_force_hexagons_up_to_3_tiles() {
let hex: Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let patches = brute_force_patches(&ts, 3);
let mut by_tiles: BTreeMap<usize, usize> = BTreeMap::new();
for ways in patches.values() {
let n = ways[0].len() + 1;
by_tiles.entry(n).and_modify(|c| *c += 1).or_insert(1);
}
assert_eq!(by_tiles.get(&1).copied().unwrap_or(0), 1, "1 mono-hex");
assert_eq!(by_tiles.get(&2).copied().unwrap_or(0), 1, "1 bi-hex");
assert!(
by_tiles.get(&3).copied().unwrap_or(0) >= 1,
"at least 1 tri-hex"
);
}
fn brute_force_recurse<T: IsRing>(
gp: &mut GrowingPatch<T>,
history: &mut Vec<PatchMatch>,
max_tiles: usize,
results: &mut BTreeMap<Rat<T>, Vec<Vec<PatchMatch>>>,
) {
let num_tiles = history.len() + 1;
let rat = gp.to_rat();
results.entry(rat).or_default().push(history.clone());
if num_tiles >= max_tiles {
return;
}
for pm in &gp.get_all_matches() {
let mut gp2 = gp.clone();
if gp2.add_tile(pm) {
history.push(*pm);
brute_force_recurse(&mut gp2, history, max_tiles, results);
history.pop();
}
}
}
fn brute_force_patches<T: IsRing>(
ts: &Arc<TileSet<T>>,
max_tiles: usize,
) -> BTreeMap<Rat<T>, Vec<Vec<PatchMatch>>> {
let mut results: BTreeMap<Rat<T>, Vec<Vec<PatchMatch>>> = BTreeMap::new();
results
.entry(ts.rat(0).clone())
.or_default()
.push(Vec::new());
let seed = PatchSeed::new(Arc::clone(ts), 0);
let seed_matches = seed.candidate_matches().to_vec();
for pm in &seed_matches {
let mut gp = seed.clone().grow(pm).expect("first add");
let mut history = vec![*pm];
brute_force_recurse(&mut gp, &mut history, max_tiles, &mut results);
}
results
}
#[test]
fn inner_chains_empty_after_first_glue() {
let seed = hex_seed();
let pm = seed.candidate_matches()[0];
let gp2 = seed.grow(&pm).expect("first add");
for (i, chain) in gp2.inner_chains().iter().enumerate() {
assert!(
chain.is_empty(),
"inner chain at position {i} should be empty after first glue, got {chain:?}"
);
}
}
#[test]
fn inner_chains_grow_on_second_glue() {
let seed = hex_seed();
let first_match = seed.candidate_matches()[0];
let gp2 = seed.grow(&first_match).expect("first add");
let candidates = gp2.get_all_matches();
let second = candidates
.iter()
.find(|pm| pm.len() == 1)
.expect("need len-1 match");
let mut gp3 = gp2.clone();
assert!(gp3.add_tile(second), "second add");
let n = gp3.boundary_len();
let edges = gp3.edges();
let ptids = gp3.patch_tile_ids();
let inners = gp3.inner_chains();
for pos in 0..n {
let prev = (pos + n - 1) % n;
let cw_ptid = ptids[prev];
let ccw_ptid = ptids[pos];
for entry in &inners[pos] {
assert_ne!(
entry.tile_id, edges[prev].tile_id,
"inner at {pos} should not be from CW tile"
);
assert_ne!(
entry.tile_id, edges[pos].tile_id,
"inner at {pos} should not be from CCW tile"
);
assert!(
cw_ptid != ccw_ptid || inners[pos].is_empty(),
"when CW and CCW have same ptid, inner should be empty at {pos}"
);
}
}
}
#[test]
fn junction_vertex_type_roundtrip_after_first_glue() {
let seed = hex_seed();
let pm = seed.candidate_matches()[0];
let gp2 = seed.grow(&pm).expect("first add");
let n = gp2.boundary_len();
let mut junction_count = 0;
for i in 0..n {
if let Some(vt) = gp2.junction_vertex_type_at(i) {
assert!(vt.inner.is_empty(), "inner should be empty at pos {i}");
junction_count += 1;
}
}
assert!(junction_count > 0, "should have at least one junction");
}
#[test]
fn construct_minimal_witness_hex_roundtrip() {
let seed = hex_seed();
let mi = seed.match_index().clone();
for pm in seed.candidate_matches() {
let glued = seed.clone().grow(pm).expect("glue should succeed");
assert_minimal_witness_roundtrips_for(&glued, &mi, &format!("hex pm {:?}", pm));
}
}
#[test]
fn construct_minimal_witness_square_roundtrip() {
let seed = square_seed();
let mi = seed.match_index().clone();
for pm in seed.candidate_matches() {
let glued = seed.clone().grow(pm).expect("glue should succeed");
assert_minimal_witness_roundtrips_for(&glued, &mi, &format!("square pm {:?}", pm));
}
}
#[test]
fn construct_minimal_witness_hex_with_inner() {
let seed = hex_seed();
let mi = seed.match_index().clone();
let first = seed.candidate_matches()[0];
let gp2 = seed.grow(&first).expect("first add");
let len1_match = gp2
.get_all_matches()
.into_iter()
.find(|pm| pm.len() == 1)
.expect("need len-1 match");
let mut gp3 = gp2.clone();
assert!(gp3.add_tile(&len1_match), "second add");
assert_minimal_witness_roundtrips_for(&gp3, &mi, "hex two-glue with inner");
}
#[test]
fn get_matches_touching_vertex_lazy_matches_eager() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let seed = PatchSeed::new(Arc::clone(&ts), 0);
let pm = *seed.candidate_matches().first().unwrap();
let mut gp = seed.grow(&pm).unwrap();
gp.ensure_candidates_materialized();
let n = gp.boundary_len();
for target in 0..n {
let lazy = {
let gp2 = gp.clone();
gp2.get_matches_touching_vertex(target)
};
let eager = gp.get_matches_touching_vertex(target);
let mut lazy_sorted = lazy;
lazy_sorted.sort_by_key(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
});
let mut eager_sorted = eager;
eager_sorted.sort_by_key(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
});
assert_eq!(
lazy_sorted,
eager_sorted,
"Mismatch at target={target}: lazy={}, eager={}",
lazy_sorted.len(),
eager_sorted.len()
);
}
}
#[test]
fn compute_candidates_covering_position_matches_full_enumeration() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let mi: Arc<MatchTypeIndex<ZZ12>> = Arc::new(MatchTypeIndex::new(Arc::clone(&ts)));
let gp = grow_first(Arc::clone(&ts));
let all_cands = GrowingPatch::compute_all_candidates(&mi, gp.angles(), gp.edges());
let n = gp.angles().len();
let sort_key = |pm: &PatchMatch| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
};
for target in 0..n {
let mut covering = GrowingPatch::compute_candidates_covering_position(
&mi,
gp.angles(),
gp.edges(),
target,
);
let mut touching_truth: Vec<PatchMatch> = all_cands
.iter()
.flatten()
.filter(|pm| cyclic_range_contains(pm.a_range.start_offset, pm.len(), target, n))
.cloned()
.collect();
covering.sort_by_key(sort_key);
touching_truth.sort_by_key(sort_key);
assert_eq!(
covering, touching_truth,
"covering vs touching-from-all mismatch at target={target}",
);
}
}
fn classify_candidates<T: IsRing>(gp: &GrowingPatch<T>) -> Vec<(PatchMatch, bool)> {
let mut results: Vec<(PatchMatch, bool)> = gp
.get_all_matches()
.into_iter()
.map(|pm| {
let mut trial = gp.clone();
let ok = trial.add_tile(&pm);
(pm, ok)
})
.collect();
results.sort_by_key(|(pm, _)| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
});
results
}
#[allow(clippy::type_complexity)]
fn snapshot_growing<T: IsRing>(
gp: &GrowingPatch<T>,
) -> (
Vec<i8>,
Vec<EdgeInfo>,
Vec<Vec<EdgeInfo>>,
Vec<usize>,
usize,
usize,
Vec<(PatchMatch, bool)>,
) {
(
gp.angles().to_vec(),
gp.edges().to_vec(),
gp.inner_chains().to_vec(),
gp.patch_tile_ids().to_vec(),
gp.next_tile_id(),
gp.boundary_len(),
classify_candidates(gp),
)
}
#[test]
fn add_tile_failure_leaves_state_unchanged() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let mut gp = grow_first(Arc::clone(&ts));
let before = snapshot_growing(&gp);
let failing_pm = before
.6
.iter()
.find(|(_, ok)| !*ok)
.map(|(pm, _)| *pm)
.expect("expected at least one colliding candidate");
assert!(
!gp.add_tile(&failing_pm),
"must reject a colliding candidate",
);
assert_eq!(
snapshot_growing(&gp),
before,
"state changed after a geometrically-rejected pm",
);
}
#[test]
fn add_tile_rejects_geometrically_invalid_candidate() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let gp = grow_first(Arc::clone(&ts));
let candidates = gp.get_all_matches();
let (mut accepted, mut rejected) = (0usize, 0usize);
for pm in &candidates {
let mut trial = gp.clone();
if trial.add_tile(pm) {
accepted += 1;
} else {
rejected += 1;
}
}
assert!(
rejected > 0,
"expected at least one geometrically-invalid candidate to be rejected; \
all {} candidates were accepted",
candidates.len()
);
assert!(
accepted > 0,
"expected at least one valid candidate to be accepted; all {} rejected",
candidates.len()
);
}
#[test]
fn add_tile_decision_agrees_with_snake_on_spectre() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let gp = grow_first(Arc::clone(&ts));
let candidates = gp.get_all_matches();
let tileset = gp.tileset().clone();
let mut compared = 0usize;
let mut discrepancies: Vec<(PatchMatch, bool, bool)> = Vec::new();
for pm in &candidates {
let new_angles = match compute_glue_angles::<ZZ12>(gp.angles(), pm, &tileset) {
Ok(a) => a,
Err(_) => continue,
};
let snake_ok = Snake::<ZZ12>::try_from(new_angles.as_slice()).is_ok();
let mut trial = gp.clone();
let gp_ok = trial.add_tile(pm);
if snake_ok != gp_ok {
discrepancies.push((*pm, snake_ok, gp_ok));
}
compared += 1;
}
assert!(compared > 0, "expected non-zero candidates to compare");
assert!(
discrepancies.is_empty(),
"Snake and add_tile disagreed on {} of {} candidates: {:?}",
discrepancies.len(),
compared,
discrepancies
);
}
#[test]
fn growing_patch_boundary_validates_as_snake_through_growth() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let mut gp = grow_first(Arc::clone(&ts));
{
let angles = gp.angles().to_vec();
let snake = Snake::<ZZ12>::try_from(angles.as_slice());
assert!(
snake.is_ok(),
"step 0: snake validation failed: angles={angles:?}"
);
assert!(
snake.unwrap().is_closed(),
"step 0: boundary should close as a polygon"
);
}
let mut step = 1usize;
while step < 4 {
let pm = match gp.get_all_matches().first() {
Some(pm) => *pm,
None => break,
};
if !gp.add_tile(&pm) {
break;
}
let angles = gp.angles().to_vec();
let snake = Snake::<ZZ12>::try_from(angles.as_slice());
assert!(
snake.is_ok(),
"step {step}: GrowingPatch's boundary failed Snake validation: angles={angles:?}"
);
assert!(
snake.unwrap().is_closed(),
"step {step}: GrowingPatch's boundary should close as a polygon"
);
step += 1;
}
assert!(step > 0, "expected at least one successful add");
}
#[test]
fn get_all_matches_matches_brute_force_on_spectre() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let gp = grow_first(Arc::clone(&ts));
let n = gp.boundary_len();
let rat = Rat::from_slice_unchecked(gp.angles());
let mut brute: std::collections::BTreeSet<(usize, usize, usize, usize)> =
std::collections::BTreeSet::new();
for tile_id_b in 0..ts.num_tiles() {
let tile_b = ts.rat(tile_id_b);
let b_seq = tile_b.seq();
let m_tile = b_seq.len();
for ib in 0..m_tile {
for start_a in 0..n {
let (ns, len, ne) = rat.get_match((start_a as i64, ib as i64), tile_b);
if len == 0 {
continue;
}
let ns_u = ns.rem_euclid(n as i64) as usize;
let ne_u = ne.rem_euclid(m_tile as i64) as usize;
if !crate::geom::glue::junctions_glueable(gp.angles(), ns_u, len, b_seq, ne_u) {
continue;
}
if rat
.try_glue_precomputed((ns, len, ne), tile_b, true)
.is_ok()
{
brute.insert((ns_u, len, ne_u, tile_id_b));
}
}
}
}
let from_api: std::collections::BTreeSet<(usize, usize, usize, usize)> = gp
.get_all_matches()
.into_iter()
.map(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
})
.collect();
assert_eq!(
brute, from_api,
"brute-force candidate set differs from get_all_matches()"
);
}
#[test]
fn get_matches_touching_vertex_matches_brute_force_on_spectre() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let mut gp = grow_first(Arc::clone(&ts));
gp.ensure_candidates_materialized();
let n = gp.boundary_len();
let rat = Rat::from_slice_unchecked(gp.angles());
let mut brute_matches: Vec<PatchMatch> = Vec::new();
for tile_id_b in 0..ts.num_tiles() {
let tile_b = ts.rat(tile_id_b);
let b_seq = tile_b.seq();
let m_tile = b_seq.len();
for ib in 0..m_tile {
for start_a in 0..n {
let (ns, len, ne) = rat.get_match((start_a as i64, ib as i64), tile_b);
if len == 0 {
continue;
}
let ns_u = ns.rem_euclid(n as i64) as usize;
let ne_u = ne.rem_euclid(m_tile as i64) as usize;
if !crate::geom::glue::junctions_glueable(gp.angles(), ns_u, len, b_seq, ne_u) {
continue;
}
if rat
.try_glue_precomputed((ns, len, ne), tile_b, true)
.is_ok()
{
brute_matches.push(PatchMatch::new(
EdgeRange::new(ns_u, len),
Segment::new(tile_id_b, EdgeRange::new(ne_u, len)),
));
}
}
}
}
let brute_set: std::collections::BTreeSet<(usize, usize, usize, usize)> = brute_matches
.iter()
.map(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
})
.collect();
for target in 0..n {
let touching_brute: std::collections::BTreeSet<(usize, usize, usize, usize)> =
brute_set
.iter()
.copied()
.filter(|(start_a, len, _, _)| {
let cyclic_diff = (target + n - *start_a % n) % n;
cyclic_diff <= *len
})
.collect();
let touching_api: std::collections::BTreeSet<(usize, usize, usize, usize)> = gp
.get_matches_touching_vertex(target)
.into_iter()
.map(|pm| {
(
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
pm.b.tile_id,
)
})
.collect();
assert_eq!(
touching_brute, touching_api,
"mismatch at target={target}: brute={touching_brute:?} api={touching_api:?}"
);
}
}
#[test]
fn neighbor_junction_offsets_returns_valid_offsets() {
let seed = hex_seed();
let pm = *seed
.candidate_matches()
.iter()
.find(|p| p.len() == 1)
.expect("len-1 hex match");
let gp = seed.grow(&pm).expect("fixture");
let n = gp.boundary_len();
let edges = gp.edges().to_vec();
let ts = gp.tileset().clone();
for pos in 0..n {
let (cw_off, ccw_off) = gp
.neighbor_junction_offsets(pos)
.expect("Some for valid pos");
let mut j_cw = (pos + n - 1) % n;
while j_cw != pos && !gp.is_junction(j_cw) {
j_cw = (j_cw + n - 1) % n;
}
let cw_tile_len = ts.rat(edges[j_cw].tile_id).len();
assert!(cw_off < cw_tile_len, "cw_off out of range at pos {pos}");
assert_eq!(
cw_off, edges[j_cw].tile_offset,
"cw_off should be the CW junction's tile_offset at pos {pos}",
);
let mut j_ccw = (pos + 1) % n;
while j_ccw != pos && !gp.is_junction(j_ccw) {
j_ccw = (j_ccw + 1) % n;
}
let ccw_prev_edge = edges[(j_ccw + n - 1) % n];
let ccw_tile_len = ts.rat(ccw_prev_edge.tile_id).len();
assert!(ccw_off < ccw_tile_len, "ccw_off out of range at pos {pos}");
assert_eq!(
ccw_off,
(ccw_prev_edge.tile_offset + 1) % ccw_tile_len,
"ccw_off should be (ccw_prev edge's offset + 1) at pos {pos}",
);
}
assert!(gp.neighbor_junction_offsets(n).is_none());
}
#[test]
fn tile_segments_partitions_boundary() {
let seed = hex_seed();
let pm = *seed
.candidate_matches()
.iter()
.find(|p| p.len() == 1)
.expect("len-1 hex match");
let gp = seed.grow(&pm).expect("fixture");
let n = gp.boundary_len();
let edges = gp.edges().to_vec();
let segs = gp.tile_segments();
assert_eq!(
segs.first().map(|s| s.range.start_offset),
Some(0),
"first segment starts at 0"
);
assert_eq!(
segs.last().map(|s| s.range.start_offset + s.range.len),
Some(n),
"last segment ends at n"
);
for w in segs.windows(2) {
assert_eq!(
w[0].range.start_offset + w[0].range.len,
w[1].range.start_offset,
"segments must be contiguous"
);
}
for seg in &segs {
let tile_id = seg.tile_seg.tile_id;
let tile_len = gp.tileset().rat(tile_id).len();
for k in 0..seg.range.len {
let pos = seg.range.start_offset + k;
assert_eq!(edges[pos].tile_id, tile_id, "tile_id at pos {pos}");
assert_eq!(
edges[pos].tile_offset,
(seg.tile_seg.range.start_offset + k) % tile_len,
"tile_offset at pos {pos}",
);
}
}
let expected_starts: std::collections::BTreeSet<usize> = std::iter::once(0)
.chain((0..n).filter(|&i| gp.is_junction(i)))
.collect();
let actual_starts: std::collections::BTreeSet<usize> =
segs.iter().map(|s| s.range.start_offset).collect();
assert_eq!(
actual_starts, expected_starts,
"segment starts must equal {{0}} union junctions"
);
}
#[test]
fn construct_minimal_witness_hex_boundary_matches_brute_force() {
let seed = hex_seed();
let mi = seed.match_index().clone();
for pm in seed.candidate_matches() {
let brute = seed.clone().grow(pm).expect("brute glue");
assert_witness_matches_brute_force(&brute, &mi, &format!("hex pm {:?}", pm));
}
}
#[test]
fn construct_minimal_witness_square_boundary_matches_brute_force() {
let seed = square_seed();
let mi = seed.match_index().clone();
for pm in seed.candidate_matches() {
let brute = seed.clone().grow(pm).expect("brute glue");
assert_witness_matches_brute_force(&brute, &mi, &format!("square pm {:?}", pm));
}
}
#[test]
fn construct_minimal_witness_spectre_roundtrip() {
let ts: Arc<TileSet<ZZ12>> = Arc::new(TileSet::new(vec![
Rat::try_from(&tiles::spectre()).unwrap(),
]));
let gp = grow_first(Arc::clone(&ts));
let mi = gp.match_index().clone();
assert_minimal_witness_roundtrips_for(&gp, &mi, "spectre first-glue");
}
#[test]
fn forward_match_length_hex_basic() {
let hex: Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let seq = rat.seq();
assert_eq!(forward_match_length(seq, 0, seq, 0), 1);
assert_eq!(forward_match_length(seq, 3, seq, 3), 1);
assert_eq!(forward_match_length(seq, 0, seq, 1), 1);
let boundary: Vec<i8> = vec![-2, 2, 2, 2, 2, -2, 2, 2, 2, 2];
assert_eq!(forward_match_length(&boundary, 5, seq, 0), 1);
assert_eq!(forward_match_length(&boundary, 0, seq, 0), 1);
}
#[test]
fn forward_match_length_square_basic() {
let sq: Snake<ZZ4> = tiles::square();
let rat = Rat::try_from(&sq).unwrap();
let seq = rat.seq();
assert_eq!(forward_match_length(seq, 0, seq, 0), 1);
assert_eq!(forward_match_length(seq, 2, seq, 2), 1);
}
#[test]
fn glue_raw_angles_hex_self_glue() {
let hex: Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let seq = rat.seq().to_vec();
let result = glue::glue_raw_angles::<ZZ12>(&seq, &seq, 0, 1, 0);
assert!(result.is_some());
let gr = result.unwrap();
assert_eq!(gr.angles.len(), 10);
assert_eq!(gr.a_yx, Some(-2));
assert_eq!(gr.a_xy, Some(-2));
}
#[test]
fn glue_raw_angles_matches_rat_glue() {
let hex: Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let seq = rat.seq();
let rat_result = rat.try_glue((0, 0), &rat).expect("rat glue");
let raw_result = glue::glue_raw_angles::<ZZ12>(seq, seq, 0, 1, 0).expect("raw glue");
assert_same_cyclic_shape(rat_result.seq(), &raw_result.angles, "rat vs raw glue");
}
#[test]
fn test_junction_angle_sequence_hex() {
let seed = hex_seed();
let mi = seed.match_index().clone();
for pm in seed.candidate_matches() {
let glued = seed.clone().grow(pm).expect("glue");
assert_junction_angle_sequence_valid(&glued, &mi, &format!("hex pm {:?}", pm));
}
}
#[test]
fn construct_witness_from_vt_sequence_single_vt_roundtrip() {
let seed = hex_seed();
let mi = seed.match_index().clone();
let pm = *seed
.candidate_matches()
.iter()
.find(|pm| pm.len() == 1)
.expect("len-1 match");
let gp = seed.grow(&pm).expect("first glue");
let vt = gp.junction_vertex_type_at(0).expect("junction at 0");
let (minimal, _wpos) =
GrowingPatch::construct_minimal_witness(&vt, mi.clone()).expect("minimal witness");
let (reconstructed, _junc_positions) =
GrowingPatch::construct_witness_from_vt_sequence(std::slice::from_ref(&vt), mi)
.expect("reconstruction");
assert_eq!(minimal.angles(), reconstructed.angles());
assert_eq!(minimal.edges(), reconstructed.edges());
assert_eq!(minimal.inner_chains(), reconstructed.inner_chains());
}
fn five_hex_cross() -> GrowingPatch<ZZ12> {
let glues = [
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(0, 1))),
PatchMatch::new(EdgeRange::new(1, 1), Segment::new(0, EdgeRange::new(1, 1))),
PatchMatch::new(EdgeRange::new(2, 2), Segment::new(0, EdgeRange::new(1, 2))),
PatchMatch::new(EdgeRange::new(9, 2), Segment::new(0, EdgeRange::new(1, 2))),
];
build_from_glues(hex_seed(), &glues, "five_hex_cross")
}
#[test]
fn five_hex_cross_structure() {
let gp = five_hex_cross();
let n = gp.boundary_len();
assert_eq!(n, 18);
let angles = gp.angles();
assert_eq!(&angles[..9], &angles[9..], "boundary should be symmetric");
let junctions: Vec<usize> = (0..n).filter(|&i| gp.is_junction(i)).collect();
assert_eq!(junctions.len(), 6);
let mut segs: Vec<usize> = Vec::new();
for w in junctions.windows(2) {
segs.push(w[1] - w[0]);
}
segs.push(n - junctions[5] + junctions[0]);
assert_eq!(
segs,
vec![1, 4, 4, 1, 4, 4],
"junction offsets should be 1,4,4,1,4,4"
);
for i in 0..n {
let prev = (i + n - 1) % n;
let id = gp.patch_tile_ids()[i];
let prev_id = gp.patch_tile_ids()[prev];
if gp.is_junction(i) {
assert_ne!(
id, prev_id,
"junction at {i} should have distinct patch_tile_ids"
);
}
}
let mut run_start = 0;
let mut runs: Vec<(usize, usize)> = Vec::new();
for i in 1..=n {
if i == n || gp.patch_tile_ids()[i] != gp.patch_tile_ids()[run_start] {
runs.push((gp.patch_tile_ids()[run_start], i - run_start));
run_start = i;
}
}
assert_eq!(runs.len(), 6, "should have 6 runs of patch_tile_ids");
let center_runs: Vec<&(usize, usize)> = runs.iter().filter(|(id, _)| *id == 0).collect();
assert_eq!(
center_runs.len(),
2,
"center tile should appear in exactly 2 runs"
);
assert_eq!(center_runs[0].1, 1, "each center run should be 1 edge");
assert_eq!(center_runs[1].1, 1, "each center run should be 1 edge");
}
#[test]
fn reconstruct_five_hex_cross() {
let gp = five_hex_cross();
let mi = gp.match_index().clone();
let n = gp.boundary_len();
let mut vt_seq: Vec<OpenVertexType> = Vec::new();
for i in 0..n {
if gp.is_junction(i) {
let vt = gp.junction_vertex_type_at(i).unwrap();
assert!(
vt.inner.is_empty(),
"hex boundary junctions should have empty inner"
);
vt_seq.push(vt);
}
}
assert_eq!(vt_seq.len(), 6);
let result = GrowingPatch::construct_witness_from_vt_sequence(&vt_seq, mi);
let (reconstructed, _junc_positions) = result.expect("reconstruction should succeed");
assert_same_cyclic_shape(
gp.angles(),
reconstructed.angles(),
"5-hex-cross: reconstructed vs original",
);
assert_eq!(
reconstructed.boundary_len(),
gp.boundary_len(),
"boundary length should match"
);
let recon_juncs: Vec<usize> = (0..reconstructed.boundary_len())
.filter(|&i| reconstructed.is_junction(i))
.collect();
assert_eq!(recon_juncs.len(), 6, "should have 6 junctions");
}
#[test]
fn next_junction_on_raw_boundary_finds_all_junctions() {
let seed = hex_seed();
let ts = seed.tileset().clone();
let pm = *seed
.candidate_matches()
.iter()
.find(|pm| pm.len() == 1)
.expect("len-1 match");
let gp = seed.grow(&pm).expect("first glue");
let raw = RawBoundary {
angles: gp.angles().to_vec(),
edges: gp.edges().to_vec(),
inner_chains: gp.inner_chains().to_vec(),
patch_tile_ids: gp.patch_tile_ids().to_vec(),
};
let n = raw.angles.len();
let mut junctions: Vec<usize> = Vec::new();
for i in 0..n {
if raw_is_junction(&raw, ts.as_ref(), i) {
junctions.push(i);
}
}
assert_eq!(junctions.len(), 2, "two-hex should have 2 junctions");
let j1 = next_junction_on_raw_boundary(&raw, ts.as_ref(), junctions[0])
.expect("should find next junction");
assert_eq!(j1, junctions[1], "should find the other junction");
let j0 = next_junction_on_raw_boundary(&raw, ts.as_ref(), junctions[1])
.expect("should wrap around");
assert_eq!(j0, junctions[0], "should wrap to first junction");
}
#[test]
fn test_junction_angle_sequence_square() {
let seed = square_seed();
let mi = seed.match_index().clone();
for pm in seed.candidate_matches() {
let glued = seed.clone().grow(pm).expect("glue");
assert_junction_angle_sequence_valid(&glued, &mi, &format!("square pm {:?}", pm));
}
}
#[test]
fn normalize_five_hex_cross() {
let gp = five_hex_cross();
let mut gp2 = gp.clone();
gp2.normalize();
assert_eq!(gp2.boundary_len(), 18);
let ptids = gp2.patch_tile_ids();
let mut seen = std::collections::HashSet::new();
for &id in ptids {
seen.insert(id);
}
let max_id = *seen.iter().max().unwrap();
assert_eq!(
seen.len(),
max_id + 1,
"ptids should be 0..=max with no gaps"
);
assert_eq!(gp2.next_tile_id(), seen.len());
let angles = gp2.angles();
let min_angle = *angles.iter().min().unwrap();
assert_eq!(
angles[0], min_angle,
"normalized boundary should start at lex-min angle"
);
}
#[test]
fn normalize_idempotent() {
let gp = five_hex_cross();
let mut gp1 = gp.clone();
gp1.normalize();
let snap1 = (
gp1.angles().to_vec(),
gp1.edges().to_vec(),
gp1.patch_tile_ids().to_vec(),
);
gp1.normalize();
let snap2 = (
gp1.angles().to_vec(),
gp1.edges().to_vec(),
gp1.patch_tile_ids().to_vec(),
);
assert_eq!(snap1, snap2, "normalize should be idempotent");
}
fn t_tetromino_angles() -> Vec<i8> {
let snake: Snake<ZZ4> = tiles::tetromino_T();
let rat = Rat::try_from(&snake).unwrap();
rat.seq().to_vec()
}
fn t_tetromino() -> GrowingPatch<ZZ4> {
let glues = [
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(0, 1))),
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(1, 1))),
PatchMatch::new(EdgeRange::new(0, 1), Segment::new(0, EdgeRange::new(1, 1))),
];
let gp = build_from_glues(square_seed(), &glues, "t_tetromino");
assert_eq!(gp.boundary_len(), 10, "T-tetromino should have 10 edges");
gp
}
#[test]
fn reconstruct_t_tetromino() {
let gp = t_tetromino();
let mi = gp.match_index().clone();
let n = gp.boundary_len();
assert_eq!(n, 10);
let ref_angles = t_tetromino_angles();
assert_same_cyclic_shape(
gp.angles(),
&ref_angles,
"built patch should be the T tetromino shape",
);
let mut vt_seq: Vec<OpenVertexType> = Vec::new();
for i in 0..n {
if gp.is_junction(i) {
let vt = gp.junction_vertex_type_at(i).unwrap();
vt_seq.push(vt);
}
}
assert!(!vt_seq.is_empty(), "T should have junctions");
let has_inner = vt_seq.iter().any(|vt| !vt.inner.is_empty());
assert!(
has_inner,
"T tetromino should have junctions with non-empty inner"
);
let result = GrowingPatch::construct_witness_from_vt_sequence(&vt_seq, mi);
let (reconstructed, _junc_positions) = result.expect("reconstruction should succeed");
assert_eq!(
reconstructed.boundary_len(),
n,
"boundary length should match"
);
assert_same_cyclic_shape(
reconstructed.angles(),
&ref_angles,
"reconstructed angles should match T",
);
let recon_juncs: Vec<usize> = (0..reconstructed.boundary_len())
.filter(|&i| reconstructed.is_junction(i))
.collect();
assert_eq!(
recon_juncs.len(),
vt_seq.len(),
"junction count should match"
);
}
}