use std::collections::BTreeMap;
use std::sync::Arc;
use rustc_hash::FxHashSet;
use crate::analysis::matchfinder::{BpSeed, MatchFinder};
use crate::cyclotomic::IsRing;
use crate::geom::rat::Rat;
use crate::geom::tileset::TileSet;
pub fn enum_patches<T>(
tileset: Arc<TileSet<T>>,
max_size: usize,
) -> BTreeMap<usize, FxHashSet<Rat<T>>>
where
T: IsRing,
{
let mut results: BTreeMap<usize, FxHashSet<Rat<T>>> = BTreeMap::new();
if max_size == 0 || tileset.num_tiles() == 0 {
return results;
}
results.insert(1, tileset.rats().iter().cloned().collect());
let bp_seed = BpSeed::new(Arc::clone(&tileset));
for k in 2..=max_size {
let prev = match results.get(&(k - 1)) {
Some(s) if !s.is_empty() => s,
_ => break,
};
let patches_vec: Vec<Rat<T>> = prev.iter().cloned().collect();
let patch_ts = Arc::new(TileSet::new(patches_vec));
let mf = MatchFinder::crossing_with_seed(Arc::clone(&patch_ts), bp_seed.clone());
let pairs: Vec<(usize, usize)> = (0..mf.num_tiles_a())
.flat_map(|i| (0..mf.num_tiles_b()).map(move |j| (i, j)))
.collect();
let layer: FxHashSet<Rat<T>> = mf.valid_results_for_pairs(&pairs).into_iter().collect();
if layer.is_empty() {
break;
}
results.insert(k, layer);
}
results
}
pub fn enum_patches_free<T>(
tileset: Arc<TileSet<T>>,
max_size: usize,
) -> BTreeMap<usize, FxHashSet<Rat<T>>>
where
T: IsRing,
{
enum_patches(tileset, max_size)
.into_iter()
.map(|(k, set)| {
let free: FxHashSet<Rat<T>> = set
.into_iter()
.map(|r| std::cmp::min(r.clone(), r.reflected()))
.collect();
(k, free)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::{ZZ4, ZZ12};
use crate::geom::tileset;
fn brute_force_grow<T>(
tileset: &Arc<TileSet<T>>,
max_size: usize,
) -> BTreeMap<usize, FxHashSet<Rat<T>>>
where
T: IsRing,
{
let mut results: BTreeMap<usize, FxHashSet<Rat<T>>> = BTreeMap::new();
if max_size == 0 || tileset.num_tiles() == 0 {
return results;
}
results.insert(1, tileset.rats().iter().cloned().collect());
for k in 2..=max_size {
let prev: Vec<Rat<T>> = results[&(k - 1)].iter().cloned().collect();
let mut next: FxHashSet<Rat<T>> = FxHashSet::default();
for patch in &prev {
for tile_idx in 0..tileset.num_tiles() {
let tile = tileset.rat(tile_idx);
for ia in 0..patch.len() {
for ib in 0..tile.len() {
if let Ok(glued) = patch.try_glue((ia as i64, ib as i64), tile) {
next.insert(glued);
}
}
}
}
}
results.insert(k, next);
}
results
}
fn assert_grow_matches_brute_force<T>(label: &str, tileset: Arc<TileSet<T>>, max_size: usize)
where
T: IsRing,
{
let fast = enum_patches(Arc::clone(&tileset), max_size);
let brute = brute_force_grow(&tileset, max_size);
for k in 1..=max_size {
let fast_set = fast.get(&k).cloned().unwrap_or_default();
let brute_set = brute.get(&k).cloned().unwrap_or_default();
assert_eq!(
fast_set.len(),
brute_set.len(),
"{label} size {k}: enum_patches={} brute_force={}",
fast_set.len(),
brute_set.len()
);
for r in &brute_set {
assert!(
fast_set.contains(r),
"{label} size {k}: brute force has rat {r:?} that enum_patches missed"
);
}
for r in &fast_set {
assert!(
brute_set.contains(r),
"{label} size {k}: enum_patches has rat {r:?} that brute force missed"
);
}
}
}
const FREE_POLYOMINOES_NO_HOLES: &[usize] = &[
1, 1, 1, 2, 5, 12, 35, 107, 363, 1248, 4460, 16094, 58937, 217117,
];
#[test]
fn square_zz4_free_polyominoes_match_oeis() {
let max_size = 9;
let ts = tileset::square::<ZZ4>();
let free = enum_patches_free(ts, max_size);
for (k, &expected) in FREE_POLYOMINOES_NO_HOLES
.iter()
.enumerate()
.take(max_size + 1)
.skip(1)
{
let got = free.get(&k).map(|s| s.len()).unwrap_or(0);
assert_eq!(got, expected, "size {k}: got {got}, expected {expected}");
}
}
#[test]
fn hex_zz12_matches_brute_force_size4() {
assert_grow_matches_brute_force("hex", tileset::hex::<ZZ12>(), 4);
}
#[test]
fn spectre_zz12_matches_brute_force_size3() {
assert_grow_matches_brute_force("spectre", tileset::spectre::<ZZ12>(), 3);
}
#[test]
fn mixed_square_hex_zz12_matches_brute_force_size3() {
assert_grow_matches_brute_force("mixed", tileset::mixed::<ZZ12>(), 3);
}
#[test]
fn tetrominoes_zz4_matches_brute_force_size3() {
assert_grow_matches_brute_force("tetrominoes", tileset::tetrominoes::<ZZ4>(), 3);
}
#[test]
fn max_size_zero_returns_empty() {
let result = enum_patches(tileset::square::<ZZ4>(), 0);
assert!(result.is_empty());
}
#[test]
fn size_one_equals_input_tileset() {
let ts = tileset::mixed::<ZZ12>();
let result = enum_patches(Arc::clone(&ts), 1);
let size_1 = result.get(&1).expect("size 1 must be present");
assert_eq!(size_1.len(), ts.num_tiles());
for r in ts.rats() {
assert!(size_1.contains(r));
}
}
}