use crate::classify::heesch::{BurySearch, TileSink, bury};
use crate::cyclotomic::IsRing;
use crate::geom::iso::{Iso, dir_of_unit};
use crate::geom::matches::PatchMatch;
use crate::geom::patch::{BasicPatch, IPatch, Patch, WithAdjacency, boundary_vertices};
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
use crate::geom::vertices::OpenJunctionType;
pub(crate) fn capture_placement<T: IsRing, P: Patch<T>>(
gp: &P,
v: &[T],
k: usize,
) -> Option<Iso<T>> {
let n = v.len();
let pos = gp.patch_tile_ids().iter().position(|&x| x == k)?;
let off = gp.edges()[pos].canon_offset;
let bp = gp.boundary_positions();
let (p0, p1) = (bp[pos], bp[pos + 1]);
let src = dir_of_unit(v[(off + 1) % n] - v[off])?;
let dst = dir_of_unit(p1 - p0)?;
Some(Iso::carrying(v[off], src, p0, dst))
}
struct GrowSink<'a, T: IsRing> {
out: &'a mut Vec<(usize, Iso<T>)>,
v: &'a [T],
target: usize,
patch: Option<IPatch<T>>,
}
impl<T: IsRing> TileSink<T, WithAdjacency<BasicPatch<T>>> for GrowSink<'_, T> {
fn push(
&mut self,
_pm: &PatchMatch,
patch: &WithAdjacency<BasicPatch<T>>,
new_id: usize,
) -> bool {
let Some(iso) = capture_placement(patch, self.v, new_id) else {
return false; };
self.out.push((new_id, iso));
true
}
fn pop(&mut self) {
self.out.pop();
}
fn mark(&mut self, depth: usize, patch: &WithAdjacency<BasicPatch<T>>) {
if depth == self.target {
self.patch = Some(patch.clone());
}
}
}
pub(crate) fn replay_recipe<T: IsRing>(
base: &Rat<T>,
build: &[PatchMatch],
mut observe: impl FnMut(&BasicPatch<T>, usize) -> bool,
) -> Option<BasicPatch<T>> {
let seed = BasicPatch::single_tile(TileSet::single(base.clone()), 0);
let mut gp = seed.with_tile(&build[0])?;
if !observe(&gp, 1) {
return None;
}
for pm in &build[1..] {
let new_id = gp.next_tile_id();
gp.add_tile(pm)?;
if !observe(&gp, new_id) {
return None;
}
}
Some(gp)
}
pub fn replay_placements<T: IsRing>(base: &Rat<T>, build: &[PatchMatch]) -> Option<Vec<Iso<T>>> {
let v = boundary_vertices::<T>(base.seq());
if build.is_empty() {
return Some(vec![Iso::id()]);
}
let mut out = Vec::new();
replay_recipe(base, build, |gp, new_id| {
let first = if new_id == 1 { 0 } else { new_id };
for id in first..=new_id {
match capture_placement(gp, &v, id) {
Some(iso) => out.push(iso),
None => return false,
}
}
true
})?;
let i0inv = out[0].inv();
Some(out.into_iter().map(|iso| i0inv.after(&iso)).collect())
}
pub fn grow_coronas<T: IsRing>(tile_seq: &[i8], n_coronas: usize) -> Vec<Iso<T>> {
grow_coronas_build(tile_seq, n_coronas).map_or_else(Vec::new, |(pls, _)| pls)
}
pub fn grow_coronas_build<T: IsRing>(
tile_seq: &[i8],
n_coronas: usize,
) -> Option<(Vec<Iso<T>>, IPatch<T>)> {
let ts = TileSet::single(Rat::<T>::from_slice_trusted(tile_seq));
let v = boundary_vertices::<T>(tile_seq);
let seed = WithAdjacency::single_tile(ts, 0);
let empty_frozen: rustc_hash::FxHashSet<T> = rustc_hash::FxHashSet::default();
let empty_cursed: rustc_hash::FxHashSet<OpenJunctionType> = rustc_hash::FxHashSet::default();
const GROW_BUDGET: usize = 1_500_000;
let mut ctx = BurySearch::new(n_coronas, GROW_BUDGET, &empty_cursed, true);
for first in seed.get_all_matches() {
let mut gp = seed.clone();
if gp.add_tile(&first).is_none() {
continue;
}
let mut out: Vec<(usize, Iso<T>)> = Vec::new();
if let Some(i0) = capture_placement(&gp, &v, 0) {
out.push((0, i0));
}
if let Some(i1) = capture_placement(&gp, &v, 1) {
out.push((1, i1));
}
let mut sink = GrowSink {
out: &mut out,
v: &v,
target: n_coronas,
patch: None,
};
let reached =
bury(&gp, 1, &empty_frozen, 0, &mut ctx, &mut sink, &|_, _| None) >= n_coronas;
let patch = sink.patch;
if reached {
let patch = patch.expect("winning closure fired mark(n_coronas)");
out.sort_by_key(|&(k, _)| k);
let i0inv = out[0].1.inv();
let pls = out.into_iter().map(|(_, iso)| i0inv.after(&iso)).collect();
return Some((pls, patch));
}
if ctx.budget_hit || ctx.spent >= GROW_BUDGET {
break;
}
}
None
}
pub(crate) fn replay_placements_graph<T: IsRing>(
base: &Rat<T>,
build: &[PatchMatch],
) -> Option<(Vec<Iso<T>>, IPatch<T>)> {
if build.is_empty() {
return None;
}
let v = boundary_vertices::<T>(base.seq());
let ts = TileSet::single(base.clone());
let mut gp = WithAdjacency::single_tile(ts, 0);
gp.add_tile(&build[0])?;
let mut out = vec![
capture_placement(&gp, &v, 0)?,
capture_placement(&gp, &v, 1)?,
];
for pm in &build[1..] {
let new_id = gp.next_tile_id();
gp.add_tile(pm)?;
out.push(capture_placement(&gp, &v, new_id)?);
}
let i0inv = out[0].inv();
let pls = out.into_iter().map(|iso| i0inv.after(&iso)).collect();
Some((pls, gp))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::heesch::heesch_number_witnessed;
use crate::cyclotomic::ZZ12;
use crate::geom::tiles;
#[test]
fn replay_placements_reconstructs_witness_patch() {
let tri = Rat::<ZZ12>::from_snake_trusted(&tiles::triangle());
let (h, build) = heesch_number_witnessed(TileSet::single(tri.clone()), 0, 1, 200_000);
assert!(!h.cannot_tile() && !build.is_empty());
let pls = replay_placements(&tri, &build).expect("witness replays");
assert_eq!(pls.len(), build.len() + 1, "one placement per placed copy");
assert_eq!(pls[0], Iso::id(), "base normalized to the identity");
let distinct: std::collections::HashSet<_> = pls.iter().copied().collect();
assert_eq!(distinct.len(), pls.len(), "no duplicate placements");
assert_eq!(replay_placements(&tri, &[]), Some(vec![Iso::id()]));
}
}