use std::collections::HashSet;
use crate::cyclotomic::IsRing;
use crate::enumerate::canonical::{free_canonical, make_ops};
use crate::enumerate::dfs::rat_enum_with;
use crate::enumerate::prune::Prunes;
use crate::enumerate::stats::DfsStats;
use crate::geom::snake::Snake;
fn rat_enum<ZZ: IsRing>(max_steps: usize, step: i8) -> (Vec<Vec<i8>>, DfsStats) {
rat_enum_with::<ZZ, Snake<ZZ>, _>(
|seq| Snake::<ZZ>::from_slice_trusted(seq),
max_steps,
step,
make_ops(false),
"enumeration",
"",
false,
&Prunes::default(),
)
}
fn rat_enum_free<ZZ: IsRing>(max_steps: usize, step: i8) -> (Vec<Vec<i8>>, DfsStats) {
rat_enum_with::<ZZ, Snake<ZZ>, _>(
|seq| Snake::<ZZ>::from_slice_trusted(seq),
max_steps,
step,
make_ops(true),
"free enumeration",
"free ",
false,
&Prunes::default(),
)
}
#[cfg(test)]
mod free_tests {
use super::*;
use crate::cyclotomic::geometry::intersect;
use crate::cyclotomic::{IsRing, Units, ZZ4, ZZ8, ZZ12};
use std::collections::HashMap;
fn validate_simple_polygon<ZZ: IsRing>(angles: &[i8]) -> Result<(), String> {
let n = angles.len();
if n < 3 {
return Err(format!("perimeter {n} < 3"));
}
let mut pts: Vec<ZZ> = Vec::with_capacity(n + 1);
pts.push(ZZ::zero());
let mut dir: i64 = 0;
for &a in angles {
dir = (dir + a as i64).rem_euclid(ZZ::turn() as i64);
let next = *pts.last().unwrap() + <ZZ as Units>::unit(dir as i8);
pts.push(next);
}
if !pts.last().unwrap().is_zero() {
return Err(format!("does not close: last={:?}", pts.last().unwrap()));
}
pts.pop();
let mut seen: HashMap<ZZ, usize> = HashMap::new();
for (i, &p) in pts.iter().enumerate() {
if let Some(&j) = seen.get(&p) {
return Err(format!("duplicate vertex: pts[{j}] == pts[{i}]"));
}
seen.insert(p, i);
}
for i in 0..n {
let a1 = pts[i];
let a2 = pts[(i + 1) % n];
for j in (i + 2)..n {
if i == 0 && j == n - 1 {
continue;
}
let b1 = pts[j];
let b2 = pts[(j + 1) % n];
if intersect(&(a1, a2), &(b1, b2)) {
return Err(format!(
"edges {i} ({a1:?}->{a2:?}) and {j} ({b1:?}->{b2:?}) intersect/touch"
));
}
}
}
Ok(())
}
#[test]
#[cfg_attr(
debug_assertions,
ignore = "release-only: full A316192 pin to n>=10 takes ~12 min debug / ~30 s release"
)]
fn test_oeis_a316192_zz12() {
const OEIS: &[(usize, usize)] = &[
(3, 1),
(4, 3),
(5, 4),
(6, 22),
(7, 69),
(8, 418),
(9, 2210),
(10, 14024),
];
let max_n = OEIS.iter().map(|&(n, _)| n).max().unwrap();
let (rats, _) = super::rat_enum_free::<ZZ12>(max_n, 1);
let mut by_len: std::collections::BTreeMap<usize, usize> =
std::collections::BTreeMap::new();
for seq in &rats {
*by_len.entry(seq.len()).or_insert(0) += 1;
}
let mut mismatches: Vec<(usize, usize, usize)> = Vec::new();
for &(n, expected) in OEIS {
let got = by_len.get(&n).copied().unwrap_or(0);
if got != expected {
mismatches.push((n, got, expected));
}
}
if !mismatches.is_empty() {
for (n, got, expected) in &mismatches {
eprintln!(
"n={n}: got {got}, expected (OEIS A316192) {expected}, diff {:+}",
*got as i64 - *expected as i64
);
}
panic!(
"free ZZ12 enumeration differs from OEIS A316192 at {} perimeter length(s)",
mismatches.len()
);
}
}
#[test]
fn test_free_output_polygons_are_simple_and_unique() {
let (free_rats, _) = super::rat_enum_free::<ZZ12>(9, 1);
let unique: std::collections::HashSet<Vec<i8>> = free_rats.iter().cloned().collect();
assert_eq!(
unique.len(),
free_rats.len(),
"duplicate sequences in free output"
);
let mut failures: Vec<(Vec<i8>, String)> = Vec::new();
for seq in &free_rats {
if let Err(why) = validate_simple_polygon::<ZZ12>(seq) {
failures.push((seq.clone(), why));
}
}
if !failures.is_empty() {
eprintln!("{} polygons failed independent validation:", failures.len());
for (seq, why) in failures.iter().take(20) {
eprintln!(" {seq:?} -- {why}");
}
panic!("non-simple polygons in free enumeration output");
}
}
fn check_quotient_match<ZZ: IsRing>(ring_label: &str, max_steps: usize) {
let (all_rats, _) = super::rat_enum::<ZZ>(max_steps, 1);
let mut expected: HashSet<Vec<i8>> = HashSet::new();
for seq in &all_rats {
expected.insert(super::free_canonical(seq));
}
let (free_rats, _) = super::rat_enum_free::<ZZ>(max_steps, 1);
let actual: HashSet<Vec<i8>> = free_rats.into_iter().collect();
if expected != actual {
for s in expected.difference(&actual) {
eprintln!("[{ring_label} n={max_steps}] MISSING: {s:?}");
}
for s in actual.difference(&expected) {
eprintln!("[{ring_label} n={max_steps}] EXTRA: {s:?}");
}
}
assert_eq!(
expected, actual,
"{ring_label} n={max_steps}: free DFS != rotation DFS / free",
);
eprintln!(
"[{ring_label} n={max_steps}] OK -- {} free classes from {} rotation-canonical rats",
actual.len(),
all_rats.len(),
);
}
#[test]
#[cfg_attr(
debug_assertions,
ignore = "release-only: ZZ12 n<=9 + ZZ8 n<=12 dihedral cross-check is ~9 min debug / ~25 s release"
)]
fn test_free_enum_matches_dfs_quotient() {
for n in [4, 5, 6, 7, 8, 9] {
check_quotient_match::<ZZ12>("ZZ12", n);
}
for n in [4, 6, 8, 10, 12] {
check_quotient_match::<ZZ8>("ZZ8", n);
}
for n in [4, 6, 8, 10, 12, 14] {
check_quotient_match::<ZZ4>("ZZ4", n);
}
}
#[test]
fn test_seed_partitioning_matches_one_shot() {
for &n in &[5usize, 6, 7] {
for &free in &[false, true] {
for &split_depth in &[1usize, 2, 3] {
if split_depth >= n {
continue;
}
let (one_shot_seqs, _) = if free {
super::rat_enum_free::<ZZ12>(n, 1)
} else {
super::rat_enum::<ZZ12>(n, 1)
};
let one_shot: HashSet<Vec<i8>> = one_shot_seqs.into_iter().collect();
let (mut seeded, prefixes) = crate::enumerate::seed::collect_seed_prefixes::<
ZZ12,
>(n, 1, split_depth, free);
for prefix in &prefixes {
for nthreads in &[1usize, 4] {
seeded.extend(crate::enumerate::seed::enumerate_from_seed::<ZZ12>(
n, 1, prefix, *nthreads, free, false,
));
}
}
assert_eq!(
one_shot,
seeded,
"n={n} free={free} split_depth={split_depth}: \
seed-partitioned != one-shot \
({} prefixes + {} pre-closed)",
prefixes.len(),
seeded.len() - prefixes.iter().map(|_| 0).sum::<usize>(),
);
}
}
}
}
}
#[cfg(test)]
mod opt_correctness_tests {
use super::*;
use crate::cyclotomic::{ZZ4, ZZ6, ZZ8, ZZ10, ZZ12, ZZ14, ZZ16, ZZ18, ZZ20, ZZ24, ZZ32, ZZ60};
use crate::enumerate::prune::closure_table::{ClosureTablePrune, collect_closure_keys};
use crate::enumerate::prune::modular::ModularPrune;
use crate::enumerate::prune::shadow::ShadowPrune;
use crate::enumerate::prune::units::unit_vectors_for_ring;
use crate::enumerate::seed::rat_enum_parallel;
use std::sync::Arc;
fn closure_tables_for(ring: u8, max_l: usize) -> rustc_hash::FxHashSet<(Vec<i64>, i8)> {
crate::dispatch_ring!(ring, collect_closure_keys::<ZZ>(max_l))
}
fn build_prunes(
ring: u8,
max_steps: usize,
with_mod: bool,
with_ck: bool,
with_shadow: bool,
) -> Prunes {
let (units, phi) = unit_vectors_for_ring(ring);
let mut prunes = Prunes::default();
if with_mod {
let mp = ModularPrune::build(&units, phi, max_steps, None);
prunes.modular_prune = Some(Arc::new(mp));
}
if with_ck {
let max_l = 4;
let keys = closure_tables_for(ring, max_l);
prunes.closure_table_prune = Some(Arc::new(ClosureTablePrune { max_l, keys }));
}
if with_shadow {
prunes.shadow_prune = Some(Arc::new(ShadowPrune::for_ring(ring)));
}
prunes
}
fn run<ZZ: IsRing + Sync>(
max_steps: usize,
free: bool,
n_threads: usize,
prunes: &Prunes,
) -> std::collections::HashSet<Vec<i8>> {
let ops = make_ops(free);
let mk = |seq: &[i8]| Snake::<ZZ>::from_slice_trusted(seq);
let (rats, _) = if n_threads <= 1 {
rat_enum_with::<ZZ, Snake<ZZ>, _>(mk, max_steps, 1, ops, "test", "", false, prunes)
} else {
rat_enum_parallel::<ZZ, Snake<ZZ>, _>(
mk, max_steps, 1, n_threads, ops, "test", "", false, prunes,
)
};
rats.into_iter().collect()
}
fn opt_subsets() -> impl Iterator<Item = (bool, bool, bool)> {
(0..8).map(|mask| (mask & 1 != 0, mask & 2 != 0, mask & 4 != 0))
}
fn check_ring<ZZ: IsRing + Sync>(ring: u8, max_steps: usize) {
for &free in &[false, true] {
let baseline = run::<ZZ>(max_steps, free, 1, &Prunes::default());
for (with_mod, with_ck, with_shadow) in opt_subsets() {
let prunes = build_prunes(ring, max_steps, with_mod, with_ck, with_shadow);
for &n_threads in &[1usize, 4] {
let got = run::<ZZ>(max_steps, free, n_threads, &prunes);
assert_eq!(
got,
baseline,
"ZZ{ring} n={max_steps} free={free} \
mod={with_mod} ck={with_ck} shadow={with_shadow} threads={n_threads}: \
result set differs from baseline ({} vs {} rats)",
got.len(),
baseline.len(),
);
}
}
}
}
#[test]
fn cross_validate_zz4() {
check_ring::<ZZ4>(4, 10);
}
#[test]
fn cross_validate_zz8() {
check_ring::<ZZ8>(8, 10);
}
#[test]
fn cross_validate_zz12() {
check_ring::<ZZ12>(12, 8);
}
#[test]
#[cfg_attr(
debug_assertions,
ignore = "release-only: ZZ14 n=7 32-prune-combo cross-check is ~5 min debug / ~15 s release"
)]
fn cross_validate_zz14() {
check_ring::<ZZ14>(14, 7);
}
#[test]
#[cfg_attr(
debug_assertions,
ignore = "release-only: ZZ18 n=7 32-prune-combo cross-check is ~13 min debug / ~40 s release"
)]
fn cross_validate_zz18() {
check_ring::<ZZ18>(18, 7);
}
#[test]
fn cross_validate_zz18_step3_matches_zz6() {
let (zz6_rats, _) = rat_enum_free::<ZZ6>(8, 1);
let mut zz6_by_len: std::collections::BTreeMap<usize, usize> =
std::collections::BTreeMap::new();
for r in &zz6_rats {
*zz6_by_len.entry(r.len()).or_insert(0) += 1;
}
let (zz18_rats, _) = rat_enum_free::<ZZ18>(8, 3);
let mut zz18_by_len: std::collections::BTreeMap<usize, usize> =
std::collections::BTreeMap::new();
for r in &zz18_rats {
*zz18_by_len.entry(r.len()).or_insert(0) += 1;
}
let mut all_lens: std::collections::BTreeSet<usize> = zz6_by_len.keys().copied().collect();
all_lens.extend(zz18_by_len.keys().copied());
let mut mismatches: Vec<(usize, usize, usize)> = Vec::new();
for &n in &all_lens {
let z6 = zz6_by_len.get(&n).copied().unwrap_or(0);
let z18 = zz18_by_len.get(&n).copied().unwrap_or(0);
if z6 != z18 {
mismatches.push((n, z6, z18));
}
}
if !mismatches.is_empty() {
for (n, z6, z18) in &mismatches {
eprintln!(
"perim={n}: ZZ6={z6}, ZZ18-step-3={z18}, diff={:+}",
*z18 as i64 - *z6 as i64
);
}
panic!(
"ZZ18-step-3 disagrees with ZZ6 at {} perimeter(s) -- sign helper or \
cell_floor regression in ZZ18",
mismatches.len()
);
}
}
fn assert_step_subset<Big: IsRing, Small: IsRing>(max_steps: usize, step: i8, label: &str) {
let count_by_len = |rats: &[Vec<i8>]| {
let mut m = std::collections::BTreeMap::<usize, usize>::new();
for r in rats {
*m.entry(r.len()).or_insert(0) += 1;
}
m
};
let (big, _) = rat_enum_free::<Big>(max_steps, step);
let (small, _) = rat_enum_free::<Small>(max_steps, 1);
assert_eq!(
count_by_len(&big),
count_by_len(&small),
"{label}: step-{step} subset counts disagree with the reference ring \
-- a sign-helper or cell_floor bug in the bigger ring",
);
}
#[test]
fn zz16_step2_matches_zz8() {
assert_step_subset::<ZZ16, ZZ8>(8, 2, "zz16_step2");
}
#[test]
fn zz20_step2_matches_zz10() {
assert_step_subset::<ZZ20, ZZ10>(7, 2, "zz20_step2");
}
#[test]
fn zz24_step2_matches_zz12() {
assert_step_subset::<ZZ24, ZZ12>(7, 2, "zz24_step2");
}
#[test]
fn zz32_step4_matches_zz8() {
assert_step_subset::<ZZ32, ZZ8>(6, 4, "zz32_step4");
}
#[test]
fn zz60_step5_matches_zz12() {
assert_step_subset::<ZZ60, ZZ12>(6, 5, "zz60_step5");
}
#[test]
#[cfg_attr(
debug_assertions,
ignore = "release-only: A316192 pin x 32 prune combos is ~17 min debug / ~60-90 s release"
)]
fn oeis_a316192_each_opt_combo() {
const OEIS: &[(usize, usize)] = &[
(3, 1),
(4, 3),
(5, 4),
(6, 22),
(7, 69),
(8, 418),
(9, 2210), (10, 14024), ];
let max_n = OEIS.iter().map(|&(n, _)| n).max().unwrap();
for (with_mod, with_ck, with_shadow) in opt_subsets() {
let prunes = build_prunes(12, max_n, with_mod, with_ck, with_shadow);
for &n_threads in &[1usize, 4] {
let rats = run::<ZZ12>(max_n, true, n_threads, &prunes);
let mut by_len: std::collections::BTreeMap<usize, usize> =
std::collections::BTreeMap::new();
for seq in &rats {
*by_len.entry(seq.len()).or_insert(0) += 1;
}
let mut mismatches: Vec<(usize, usize, usize)> = Vec::new();
for &(n, expected) in OEIS {
let got = by_len.get(&n).copied().unwrap_or(0);
if got != expected {
mismatches.push((n, got, expected));
}
}
if !mismatches.is_empty() {
for (n, got, expected) in &mismatches {
eprintln!(
"n={n}: got {got}, expected (OEIS A316192) {expected}, diff {:+}",
*got as i64 - *expected as i64
);
}
panic!(
"OEIS mismatch with mod={with_mod} ck={with_ck} shadow={with_shadow} \
threads={n_threads}: {} length(s) differ",
mismatches.len()
);
}
}
}
}
#[test]
#[ignore = "opt-in (cargo test -- --include-ignored): ~4 min; pre-submission / thorough CI \
guard, not part of the default debug or release suite"]
fn zz12_extension_frontier_guard() {
let none = build_prunes(12, 11, false, false, false);
let all11 = build_prunes(12, 11, true, true, true);
let base = by_length(&run::<ZZ12>(11, true, 0, &none));
let pruned = by_length(&run::<ZZ12>(11, true, 0, &all11));
assert_eq!(
base, pruned,
"ZZ12 free n=11: prunes changed the count -- over-pruning at the frontier",
);
let all12 = build_prunes(12, 12, true, true, true);
let c12 = by_length(&run::<ZZ12>(12, true, 0, &all12));
assert_eq!(c12.get(&11).copied(), Some(89075), "ZZ12 a(11) regression");
assert_eq!(c12.get(&12).copied(), Some(597581), "ZZ12 a(12) regression");
}
fn by_length(
rats: &std::collections::HashSet<Vec<i8>>,
) -> std::collections::BTreeMap<usize, usize> {
let mut by_len = std::collections::BTreeMap::new();
for seq in rats {
*by_len.entry(seq.len()).or_insert(0) += 1;
}
by_len
}
fn assert_oeis_pins<ZZ: IsRing + Sync>(ring: u8, oeis_name: &str, oeis: &[(usize, usize)]) {
let max_n = oeis.iter().map(|&(n, _)| n).max().unwrap();
let prunes = build_prunes(ring, max_n, true, true, true);
let rats = run::<ZZ>(max_n, true, 1, &prunes);
let by_len = by_length(&rats);
let mut mismatches: Vec<(usize, usize, usize)> = Vec::new();
for &(n, expected) in oeis {
let got = by_len.get(&n).copied().unwrap_or(0);
if got != expected {
mismatches.push((n, got, expected));
}
}
if !mismatches.is_empty() {
for (n, got, expected) in &mismatches {
eprintln!(
"{oeis_name} ZZ{ring} perim={n}: got {got}, OEIS says {expected}, diff {:+}",
*got as i64 - *expected as i64
);
}
panic!("{oeis_name} ZZ{ring}: {} term(s) differ", mismatches.len());
}
}
#[test]
fn oeis_a266549_zz4_pin() {
const OEIS: &[(usize, usize)] = &[
(4, 1), (6, 1), (8, 3), (10, 6), (12, 25), (14, 86), ];
assert_oeis_pins::<ZZ4>(4, "A266549", OEIS);
}
#[test]
fn oeis_a316198_zz8_pin() {
const OEIS: &[(usize, usize)] = &[
(4, 2), (6, 6), (8, 59), (10, 695), (12, 12198), ];
assert_oeis_pins::<ZZ8>(8, "A316198", OEIS);
}
#[test]
fn oeis_a316200_zz10_pin() {
const OEIS: &[(usize, usize)] = &[
(4, 2), (5, 2), (6, 10), (7, 15), (8, 124), (9, 352), (10, 2378), ];
assert_oeis_pins::<ZZ10>(10, "A316200", OEIS);
}
#[test]
fn oeis_a284869_zz6_pin() {
const OEIS: &[(usize, usize)] = &[
(3, 1), (4, 1), (5, 1), (6, 4), (7, 5), (8, 16), (9, 37), (10, 120), (11, 344), (12, 1175), (13, 3807), (14, 13224), ];
assert_oeis_pins::<ZZ6>(6, "A284869", OEIS);
}
}