tilezz 0.2.0

Utilities to work with perfect-precision polygonal tiles built on top of cyclotomic integer rings.
Documentation
//! Constructed-tiling infrastructure: the shared shape every periodic
//! detector's positive takes -- a [`Tiling`] (generators + lattice + placed
//! orbit) -- and the EXACT verification that gates every accept.
//!
//! The detectors (Conway, isohedral, anisohedral cluster) only ever PROPOSE
//! generators; [`verify_tiling`] builds nothing on trust. Its core,
//! `tile_exactly_surrounded`, checks a placed tile is exactly surrounded --
//! every corner's incident interior angles sum to a full turn and every edge
//! is shared by exactly two placements -- in exact ring arithmetic, so a wrong
//! generator or a float-noise proposal can never be accepted.

use rustc_hash::FxHashMap;

use crate::cyclotomic::IsRing;
use crate::cyclotomic::geometry::cmp_xy;
use crate::geom::iso::Iso;

/// Orbit radius (centroid distance) for building a tiling / isohedral patch, as a
/// multiple of the tile perimeter: `radius = ORBIT_RADIUS_FACTOR * seq.len()`.
/// Large enough that the central tile is fully surrounded (its whole corona is
/// present) so gap/overlap checks are meaningful, without over-growing.
pub const ORBIT_RADIUS_FACTOR: f64 = 2.4;

/// Placement cap for detector-built single-tile orbits (Conway / isohedral):
/// comfortably fills an `ORBIT_RADIUS_FACTOR` disk at the perimeters we
/// screen; a non-discrete generator set hits it and is rejected by the
/// verifier.
pub(crate) const DETECT_ORBIT_CAP: usize = 2_000;

/// Placement cap for CLUSTER (meta-tile) orbits: fewer copies fill the same
/// disk (each copy is k tiles), but the boundary is craggier, so keep margin.
pub(crate) const CLUSTER_ORBIT_CAP: usize = 2_500;

/// Polygon area (unit-edge-squared) below which a float area is treated as
/// degenerate/zero. Exact ring shapes have area >= the unit triangle (~0.43),
/// so only a genuinely broken polygon lands below this.
pub(crate) const AREA_EPS: f64 = 1e-9;

/// A constructed periodic tiling around one tile: two translational lattice
/// vectors and the placed tiles (as `Iso`s). (The generating isometries are
/// consumed by the orbit build and not retained -- nothing downstream reads
/// them; the cert layer re-derives its own generators from the glue.)
pub struct Tiling<T> {
    pub verts: Vec<T>,
    pub seq: Vec<i8>,
    pub lattice: (T, T),
    pub placements: Vec<Iso<T>>,
}

/// Outcome of locally verifying a constructed tiling around its central tile.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TilingCheck {
    /// Every vertex of the central tile has interior angles of incident tiles
    /// summing to exactly a full turn (no gap, no overlap).
    pub center_angles_full: bool,
    /// Every edge of the central tile is shared with exactly one neighbour.
    pub center_edges_shared: bool,
    /// The patch interior is invariant under both lattice translations.
    pub periodic: bool,
}

impl TilingCheck {
    pub fn ok(&self) -> bool {
        self.center_angles_full && self.center_edges_shared && self.periodic
    }
}

/// Canonical unordered edge key.
fn canon<T: IsRing>(p: T, q: T) -> (T, T) {
    if cmp_xy(&p, &q) == std::cmp::Ordering::Less {
        (p, q)
    } else {
        (q, p)
    }
}

/// The exact surroundedness accounting of a placement set: the interior-angle
/// sum landing on every vertex coordinate (a fully-surrounded interior vertex
/// sums to a full turn) and the placement count on every unordered edge.
struct SurroundMaps<T> {
    angle_at: FxHashMap<T, i64>,
    edge_count: FxHashMap<(T, T), usize>,
}

fn surround_maps<T: IsRing>(verts: &[T], seq: &[i8], placements: &[Iso<T>]) -> SurroundMaps<T> {
    let n = verts.len();
    let turn = T::turn() as i64;
    let half = turn / 2;
    let mut angle_at: FxHashMap<T, i64> = FxHashMap::default();
    let mut edge_count: FxHashMap<(T, T), usize> = FxHashMap::default();
    for iso in placements {
        let pts = iso.tile(verts);
        for j in 0..n {
            *angle_at.entry(pts[j]).or_insert(0) += half - seq[j] as i64;
            *edge_count
                .entry(canon(pts[j], pts[(j + 1) % n]))
                .or_insert(0) += 1;
        }
    }
    SurroundMaps {
        angle_at,
        edge_count,
    }
}

