use std::collections::{BTreeSet, HashMap, VecDeque};
use std::sync::Arc;
use crate::cyclotomic::IsRing;
use crate::geom::patch::{GrowingPatch, PatchMatch, PatchSeed};
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
use crate::geom::vertices::{ClosedVertexType, EdgeInfo, OpenVertexType, TransitionSide};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum VTypeKind {
Dead,
Undead,
Blessed,
Free,
}
impl VTypeKind {
pub fn segment_kind(a: VTypeKind, b: VTypeKind) -> VTypeKind {
let cursed = matches!(a, VTypeKind::Dead | VTypeKind::Undead)
|| matches!(b, VTypeKind::Dead | VTypeKind::Undead);
let dead = matches!(a, VTypeKind::Dead) || matches!(b, VTypeKind::Dead);
let blessed = matches!(a, VTypeKind::Blessed) && matches!(b, VTypeKind::Blessed);
if dead {
VTypeKind::Dead
} else if cursed {
VTypeKind::Undead
} else if blessed {
VTypeKind::Blessed
} else {
VTypeKind::Free
}
}
}
pub struct OpenVertexTypeInfo<T: IsRing> {
vtype: OpenVertexType,
kind: VTypeKind,
is_initial: bool,
realizing_rat: Rat<T>,
witness: GrowingPatch<T>,
witness_pos: usize,
gap_angle: i8,
cw_neighbor_offset: usize,
ccw_neighbor_offset: usize,
}
impl<T: IsRing> OpenVertexTypeInfo<T> {
pub fn vtype(&self) -> &OpenVertexType {
&self.vtype
}
pub fn kind(&self) -> VTypeKind {
self.kind
}
pub fn is_dead(&self) -> bool {
self.kind == VTypeKind::Dead
}
pub fn is_undead(&self) -> bool {
self.kind == VTypeKind::Undead
}
pub fn is_blessed(&self) -> bool {
self.kind == VTypeKind::Blessed
}
pub fn is_free(&self) -> bool {
self.kind == VTypeKind::Free
}
pub fn is_cursed(&self) -> bool {
matches!(self.kind, VTypeKind::Dead | VTypeKind::Undead)
}
pub fn is_alive(&self) -> bool {
matches!(self.kind, VTypeKind::Blessed | VTypeKind::Free)
}
pub fn is_initial(&self) -> bool {
self.is_initial
}
pub fn realizing_rat(&self) -> &Rat<T> {
&self.realizing_rat
}
pub fn gap_angle(&self) -> i8 {
self.gap_angle
}
pub fn witness(&self) -> &GrowingPatch<T> {
&self.witness
}
pub fn witness_pos(&self) -> usize {
self.witness_pos
}
pub fn cw_neighbor_offset(&self) -> usize {
self.cw_neighbor_offset
}
pub fn ccw_neighbor_offset(&self) -> usize {
self.ccw_neighbor_offset
}
}
pub const CLOSED_ID: usize = 0;
pub struct TransitionInfo {
pub src_id: usize,
pub dst_id: usize,
pub side: TransitionSide,
pub tile_id: usize,
pub tile_offset: usize,
pub closed_vt_id: Option<usize>,
}
impl TransitionInfo {
pub fn is_closed(&self) -> bool {
self.dst_id == CLOSED_ID
}
}
pub struct ClosedVertexTypeInfo {
vtype: ClosedVertexType,
id: usize,
transition_count: usize,
}
impl ClosedVertexTypeInfo {
pub fn vtype(&self) -> &ClosedVertexType {
&self.vtype
}
pub fn id(&self) -> usize {
self.id
}
pub fn transition_count(&self) -> usize {
self.transition_count
}
}
pub struct OpenVertexTypeIndex<T: IsRing> {
tileset: Arc<TileSet<T>>,
entries: Vec<OpenVertexTypeInfo<T>>,
transitions: Vec<TransitionInfo>,
reverse: HashMap<OpenVertexType, usize>,
closed_entries: Vec<ClosedVertexTypeInfo>,
closed_reverse: HashMap<ClosedVertexType, usize>,
}
impl<T: IsRing> OpenVertexTypeIndex<T> {
pub fn new(tileset: Arc<TileSet<T>>) -> Self {
let mut state = BfsState::default();
seed_phase(&mut state, &tileset);
bfs_phase(&mut state, &tileset);
let BfsState {
all_types,
initial_types,
raw_transitions,
witness_store,
..
} = state;
let (vt_list, reverse) = build_id_map(all_types);
let (closed_list, closed_reverse) = build_closed_id_map(&raw_transitions);
let (succ_sets, transition_infos, closed_counts) = build_transition_arrays(
&reverse,
&closed_reverse,
closed_list.len(),
&raw_transitions,
);
let has_any_realized = compute_has_any_realized(vt_list.len(), &transition_infos);
let has_closing = compute_has_closing(vt_list.len(), &transition_infos);
let is_cursed = compute_cursed(&vt_list, &has_any_realized, &has_closing, &succ_sets);
let is_blessed = compute_blessed(&vt_list, &has_any_realized, &succ_sets);
let info_entries = classify_and_finalize(
vt_list,
&has_any_realized,
witness_store,
&initial_types,
&is_cursed,
&is_blessed,
);
let closed_entries: Vec<ClosedVertexTypeInfo> = closed_list
.into_iter()
.enumerate()
.map(|(i, cvt)| ClosedVertexTypeInfo {
vtype: cvt,
id: i + 1,
transition_count: closed_counts[i],
})
.collect();
OpenVertexTypeIndex {
tileset,
entries: info_entries,
transitions: transition_infos,
reverse,
closed_entries,
closed_reverse,
}
}
pub fn num_types(&self) -> usize {
self.entries.len()
}
pub fn transitions(&self) -> &[TransitionInfo] {
&self.transitions
}
pub fn entries(&self) -> &[OpenVertexTypeInfo<T>] {
&self.entries
}
pub fn range_by_cw(&self, tile_id: usize) -> &[OpenVertexTypeInfo<T>] {
let start = self
.entries
.partition_point(|e| e.vtype().cw.tile_id < tile_id);
let end = self
.entries
.partition_point(|e| e.vtype().cw.tile_id <= tile_id);
&self.entries[start..end]
}
pub fn get_id(&self, vtype: &OpenVertexType) -> Option<usize> {
self.reverse.get(vtype).copied()
}
pub fn get_type(&self, id: usize) -> &OpenVertexType {
assert!(
id >= 1 && id <= self.entries.len(),
"vertex type id out of range"
);
&self.entries[id - 1].vtype
}
pub fn get_info(&self, id: usize) -> &OpenVertexTypeInfo<T> {
assert!(
id >= 1 && id <= self.entries.len(),
"vertex type id out of range"
);
&self.entries[id - 1]
}
pub fn tileset(&self) -> &Arc<TileSet<T>> {
&self.tileset
}
pub fn num_closed_types(&self) -> usize {
self.closed_entries.len()
}
pub fn closed_entries(&self) -> &[ClosedVertexTypeInfo] {
&self.closed_entries
}
pub fn get_closed_id(&self, cvt: &ClosedVertexType) -> Option<usize> {
self.closed_reverse.get(cvt).copied()
}
pub fn get_closed_type(&self, id: usize) -> &ClosedVertexType {
assert!(
id >= 1 && id <= self.closed_entries.len(),
"closed vertex type id out of range"
);
&self.closed_entries[id - 1].vtype
}
pub fn get_closed_info(&self, id: usize) -> &ClosedVertexTypeInfo {
assert!(
id >= 1 && id <= self.closed_entries.len(),
"closed vertex type id out of range"
);
&self.closed_entries[id - 1]
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum RawDst {
Open(OpenVertexType),
Closed(ClosedVertexType),
}
type RawTransition = (OpenVertexType, RawDst, TransitionSide, usize, usize);
struct BfsState<T: IsRing> {
all_types: BTreeSet<OpenVertexType>,
initial_types: BTreeSet<OpenVertexType>,
raw_transitions: BTreeSet<RawTransition>,
visited: BTreeSet<OpenVertexType>,
queue: VecDeque<OpenVertexType>,
witness_store: HashMap<OpenVertexType, (GrowingPatch<T>, usize, i8)>,
}
impl<T: IsRing> Default for BfsState<T> {
fn default() -> Self {
BfsState {
all_types: BTreeSet::new(),
initial_types: BTreeSet::new(),
raw_transitions: BTreeSet::new(),
visited: BTreeSet::new(),
queue: VecDeque::new(),
witness_store: HashMap::new(),
}
}
}
fn seed_phase<T: IsRing>(state: &mut BfsState<T>, tileset: &Arc<TileSet<T>>) {
for seed_id in 0..tileset.num_tiles() {
let seed = PatchSeed::new(Arc::clone(tileset), seed_id);
for pm in seed.candidate_matches() {
let gp = match seed.clone().grow(pm) {
Some(g) => g,
None => continue,
};
for pos in 0..gp.boundary_len() {
if !gp.is_junction(pos) {
continue;
}
if let Some(vt) = gp.junction_vertex_type_at(pos) {
state.all_types.insert(vt.clone());
state.initial_types.insert(vt.clone());
state
.witness_store
.entry(vt.clone())
.or_insert_with(|| (gp.clone(), pos, gp.angles()[pos]));
if state.visited.insert(vt.clone()) {
state.queue.push_back(vt);
}
}
}
}
}
}
fn bfs_phase<T: IsRing>(state: &mut BfsState<T>, tileset: &Arc<TileSet<T>>) {
while let Some(vt) = state.queue.pop_front() {
let (touching, n) = {
let (gp, pos, _gap) = &state.witness_store[&vt];
let n = gp.boundary_len();
let t: Vec<PatchMatch> = gp.get_matches_touching_vertex(*pos);
if t.is_empty() {
continue;
}
(t, n)
};
let pos = state.witness_store[&vt].1;
for pm in touching {
let gp = &state.witness_store[&vt].0;
let mut gp2 = gp.clone();
if !gp2.add_tile(&pm) {
continue;
}
let edge_in_match =
|edge: usize| -> bool { (edge + n - pm.a_range.start_offset) % n < pm.len() };
let consumes_cw_edge = edge_in_match((pos + n - 1) % n);
let consumes_ccw_edge = edge_in_match(pos);
debug_assert!(
consumes_cw_edge || consumes_ccw_edge,
"touching match doesn't consume any incident edge of pos"
);
let side = match (consumes_cw_edge, consumes_ccw_edge) {
(true, true) => TransitionSide::Both,
(true, false) => TransitionSide::Cw,
(false, true) => TransitionSide::Ccw,
(false, false) => unreachable!(),
};
let edge_pos = match side {
TransitionSide::Ccw => pos,
TransitionSide::Cw | TransitionSide::Both => (pos + n - 1) % n,
};
let offset_in_match =
(edge_pos as i64 - pm.a_range.start_offset as i64).rem_euclid(n as i64) as usize;
let m = tileset.rat(pm.b.tile_id).len();
let tile_offset = (pm.b.range.start_offset as i64 - 1 - offset_in_match as i64)
.rem_euclid(m as i64) as usize;
let junction_pos = if pm.a_range.start_offset == pos {
n - pm.len()
} else {
0
};
if matches!(side, TransitionSide::Both) {
let closed = ClosedVertexType::from_open_via_closure(&vt);
state.raw_transitions.insert((
vt.clone(),
RawDst::Closed(closed),
side,
pm.b.tile_id,
tile_offset,
));
} else if let Some(new_vt) = gp2.junction_vertex_type_at(junction_pos) {
state.raw_transitions.insert((
vt.clone(),
RawDst::Open(new_vt),
side,
pm.b.tile_id,
tile_offset,
));
}
for new_pos in 0..gp2.boundary_len() {
if !gp2.is_junction(new_pos) {
continue;
}
if let Some(nv) = gp2.junction_vertex_type_at(new_pos) {
state.all_types.insert(nv.clone());
if state.visited.insert(nv.clone()) {
state
.witness_store
.entry(nv.clone())
.or_insert_with(|| (gp2.clone(), new_pos, gp2.angles()[new_pos]));
state.queue.push_back(nv);
}
}
}
}
}
}
fn build_id_map(
all_types: BTreeSet<OpenVertexType>,
) -> (Vec<OpenVertexType>, HashMap<OpenVertexType, usize>) {
let vt_list: Vec<OpenVertexType> = all_types.into_iter().collect();
let reverse: HashMap<OpenVertexType, usize> = vt_list
.iter()
.enumerate()
.map(|(i, vt)| (vt.clone(), i + 1))
.collect();
(vt_list, reverse)
}
fn build_closed_id_map(
raw_transitions: &BTreeSet<RawTransition>,
) -> (Vec<ClosedVertexType>, HashMap<ClosedVertexType, usize>) {
let mut all_closed: BTreeSet<ClosedVertexType> = BTreeSet::new();
for (_src, dst, _side, _tid, _toff) in raw_transitions {
if let RawDst::Closed(c) = dst {
all_closed.insert(c.clone());
}
}
let closed_list: Vec<ClosedVertexType> = all_closed.into_iter().collect();
let closed_reverse: HashMap<ClosedVertexType, usize> = closed_list
.iter()
.enumerate()
.map(|(i, c)| (c.clone(), i + 1))
.collect();
(closed_list, closed_reverse)
}
fn build_transition_arrays(
reverse: &HashMap<OpenVertexType, usize>,
closed_reverse: &HashMap<ClosedVertexType, usize>,
num_closed: usize,
raw_transitions: &BTreeSet<RawTransition>,
) -> (Vec<BTreeSet<usize>>, Vec<TransitionInfo>, Vec<usize>) {
let n = reverse.len();
let mut succ_sets: Vec<BTreeSet<usize>> = vec![BTreeSet::new(); n];
let mut transition_infos: Vec<TransitionInfo> = Vec::new();
let mut closed_transition_counts: Vec<usize> = vec![0; num_closed];
for (src, dst, side, tid, toff) in raw_transitions {
let Some(&src_id) = reverse.get(src) else {
continue;
};
match dst {
RawDst::Open(dst_vt) => {
if let Some(&dst_id) = reverse.get(dst_vt) {
succ_sets[src_id - 1].insert(dst_id);
transition_infos.push(TransitionInfo {
src_id,
dst_id,
side: *side,
tile_id: *tid,
tile_offset: *toff,
closed_vt_id: None,
});
}
}
RawDst::Closed(cvt) => {
let cvt_id = closed_reverse.get(cvt).copied();
if let Some(id) = cvt_id {
closed_transition_counts[id - 1] += 1;
}
transition_infos.push(TransitionInfo {
src_id,
dst_id: CLOSED_ID,
side: *side,
tile_id: *tid,
tile_offset: *toff,
closed_vt_id: cvt_id,
});
}
}
}
(succ_sets, transition_infos, closed_transition_counts)
}
fn classify_and_finalize<T: IsRing>(
vt_list: Vec<OpenVertexType>,
has_any_realized: &[bool],
mut witness_store: HashMap<OpenVertexType, (GrowingPatch<T>, usize, i8)>,
initial_types: &BTreeSet<OpenVertexType>,
is_cursed: &[bool],
is_blessed: &[bool],
) -> Vec<OpenVertexTypeInfo<T>> {
vt_list
.into_iter()
.enumerate()
.map(|(i, vt)| {
let (witness, witness_pos, gap_angle) = witness_store.remove(&vt).unwrap();
let rat = witness.to_rat();
let (cw_nbr, ccw_nbr) = witness
.neighbor_junction_offsets(witness_pos)
.unwrap_or((0, 0));
let no_transitions = !has_any_realized[i];
let kind = if no_transitions {
VTypeKind::Dead
} else if is_cursed[i] {
VTypeKind::Undead
} else if is_blessed[i] {
VTypeKind::Blessed
} else {
VTypeKind::Free
};
OpenVertexTypeInfo {
kind,
is_initial: initial_types.contains(&vt),
realizing_rat: rat,
vtype: vt,
witness,
witness_pos,
gap_angle,
cw_neighbor_offset: cw_nbr,
ccw_neighbor_offset: ccw_nbr,
}
})
.collect()
}
fn compute_cursed(
entries: &[OpenVertexType],
has_any_realized: &[bool],
has_closing: &[bool],
succ_sets: &[BTreeSet<usize>],
) -> Vec<bool> {
let n = entries.len();
let mut cursed: Vec<bool> = has_any_realized.iter().map(|&h| !h).collect();
let mut changed = true;
while changed {
changed = false;
for i in 0..n {
if cursed[i] {
continue;
}
if has_closing[i] {
continue;
}
let succs = &succ_sets[i];
if succs.is_empty() {
debug_assert!(
!has_any_realized[i],
"VT with no open successors and no closing transitions \
should have been Dead in the base case"
);
continue;
}
if succs.iter().all(|&s| cursed[s - 1]) {
cursed[i] = true;
changed = true;
}
}
}
cursed
}
fn compute_blessed(
entries: &[OpenVertexType],
has_any_realized: &[bool],
succ_sets: &[BTreeSet<usize>],
) -> Vec<bool> {
let n = entries.len();
let mut blessed: Vec<bool> = vec![false; n];
let mut changed = true;
while changed {
changed = false;
for i in 0..n {
if blessed[i] {
continue;
}
if !has_any_realized[i] {
continue;
}
if succ_sets[i].iter().all(|&dst| blessed[dst - 1]) {
blessed[i] = true;
changed = true;
}
}
}
blessed
}
fn compute_has_any_realized(n: usize, transitions: &[TransitionInfo]) -> Vec<bool> {
let mut v = vec![false; n];
for t in transitions {
v[t.src_id - 1] = true;
}
v
}
fn compute_has_closing(n: usize, transitions: &[TransitionInfo]) -> Vec<bool> {
let mut v = vec![false; n];
for t in transitions {
if t.is_closed() {
v[t.src_id - 1] = true;
}
}
v
}
use crate::analysis::matchtypes::MatchTypeIndex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WitnessSnapshot {
pub pos: usize,
pub angles: Vec<i8>,
pub edges: Vec<EdgeInfo>,
pub inner_chains: Vec<Vec<EdgeInfo>>,
pub patch_tile_ids: Vec<usize>,
pub next_tile_id: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VtypeRecord {
pub id: usize,
pub vtype: OpenVertexType,
pub kind: VTypeKind,
pub cw_neighbor_offset: usize,
pub ccw_neighbor_offset: usize,
pub witness: WitnessSnapshot,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransitionRecord {
pub src_id: usize,
pub dst_id: usize,
pub side: TransitionSide,
pub tile_id: usize,
pub tile_offset: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Collection {
pub ring: String,
pub tile_angles: Vec<Vec<i8>>,
pub vtypes: Vec<VtypeRecord>,
pub transitions: Vec<TransitionRecord>,
}
pub struct CompletenessReport {
pub matches_checked: usize,
pub missing: std::collections::BTreeSet<usize>,
}
impl CompletenessReport {
pub fn is_complete(&self) -> bool {
self.missing.is_empty()
}
}
impl Collection {
pub fn from_index<T>(idx: &OpenVertexTypeIndex<T>, ring: impl Into<String>) -> Self
where
T: IsRing,
{
let tile_angles = idx
.tileset()
.rats()
.iter()
.map(|r| r.seq().to_vec())
.collect();
let mut vtypes = Vec::with_capacity(idx.num_types());
for id in 1..=idx.num_types() {
let info = idx.get_info(id);
let mut w = info.witness().clone();
let rot = w.normalize();
let n = w.boundary_len();
let pos = (info.witness_pos() + n - rot) % n;
vtypes.push(VtypeRecord {
id,
vtype: info.vtype().clone(),
kind: info.kind(),
cw_neighbor_offset: info.cw_neighbor_offset(),
ccw_neighbor_offset: info.ccw_neighbor_offset(),
witness: WitnessSnapshot {
pos,
angles: w.angles().to_vec(),
edges: w.edges().to_vec(),
inner_chains: w.inner_chains().to_vec(),
patch_tile_ids: w.patch_tile_ids().to_vec(),
next_tile_id: w.next_tile_id(),
},
});
}
let transitions = idx
.transitions()
.iter()
.map(|t| TransitionRecord {
src_id: t.src_id,
dst_id: t.dst_id,
side: t.side,
tile_id: t.tile_id,
tile_offset: t.tile_offset,
})
.collect();
Collection {
ring: ring.into(),
tile_angles,
vtypes,
transitions,
}
}
pub fn reconstruct_witnesses<T>(
&self,
tile_ts: &Arc<TileSet<T>>,
) -> Result<HashMap<usize, GrowingPatch<T>>, String>
where
T: IsRing,
{
let mi = Arc::new(MatchTypeIndex::new(Arc::clone(tile_ts)));
let mut out = HashMap::with_capacity(self.vtypes.len());
for v in &self.vtypes {
let n = v.witness.angles.len();
if v.witness.inner_chains.len() != n
|| v.witness.edges.len() != n
|| v.witness.patch_tile_ids.len() != n
{
return Err(format!(
"VT {}: witness arrays length mismatch (n={}, edges={}, inner={}, ptids={})",
v.id,
n,
v.witness.edges.len(),
v.witness.inner_chains.len(),
v.witness.patch_tile_ids.len(),
));
}
let gp = GrowingPatch::from_parts(
Arc::clone(&mi),
v.witness.angles.clone(),
v.witness.edges.clone(),
v.witness.inner_chains.clone(),
v.witness.patch_tile_ids.clone(),
v.witness.next_tile_id,
)
.ok_or_else(|| format!("VT {}: from_parts failed", v.id))?;
out.insert(v.id, gp);
}
Ok(out)
}
pub fn vtype_errors<T>(
&self,
witnesses: &HashMap<usize, GrowingPatch<T>>,
) -> Vec<(usize, String)>
where
T: IsRing,
{
let mut errors = Vec::new();
for v in &self.vtypes {
let Some(gp) = witnesses.get(&v.id) else {
errors.push((v.id, "no witness".to_string()));
continue;
};
let pos = v.witness.pos;
let (expected_cw, expected_ccw) = gp.neighbor_junction_offsets(pos).unwrap_or((0, 0));
if expected_cw != v.cw_neighbor_offset || expected_ccw != v.ccw_neighbor_offset {
errors.push((
v.id,
format!(
"neighbor offsets: expected ({}, {}), recorded ({}, {})",
expected_cw, expected_ccw, v.cw_neighbor_offset, v.ccw_neighbor_offset,
),
));
}
match gp.junction_vertex_type_at(pos) {
Some(actual) if actual.cw == v.vtype.cw && actual.ccw == v.vtype.ccw => {}
Some(actual) => errors.push((
v.id,
format!(
"vtype mismatch: recorded {:?}, witness {:?}",
v.vtype, actual
),
)),
None => errors.push((v.id, "junction_vertex_type_at returned None".to_string())),
}
}
errors
}
pub fn transition_errors<T>(
&self,
tile_ts: &Arc<TileSet<T>>,
witnesses: &HashMap<usize, GrowingPatch<T>>,
) -> Vec<((usize, usize), String)>
where
T: IsRing,
{
let mut errors = Vec::new();
let known_ids: std::collections::BTreeSet<usize> =
self.vtypes.iter().map(|v| v.id).collect();
let witness_pos: HashMap<usize, usize> =
self.vtypes.iter().map(|v| (v.id, v.witness.pos)).collect();
for t in &self.transitions {
if !known_ids.contains(&t.src_id) {
errors.push(((t.src_id, t.dst_id), "unknown src id".to_string()));
continue;
}
if t.dst_id != CLOSED_ID && !known_ids.contains(&t.dst_id) {
errors.push(((t.src_id, t.dst_id), "unknown dst id".to_string()));
continue;
}
let Some(gp) = witnesses.get(&t.src_id) else {
errors.push(((t.src_id, t.dst_id), "no source witness".to_string()));
continue;
};
let pos = witness_pos[&t.src_id];
let n = gp.boundary_len();
let edge_pos = match t.side {
TransitionSide::Cw | TransitionSide::Both => (pos + n - 1) % n,
TransitionSide::Ccw => pos,
};
let cw_edge = (pos + n - 1) % n;
let ccw_edge = pos;
let edge_consumed = |pm: &PatchMatch, edge: usize| -> bool {
(edge + n - pm.a_range.start_offset) % n < pm.len()
};
let tile_len = tile_ts.rat(t.tile_id).len();
let mut found = false;
for pm in gp.get_all_matches() {
if pm.b.tile_id != t.tile_id {
continue;
}
if !edge_consumed(&pm, edge_pos) {
continue;
}
let offset_in_match = (edge_pos as i64 - pm.a_range.start_offset as i64)
.rem_euclid(n as i64) as usize;
let computed_offset = (pm.b.range.start_offset as i64 - 1 - offset_in_match as i64)
.rem_euclid(tile_len as i64) as usize;
if computed_offset != t.tile_offset {
continue;
}
let consumes_cw = edge_consumed(&pm, cw_edge);
let consumes_ccw = edge_consumed(&pm, ccw_edge);
let actual_side = match (consumes_cw, consumes_ccw) {
(true, true) => TransitionSide::Both,
(true, false) => TransitionSide::Cw,
(false, true) => TransitionSide::Ccw,
(false, false) => continue,
};
if actual_side != t.side {
continue;
}
let mut gp2 = gp.clone();
if !gp2.add_tile(&pm) {
continue;
}
if t.dst_id == CLOSED_ID {
found = true;
break;
}
let junction_pos = if pm.a_range.start_offset == pos {
n - pm.len()
} else {
0
};
if let Some(actual) = gp2.junction_vertex_type_at(junction_pos) {
let Some(dst_gp) = witnesses.get(&t.dst_id) else {
continue;
};
let Some(&dst_pos) = witness_pos.get(&t.dst_id) else {
continue;
};
if let Some(expected) = dst_gp.junction_vertex_type_at(dst_pos)
&& actual.cw == expected.cw
&& actual.ccw == expected.ccw
{
found = true;
break;
}
}
}
if !found {
errors.push(((t.src_id, t.dst_id), "no matching glue found".to_string()));
}
}
errors
}
pub fn completeness_errors<T>(
&self,
witnesses: &HashMap<usize, GrowingPatch<T>>,
) -> CompletenessReport
where
T: IsRing,
{
let mut missing: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
let mut matches_checked = 0usize;
for v in &self.vtypes {
if matches!(v.kind, VTypeKind::Dead | VTypeKind::Undead) {
continue;
}
let Some(gp) = witnesses.get(&v.id) else {
continue;
};
let pos = v.witness.pos;
let old_n = gp.boundary_len();
let edge_consumed = |pm: &PatchMatch, edge: usize| -> bool {
(edge + old_n - pm.a_range.start_offset) % old_n < pm.len()
};
for pm in gp.get_matches_touching_vertex(pos) {
matches_checked += 1;
let mut gp2 = gp.clone();
if !gp2.add_tile(&pm) {
continue;
}
let junction_pos = if pm.a_range.start_offset == pos {
old_n - pm.len()
} else {
0
};
let consumes_ccw = edge_consumed(&pm, pos);
let consumes_cw = edge_consumed(&pm, (pos + old_n - 1) % old_n);
if consumes_cw && consumes_ccw {
continue;
}
let Some(jvt) = gp2.junction_vertex_type_at(junction_pos) else {
continue;
};
let found = self.vtypes.iter().any(|other| other.vtype == jvt);
if !found {
missing.insert(v.id);
}
}
}
CompletenessReport {
matches_checked,
missing,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::{ZZ4, ZZ12};
use crate::geom::matches::{EdgeRange, Segment};
use crate::geom::tiles;
use crate::geom::tileset::{self, TileSet};
use std::sync::Arc;
#[test]
fn collection_roundtrip_validates_square_and_spectre() {
let cases: [(&str, Arc<TileSet<ZZ12>>); 2] = [
("square", tileset::square::<ZZ12>()),
("spectre", tileset::spectre::<ZZ12>()),
];
for (label, ts) in cases {
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
let collection = Collection::from_index(&idx, "ZZ12");
let json = serde_json::to_string(&collection).expect("serialize");
let restored: Collection = serde_json::from_str(&json).expect("deserialize");
let witnesses = restored.reconstruct_witnesses(&ts).expect("reconstruct");
let vt_errs = restored.vtype_errors(&witnesses);
assert!(vt_errs.is_empty(), "{label}: vtype errors: {vt_errs:?}");
let tr_errs = restored.transition_errors(&ts, &witnesses);
assert!(
tr_errs.is_empty(),
"{}: {} transition errors (first: {:?})",
label,
tr_errs.len(),
tr_errs.first(),
);
let report = restored.completeness_errors(&witnesses);
assert!(
report.is_complete(),
"{}: {} VTs produce unknown junction types: {:?}",
label,
report.missing.len(),
report.missing.iter().take(10).collect::<Vec<_>>(),
);
}
}
#[test]
fn hexagon_vertex_type_counts() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
let alive = idx.entries.iter().filter(|e| e.is_alive()).count();
let dead = idx.entries.iter().filter(|e| e.is_dead()).count();
let cursed = idx.entries.iter().filter(|e| e.is_cursed()).count();
let blessed = idx.entries.iter().filter(|e| e.is_blessed()).count();
let free = idx.entries.iter().filter(|e| e.is_free()).count();
assert_eq!(idx.num_types(), 36, "hex VT count");
assert_eq!(alive, 36, "every hex VT is alive");
assert_eq!(dead, 0, "no dead hex VTs");
assert_eq!(cursed, 0, "no cursed hex VTs");
assert_eq!(blessed + free, 36, "alive = blessed + free");
}
#[test]
fn square_vertex_type_counts() {
let sq: crate::geom::snake::Snake<ZZ4> = tiles::square();
let rat = Rat::try_from(&sq).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert_eq!(idx.num_types(), 80, "square VT count");
let alive = idx.entries.iter().filter(|e| e.is_alive()).count();
assert_eq!(alive, 80, "every square VT is alive");
}
#[test]
fn hexagon_transitions_well_formed() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
let n_types = idx.num_types();
for t in idx.transitions() {
assert!(
t.src_id >= 1 && t.src_id <= n_types,
"src_id {} out of range",
t.src_id
);
assert!(
t.dst_id == CLOSED_ID || (t.dst_id >= 1 && t.dst_id <= n_types),
"dst_id {} out of range (and not CLOSED)",
t.dst_id
);
assert!(
t.tile_id < ts.num_tiles(),
"tile_id {} out of range",
t.tile_id
);
let tile_len = ts.rat(t.tile_id).len();
assert!(
t.tile_offset < tile_len,
"tile_offset {} out of range (tile {} has {} edges)",
t.tile_offset,
t.tile_id,
tile_len
);
}
}
#[test]
fn hexagon_cursed_fixpoint_invariant() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
for info in idx.entries() {
if info.is_cursed() {
continue;
}
let id = idx.get_id(info.vtype()).unwrap();
let outgoing: Vec<&TransitionInfo> = idx
.transitions()
.iter()
.filter(|t| t.src_id == id)
.collect();
assert!(
!outgoing.is_empty(),
"non-cursed VT id {id} should have outgoing transitions"
);
for t in &outgoing {
if t.is_closed() {
continue;
}
let dst_info = idx.get_info(t.dst_id);
assert!(
!dst_info.is_dead(),
"non-cursed VT id {id} has open transition to Dead VT id {}",
t.dst_id
);
}
}
}
#[test]
fn hexagon_blessed_fixpoint_invariant() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
for info in idx.entries() {
if !info.is_blessed() {
continue;
}
let id = idx.get_id(info.vtype()).unwrap();
let outgoing: Vec<&TransitionInfo> = idx
.transitions()
.iter()
.filter(|t| t.src_id == id)
.collect();
assert!(
!outgoing.is_empty(),
"Blessed VT id {id} must have at least one transition"
);
for t in &outgoing {
assert!(
t.is_closed() || idx.get_info(t.dst_id).is_blessed(),
"Blessed VT id {id} has open transition to non-Blessed VT id {}",
t.dst_id
);
}
}
}
fn assert_catalog_validity<T>(idx: &OpenVertexTypeIndex<T>)
where
T: IsRing,
{
for info in idx.entries() {
let observed = info.witness().junction_vertex_type_at(info.witness_pos());
assert_eq!(
observed,
Some(info.vtype().clone()),
"validity violation: witness for catalog id {} does not \
realize its claimed VT",
idx.get_id(info.vtype()).unwrap()
);
}
}
fn assert_catalog_complete_brute<T>(idx: &OpenVertexTypeIndex<T>)
where
T: IsRing,
{
let ts = idx.tileset();
for info in idx.entries() {
let src_id = idx.get_id(info.vtype()).unwrap();
let witness = info.witness();
let pos = info.witness_pos();
let n = witness.boundary_len();
let boundary_rat = Rat::from_slice_unchecked(witness.angles());
let mut canonical: std::collections::BTreeSet<(usize, usize, usize, usize)> =
std::collections::BTreeSet::new();
for tile_id in 0..ts.num_tiles() {
let tile_rat = ts.rat(tile_id);
let m = tile_rat.len();
for start_a in 0..n {
for start_b in 0..m {
let (ext_start, len, ext_end) =
boundary_rat.get_match((start_a as i64, start_b as i64), tile_rat);
if len == 0 {
continue;
}
let es = ext_start.rem_euclid(n as i64) as usize;
let ee = ext_end.rem_euclid(m as i64) as usize;
canonical.insert((tile_id, es, len, ee));
}
}
}
for &(tile_id, ext_start, len, ext_end) in &canonical {
let in_range = |edge: usize| (edge + n - ext_start) % n < len;
let touches_cw = in_range((pos + n - 1) % n);
let touches_ccw = in_range(pos);
if !touches_cw && !touches_ccw {
continue;
}
let pm = PatchMatch::new(
EdgeRange::new(ext_start, len),
Segment::new(tile_id, EdgeRange::new(ext_end, len)),
);
let mut gp2 = witness.clone();
if !gp2.add_tile(&pm) {
continue;
}
for new_pos in 0..gp2.boundary_len() {
if !gp2.is_junction(new_pos) {
continue;
}
if let Some(nv) = gp2.junction_vertex_type_at(new_pos) {
assert!(
idx.get_id(&nv).is_some(),
"completeness violation: canonical glue \
(tile {tile_id}, start_a {ext_start}, start_b {ext_end}, len {len}) \
from src VT id {src_id} exposes VT at new_pos {new_pos} \
not in catalog"
);
}
}
}
}
}
#[test]
fn hexagon_catalog_validity() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert_catalog_validity(&idx);
}
#[test]
fn square_catalog_validity() {
let sq: crate::geom::snake::Snake<ZZ4> = tiles::square();
let rat = Rat::try_from(&sq).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert_catalog_validity(&idx);
}
#[test]
fn spectre_catalog_validity() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert_catalog_validity(&idx);
}
#[test]
fn hexagon_catalog_complete_brute() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert_catalog_complete_brute(&idx);
}
#[test]
fn square_catalog_complete_brute() {
let sq: crate::geom::snake::Snake<ZZ4> = tiles::square();
let rat = Rat::try_from(&sq).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert_catalog_complete_brute(&idx);
}
#[test]
fn vt_witness_touching_matches_match_brute() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
for info in idx.entries() {
let witness = info.witness();
let pos = info.witness_pos();
let n = witness.boundary_len();
let boundary_rat = Rat::from_slice_unchecked(witness.angles());
let mut filtered_brute: std::collections::BTreeSet<(usize, usize, usize, usize)> =
std::collections::BTreeSet::new();
for tile_id in 0..ts.num_tiles() {
let tile_rat = ts.rat(tile_id);
let m = tile_rat.len();
for start_a in 0..n {
for start_b in 0..m {
let (ns, len, ne) =
boundary_rat.get_match((start_a as i64, start_b as i64), tile_rat);
if len == 0 {
continue;
}
let ns_u = ns.rem_euclid(n as i64) as usize;
let ne_u = ne.rem_euclid(m as i64) as usize;
let in_range = |edge: usize| (edge + n - ns_u) % n < len;
if !in_range((pos + n - 1) % n) && !in_range(pos) {
continue;
}
if !crate::geom::glue::junctions_glueable(
witness.angles(),
ns_u,
len,
tile_rat.seq(),
ne_u,
) {
continue;
}
if boundary_rat
.try_glue_precomputed((ns, len, ne), tile_rat, true)
.is_ok()
{
filtered_brute.insert((tile_id, ns_u, len, ne_u));
}
}
}
}
let api_set: std::collections::BTreeSet<(usize, usize, usize, usize)> = witness
.get_matches_touching_vertex(pos)
.into_iter()
.map(|pm| {
(
pm.b.tile_id,
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
)
})
.collect();
assert_eq!(
filtered_brute,
api_set,
"VT id {} (witness n={n} pos={pos}): brute != api",
idx.get_id(info.vtype()).unwrap()
);
}
}
#[test]
#[ignore]
fn diagnose_spectre_touching_matches() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
let info = idx.get_info(1);
let witness = info.witness();
let pos = info.witness_pos();
let n = witness.boundary_len();
let boundary_rat = Rat::from_slice_unchecked(witness.angles());
let mut filtered_brute: std::collections::BTreeSet<(usize, usize, usize, usize)> =
std::collections::BTreeSet::new();
let mut unfiltered_brute: std::collections::BTreeSet<(usize, usize, usize, usize)> =
std::collections::BTreeSet::new();
for tile_id in 0..ts.num_tiles() {
let tile_rat = ts.rat(tile_id);
let m = tile_rat.len();
for start_a in 0..n {
for start_b in 0..m {
let (ns, len, ne) =
boundary_rat.get_match((start_a as i64, start_b as i64), tile_rat);
if len == 0 {
continue;
}
let ns_u = ns.rem_euclid(n as i64) as usize;
let ne_u = ne.rem_euclid(m as i64) as usize;
let in_range = |edge: usize| (edge + n - ns_u) % n < len;
if !in_range((pos + n - 1) % n) && !in_range(pos) {
continue;
}
unfiltered_brute.insert((tile_id, ns_u, len, ne_u));
if !crate::geom::glue::junctions_glueable(
witness.angles(),
ns_u,
len,
tile_rat.seq(),
ne_u,
) {
continue;
}
if boundary_rat
.try_glue_precomputed((ns, len, ne), tile_rat, true)
.is_ok()
{
filtered_brute.insert((tile_id, ns_u, len, ne_u));
}
}
}
}
let api_set: std::collections::BTreeSet<(usize, usize, usize, usize)> = witness
.get_matches_touching_vertex(pos)
.into_iter()
.map(|pm| {
(
pm.b.tile_id,
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset,
)
})
.collect();
eprintln!(
"VT id 1: n={n} pos={pos} unfiltered={} filtered={} api={}",
unfiltered_brute.len(),
filtered_brute.len(),
api_set.len()
);
let only_filt: Vec<_> = filtered_brute.difference(&api_set).collect();
let only_api: Vec<_> = api_set.difference(&filtered_brute).collect();
eprintln!(
" filtered-only-not-api: {} | api-only: {}",
only_filt.len(),
only_api.len()
);
let tile_seq = ts.rat(0).seq();
let bnd_angles = witness.angles();
let edges = witness.edges();
for x in only_filt.iter() {
let &(_tile, sa, len, sb) = *x;
let m = tile_seq.len();
let next_sa = (sa + len) % n;
let underlying_tile = edges[sa].tile_id;
let underlying_off = edges[sa].tile_offset;
let underlying_next_tile = edges[next_sa].tile_id;
let underlying_next_off = edges[next_sa].tile_offset;
let underlying_seq = ts.rat(underlying_tile).seq();
let underlying_next_seq = ts.rat(underlying_next_tile).seq();
let bnd_left = bnd_angles[sa] as i32 + tile_seq[sb] as i32;
let bnd_right = bnd_angles[next_sa] as i32 + tile_seq[(sb + m - len) % m] as i32;
let clean_left = underlying_seq[underlying_off] as i32 + tile_seq[sb] as i32;
let clean_right = underlying_next_seq[underlying_next_off] as i32
+ tile_seq[(sb + m - len) % m] as i32;
let clean_tile_a = ts.rat(underlying_tile);
let (ns_clean, len_clean, ne_clean) =
clean_tile_a.get_match((underlying_off as i64, sb as i64), ts.rat(0));
eprintln!(
" miss (sa={sa},len={len},sb={sb}): \
underlying_off={underlying_off} \
clean canonical at (off={underlying_off}, sb={sb}): \
(ns={ns_clean},len={len_clean},ne={ne_clean})"
);
let _ = (bnd_left, bnd_right, clean_left, clean_right);
}
let mti = crate::analysis::matchtypes::MatchTypeIndex::new(Arc::clone(&ts));
let cands = mti.candidates_starting_at(0, 0);
eprintln!(
" MatchTypeIndex.by_start[0][0] = {} candidates",
cands.len()
);
eprintln!(" filtered_brute entries:");
for x in filtered_brute.iter() {
let in_api = api_set.contains(x);
eprintln!(" brute {x:?} in_api={in_api}");
}
eprintln!(" api_set entries:");
for x in api_set.iter() {
eprintln!(" api {x:?}");
}
let mut juncs: Vec<usize> = Vec::new();
for i in 0..n {
let ei = witness.edges()[i];
if ts.rat(ei.tile_id).seq()[ei.tile_offset] != witness.angles()[i] {
juncs.push(i);
}
}
eprintln!(" junctions in witness: {juncs:?}");
eprintln!(
" edges[25]: tile_id={} tile_offset={}",
edges[25].tile_id, edges[25].tile_offset
);
for (i, e) in edges.iter().enumerate().skip(n - 3).take(3) {
eprintln!(
" edges[{i}]: tile_id={} tile_offset={}",
e.tile_id, e.tile_offset
);
}
for (i, e) in edges.iter().enumerate().take(3) {
eprintln!(
" edges[{i}]: tile_id={} tile_offset={}",
e.tile_id, e.tile_offset
);
}
let result = crate::geom::patch::GrowingPatch::<ZZ12>::compute_all_candidates(
&Arc::new(crate::analysis::matchtypes::MatchTypeIndex::new(
Arc::clone(&ts),
)),
witness.angles(),
witness.edges(),
);
eprintln!(" result[25] = {} entries:", result[25].len());
for pm in &result[25] {
eprintln!(
" cac: tile={} start_a={} len={} start_b={}",
pm.b.tile_id,
pm.a_range.start_offset,
pm.len(),
pm.b.range.start_offset
);
}
let bnd_rat = Rat::from_slice_unchecked(witness.angles());
let mut seen: std::collections::HashSet<(usize, usize, usize, usize)> =
std::collections::HashSet::new();
eprintln!(" Simulated compute_candidates_at_position(pos=25, tile_id=0, off=0):");
for c in cands {
let tile_b = ts.rat(c.tile_id);
let (ns, len, ne) = bnd_rat.get_match((25i64, c.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;
let gap_ok = crate::geom::glue::junctions_glueable(
witness.angles(),
ns_u,
len,
tile_b.seq(),
ne_u,
);
let key = (ns_u, len, ne_u, c.tile_id);
let already_seen = seen.contains(&key);
let glue_ok = bnd_rat
.try_glue_precomputed((ns, len, ne), tile_b, true)
.is_ok();
let dst = (c.tile_id, ns_u, len, ne_u);
let target_match = filtered_brute.contains(&dst) && dst.1 == 25;
if target_match || !already_seen {
eprintln!(
" cand sb={} len={} -> bnd ({ns_u},{len},{ne_u}) gap_ok={gap_ok} \
seen_already={already_seen} glue_ok={glue_ok} target={target_match}",
c.range.start_offset, c.range.len
);
}
seen.insert(key);
}
}
#[test]
#[ignore]
fn spectre_catalog_complete_brute() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert_catalog_complete_brute(&idx);
}
#[test]
fn range_by_cw_single_tile() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
let slice = idx.range_by_cw(0);
assert_eq!(
slice.len(),
idx.num_types(),
"single-tile tileset: all types have cw.tile_id == 0"
);
for info in slice {
assert_eq!(info.vtype().cw.tile_id, 0);
}
let empty = idx.range_by_cw(1);
assert!(empty.is_empty(), "no types with cw.tile_id == 1");
}
#[test]
fn range_by_cw_multi_tile() {
let sq: crate::geom::snake::Snake<ZZ12> = tiles::square();
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let sq_rat = Rat::try_from(&sq).unwrap();
let hex_rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![sq_rat, hex_rat]));
let idx = OpenVertexTypeIndex::new(ts);
let n = idx.num_types();
assert!(n > 0);
let sq_slice = idx.range_by_cw(0);
let hex_slice = idx.range_by_cw(1);
assert_eq!(
sq_slice.len() + hex_slice.len(),
n,
"partition must cover all types"
);
assert!(
!sq_slice.is_empty(),
"should have types starting with square"
);
assert!(!hex_slice.is_empty(), "should have types starting with hex");
for info in sq_slice {
assert_eq!(info.vtype().cw.tile_id, 0);
}
for info in hex_slice {
assert_eq!(info.vtype().cw.tile_id, 1);
}
let empty = idx.range_by_cw(2);
assert!(empty.is_empty());
}
#[test]
fn spectre_vertex_type_counts() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
let alive = idx.entries.iter().filter(|e| e.is_alive()).count();
let dead = idx.entries.iter().filter(|e| e.is_dead()).count();
let undead = idx.entries.iter().filter(|e| e.is_undead()).count();
let blessed = idx.entries.iter().filter(|e| e.is_blessed()).count();
let free = idx.entries.iter().filter(|e| e.is_free()).count();
let cursed = idx.entries.iter().filter(|e| e.is_cursed()).count();
eprintln!(
"spectre: {} types ({} alive: {} Blessed + {} Free, {} cursed: {} Dead + {} Undead, {} transitions)",
idx.num_types(),
alive,
blessed,
free,
cursed,
dead,
undead,
idx.transitions().len()
);
assert_eq!(idx.num_types(), 271, "spectre VT count");
assert_eq!(alive + cursed, 271, "alive + cursed = total");
assert_eq!(dead + undead, cursed, "cursed = Dead + Undead");
assert_eq!(blessed + free, alive, "alive = Blessed + Free");
assert_eq!(alive, 102, "spectre alive count");
assert_eq!(blessed, 82, "spectre blessed count");
assert_eq!(free, 20, "spectre free count");
assert_eq!(cursed, 169, "spectre cursed count");
assert_eq!(dead, 150, "spectre dead count");
assert_eq!(undead, 19, "spectre undead count");
assert_eq!(idx.transitions().len(), 414, "spectre transition count");
}
fn reconstruct_transition_match<T: IsRing>(
idx: &OpenVertexTypeIndex<T>,
ts: &Arc<TileSet<T>>,
t: &TransitionInfo,
) -> (
crate::geom::patch::GrowingPatch<T>,
usize,
crate::geom::matches::PatchMatch,
) {
let info = idx.entries().get(t.src_id - 1).expect("src VT in range");
let witness = info.witness();
let pos = info.witness_pos();
let n = witness.boundary_len();
let m = ts.rat(t.tile_id).seq().len();
let touching = witness.get_matches_touching_vertex(pos);
let matching: Vec<&crate::geom::matches::PatchMatch> = touching
.iter()
.filter(|pm| {
if pm.b.tile_id != t.tile_id {
return false;
}
let edge_in_match =
|edge: usize| -> bool { (edge + n - pm.a_range.start_offset) % n < pm.len() };
let consumes_cw = edge_in_match((pos + n - 1) % n);
let consumes_ccw = edge_in_match(pos);
let pm_side = match (consumes_cw, consumes_ccw) {
(true, true) => TransitionSide::Both,
(true, false) => TransitionSide::Cw,
(false, true) => TransitionSide::Ccw,
(false, false) => return false,
};
if pm_side != t.side {
return false;
}
let edge_pos = match pm_side {
TransitionSide::Ccw => pos,
_ => (pos + n - 1) % n,
};
let offset_in_match = (edge_pos as i64 - pm.a_range.start_offset as i64)
.rem_euclid(n as i64) as usize;
let computed = (pm.b.range.start_offset as i64 - 1 - offset_in_match as i64)
.rem_euclid(m as i64) as usize;
computed == t.tile_offset
})
.collect();
assert_eq!(
matching.len(),
1,
"transition {:?} (src=#{}) must reconstruct to exactly ONE \
PatchMatch on its witness (encoding injectivity)",
(t.dst_id, t.side, t.tile_id, t.tile_offset),
t.src_id,
);
(witness.clone(), pos, *matching[0])
}
fn assert_vt_transition_metadata_is_injective<T: IsRing>(
idx: &OpenVertexTypeIndex<T>,
ts: &Arc<TileSet<T>>,
) {
for t in idx.transitions() {
let (witness, pos, pm) = reconstruct_transition_match(idx, ts, t);
let n = witness.boundary_len();
let mut gp2 = witness.clone();
assert!(
gp2.add_tile(&pm),
"transition {:?}: add_tile failed on reconstructed glue",
(t.src_id, t.dst_id, t.side, t.tile_id, t.tile_offset)
);
if t.dst_id == CLOSED_ID {
let info = idx.entries().get(t.src_id - 1).expect("src VT in range");
let closed = ClosedVertexType::from_open_via_closure(info.vtype());
let recovered_cvt_id = idx.get_closed_id(&closed).expect("closed VT in catalog");
assert_eq!(
Some(recovered_cvt_id),
t.closed_vt_id,
"closing transition {:?} reconstructed closed_vt_id mismatch",
(t.src_id, t.side, t.tile_id, t.tile_offset)
);
} else {
let junction_pos = if pm.a_range.start_offset == pos {
n - pm.len()
} else {
0
};
let new_vt = gp2
.junction_vertex_type_at(junction_pos)
.unwrap_or_else(|| {
panic!(
"open transition {:?}: post-glue junction missing at pos {}",
(t.src_id, t.dst_id, t.side, t.tile_id, t.tile_offset),
junction_pos
)
});
let dst_info = idx.entries().get(t.dst_id - 1).expect("dst VT in range");
assert_eq!(
&new_vt,
dst_info.vtype(),
"open transition {:?}: reconstructed dst VT doesn't match catalog",
(t.src_id, t.dst_id, t.side, t.tile_id, t.tile_offset)
);
}
}
}
#[test]
fn hex_vt_transition_metadata_is_injective() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
assert_vt_transition_metadata_is_injective(&idx, &ts);
}
#[test]
fn square_vt_transition_metadata_is_injective() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::square();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
assert_vt_transition_metadata_is_injective(&idx, &ts);
}
#[test]
fn spectre_vt_transition_metadata_is_injective() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
assert_vt_transition_metadata_is_injective(&idx, &ts);
}
#[test]
fn spectre_old_encoding_would_collapse_exactly_six_transitions() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
let mut old_keyed: std::collections::HashSet<(usize, TransitionSide, usize, usize, usize)> =
std::collections::HashSet::new();
for t in idx.transitions() {
let (witness, pos, pm) = reconstruct_transition_match(&idx, &ts, t);
let n = witness.boundary_len();
let m = ts.rat(t.tile_id).seq().len();
let edge_pos = match t.side {
TransitionSide::Ccw => pos,
_ => (pos + n - 1) % n,
};
let offset_in_match =
(edge_pos as i64 - pm.a_range.start_offset as i64).rem_euclid(n as i64) as usize;
let old_tile_offset = (pm.b.range.start_offset as i64 + pm.len() as i64
- offset_in_match as i64)
.rem_euclid(m as i64) as usize;
let dst_key = if t.dst_id == CLOSED_ID {
t.closed_vt_id.unwrap_or(0) + 1_000_000
} else {
t.dst_id
};
old_keyed.insert((t.src_id, t.side, t.tile_id, old_tile_offset, dst_key));
}
let collapsed_under_old = idx.transitions().len() - old_keyed.len();
assert_eq!(
collapsed_under_old,
6,
"expected exactly 6 OLD-encoding collisions in spectre catalog \
(new total {}, OLD-keyed total {})",
idx.transitions().len(),
old_keyed.len()
);
}
#[test]
fn spectre_transitions_well_formed() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
let n_types = idx.num_types();
for t in idx.transitions() {
assert!(
t.src_id >= 1 && t.src_id <= n_types,
"src_id {} out of range",
t.src_id
);
assert!(
t.dst_id == CLOSED_ID || (t.dst_id >= 1 && t.dst_id <= n_types),
"dst_id {} out of range (and not CLOSED)",
t.dst_id
);
assert!(t.tile_id < ts.num_tiles());
assert!(t.tile_offset < ts.rat(t.tile_id).len());
}
}
#[test]
fn spectre_cursed_invariant() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
for info in idx.entries() {
if !info.is_cursed() {
continue;
}
let id = idx.get_id(info.vtype()).unwrap();
let outgoing: Vec<&TransitionInfo> = idx
.transitions()
.iter()
.filter(|t| t.src_id == id)
.collect();
if info.is_dead() {
assert!(
outgoing.is_empty(),
"Dead VT id {id} must have no transitions, got {}",
outgoing.len()
);
continue;
}
assert!(
!outgoing.is_empty(),
"Undead VT id {id} must have at least one transition"
);
for t in &outgoing {
assert!(
!t.is_closed(),
"Undead VT id {id} has a closing transition — \
should have been classified Free"
);
let dst_info = idx.get_info(t.dst_id);
assert!(
dst_info.is_cursed(),
"Undead VT id {id} has open transition to non-cursed VT id {}",
t.dst_id
);
}
}
}
#[test]
fn spectre_blessed_invariant() {
let s: crate::geom::snake::Snake<ZZ12> = tiles::spectre();
let rat = Rat::try_from(&s).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(Arc::clone(&ts));
for info in idx.entries() {
if !info.is_blessed() {
continue;
}
let id = idx.get_id(info.vtype()).unwrap();
let outgoing: Vec<&TransitionInfo> = idx
.transitions()
.iter()
.filter(|t| t.src_id == id)
.collect();
assert!(
!outgoing.is_empty(),
"Blessed VT id {id} has no transitions"
);
for t in &outgoing {
assert!(
t.is_closed() || idx.get_info(t.dst_id).is_blessed(),
"Blessed VT id {id} has open transition to non-Blessed VT id {}",
t.dst_id
);
}
}
}
#[test]
fn hex_segment_types() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
eprintln!("hex: {} vertex types", idx.num_types());
}
#[test]
fn hexagon_closed_vertex_types_are_discovered() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert!(
idx.num_closed_types() >= 1,
"hex has at least one closed VT"
);
for cvt in idx.closed_entries() {
assert!(!cvt.vtype().is_empty());
assert!(cvt.transition_count() >= 1);
for e in cvt.vtype().edges() {
assert_eq!(e.tile_id, 0, "only one tile in this tileset");
assert!(e.tile_offset < 6);
}
}
let n_closing: usize = idx.transitions().iter().filter(|t| t.is_closed()).count();
assert!(n_closing > 0, "hex has at least one closing transition");
let total_per_vt: usize = idx
.closed_entries()
.iter()
.map(|c| c.transition_count())
.sum();
assert_eq!(
total_per_vt, n_closing,
"sum of per-closed-VT transition counts equals total closings"
);
}
#[test]
fn square_closed_vertex_types_are_discovered() {
let sq: crate::geom::snake::Snake<ZZ4> = tiles::square();
let rat = Rat::try_from(&sq).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
assert!(
idx.num_closed_types() >= 1,
"square has at least one closed VT"
);
for cvt in idx.closed_entries() {
assert!(!cvt.vtype().is_empty());
assert!(cvt.transition_count() >= 1);
for e in cvt.vtype().edges() {
assert_eq!(e.tile_id, 0);
assert!(e.tile_offset < 4);
}
}
}
#[test]
fn closed_vts_are_lex_min_canonical() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
for cvt in idx.closed_entries() {
let canonical = ClosedVertexType::from_cyclic(cvt.vtype().edges());
assert_eq!(cvt.vtype(), &canonical, "closed VT not in canonical form");
}
}
#[test]
fn closing_transitions_resolve_to_canonical_closed_vts() {
let hex: crate::geom::snake::Snake<ZZ12> = tiles::hexagon();
let rat = Rat::try_from(&hex).unwrap();
let ts = Arc::new(TileSet::new(vec![rat]));
let idx = OpenVertexTypeIndex::new(ts);
for t in idx.transitions() {
if t.is_closed() {
let cvt_id = t.closed_vt_id.expect("closing transition has closed_vt_id");
assert!(cvt_id >= 1 && cvt_id <= idx.num_closed_types());
let cvt = idx.get_closed_type(cvt_id);
let src_open = idx.get_type(t.src_id);
let expected = ClosedVertexType::from_open_via_closure(src_open);
assert_eq!(
cvt,
&expected,
"closed VT for transition {:?} differs from its derived form",
(t.src_id, t.tile_id)
);
} else {
assert!(
t.closed_vt_id.is_none(),
"non-closing transition has unexpected closed_vt_id"
);
}
}
}
}