/// Whether `center` is exactly surrounded per the accounting in `maps`: every
/// corner's incident interior angles sum to exactly a full turn (no gap, no
/// overlap) and every edge is shared by exactly two placements.
fn center_exact<T: IsRing>(maps: &SurroundMaps<T>, verts: &[T], center: &Iso<T>) -> (bool, bool) {
    let n = verts.len();
    let turn = T::turn() as i64;
    let cpts = center.tile(verts);
    let angles_full = (0..n).all(|j| maps.angle_at.get(&cpts[j]) == Some(&turn));
    let edges_shared =
        (0..n).all(|j| maps.edge_count.get(&canon(cpts[j], cpts[(j + 1) % n])) == Some(&2));
    (angles_full, edges_shared)
}

/// Exact local surroundedness of one placed tile within `placements` (which
/// must include the tile itself). Returns the two checks separately as
/// `(angles_full, edges_shared)`. All comparisons are exact (ring-element
/// equality, integer angle units of `T::turn()` per full turn) -- this is THE
/// local exact check behind [`verify_tiling`] and (via
/// [`all_exactly_surrounded`]) the torus
/// [`gold_check`](crate::classify::torus::gold_check).
pub(crate) fn tile_exactly_surrounded<T: IsRing>(
    verts: &[T],
    seq: &[i8],
    placements: &[Iso<T>],
    center: &Iso<T>,
) -> (bool, bool) {
    let maps = surround_maps(verts, seq, placements);
    center_exact(&maps, verts, center)
}

/// Exact surroundedness of EVERY placement in `centers` within `placements`
/// (the accounting maps are built once). This is the k>1-sound form of the
/// check: a single checked tile only certifies its own translation class --
/// the lattice maps each class to itself, so a defect strictly between tiles
/// of the OTHER classes would go unseen. Checking one representative per
/// class extends, by lattice invariance, to every tile of the infinite
/// configuration, and "every tile exactly surrounded" rules out any gap or
/// overlap anywhere with no transitivity caveat.
pub(crate) fn all_exactly_surrounded<T: IsRing>(
    verts: &[T],
    seq: &[i8],
    placements: &[Iso<T>],
    centers: &[Iso<T>],
) -> bool {
    let maps = surround_maps(verts, seq, placements);
    centers.iter().all(|c| {
        let (a, e) = center_exact(&maps, verts, c);
        a && e
    })
}

/// Verify a constructed tiling locally. All checks are exact (lattice-point
/// equality and integer angle units; one full turn = `T::turn()` units).
pub fn verify_tiling<T: IsRing>(t: &Tiling<T>) -> TilingCheck {
    let (center_angles_full, center_edges_shared) =
        tile_exactly_surrounded(&t.verts, &t.seq, &t.placements, &Iso::id());

    // Periodicity: every placement well inside the patch has both lattice
    // translates present (translation keeps the sign, adds to the shift).
    let placed: std::collections::HashSet<Iso<T>> = t.placements.iter().copied().collect();
    let (v1, v2) = t.lattice;
    let nonzero = v1.xy() != (0.0, 0.0) && v2.xy() != (0.0, 0.0);
    // Interior = centroid within half the MAXIMUM centroid distance.
    let extent = t
        .placements
        .iter()
        .map(|p| {
            let (x, y) = p.centroid(&t.verts);
            (x * x + y * y).sqrt()
        })
        .fold(0.0_f64, f64::max);
    let interior_r = extent * 0.5;
    let periodic = nonzero
        && t.placements.iter().all(|p| {
            let (x, y) = p.centroid(&t.verts);
            if (x * x + y * y).sqrt() > interior_r {
                return true; // boundary placement: not required to be invariant
            }
            let t1 = Iso {
                rot: p.rot,
                shift: p.shift + v1,
            };
            let t2 = Iso {
                rot: p.rot,
                shift: p.shift + v2,
            };
            placed.contains(&t1) && placed.contains(&t2)
        });

    TilingCheck {
        center_angles_full,
        center_edges_shared,
        periodic,
    }
}