1use crate::cdcl::Lit;
30use crate::polycalc::{
31 apply_perm_to_mono, check_ns_lower_bound_polys, clause_polynomial, gf2_solve,
32 monomials_up_to_degree, ns_lower_bound_witness_polys, poly_degree, poly_mul_mono, Mono, Poly,
33};
34use crate::proof::Perm;
35use std::collections::{BTreeMap, BTreeSet, HashMap};
36
37pub type Structure = Vec<Vec<(u8, u32)>>;
42
43pub fn canonical_structure(structure: &Structure) -> Structure {
47 let mut sorts: BTreeMap<u8, Vec<u32>> = BTreeMap::new();
49 for var in structure {
50 for &(s, p) in var {
51 let pts = sorts.entry(s).or_default();
52 if !pts.contains(&p) {
53 pts.push(p);
54 }
55 }
56 }
57 let sort_keys: Vec<u8> = sorts.keys().copied().collect();
58 let mut best: Option<Structure> = None;
60 let mut perms: Vec<Vec<usize>> = Vec::new();
61 fn rec(
62 level: usize,
63 sort_keys: &[u8],
64 sorts: &BTreeMap<u8, Vec<u32>>,
65 perms: &mut Vec<Vec<usize>>,
66 structure: &Structure,
67 best: &mut Option<Structure>,
68 ) {
69 if level == sort_keys.len() {
70 let mut maps: BTreeMap<u8, HashMap<u32, u32>> = BTreeMap::new();
72 for (li, &s) in sort_keys.iter().enumerate() {
73 let pts = &sorts[&s];
74 let map = maps.entry(s).or_default();
75 for (i, &p) in pts.iter().enumerate() {
76 map.insert(p, perms[li][i] as u32);
77 }
78 }
79 let mut relabeled: Structure = structure
80 .iter()
81 .map(|var| {
82 let mut v: Vec<(u8, u32)> =
83 var.iter().map(|&(s, p)| (s, maps[&s][&p])).collect();
84 v.sort_unstable();
85 v
86 })
87 .collect();
88 relabeled.sort_unstable();
89 if best.as_ref().is_none_or(|b| relabeled < *b) {
90 *best = Some(relabeled);
91 }
92 return;
93 }
94 let k = sorts[&sort_keys[level]].len();
95 let mut idx: Vec<usize> = (0..k).collect();
96 permute(&mut idx, 0, &mut |perm| {
97 perms.push(perm.to_vec());
98 rec(level + 1, sort_keys, sorts, perms, structure, best);
99 perms.pop();
100 });
101 }
102 fn permute(idx: &mut Vec<usize>, at: usize, f: &mut dyn FnMut(&[usize])) {
103 if at == idx.len() {
104 f(idx);
105 return;
106 }
107 for i in at..idx.len() {
108 idx.swap(at, i);
109 permute(idx, at + 1, f);
110 idx.swap(at, i);
111 }
112 }
113 rec(0, &sort_keys, &sorts, &mut perms, structure, &mut best);
114 best.unwrap_or_default()
115}
116
117pub fn monomial_orbits_bounded(num_vars: usize, degree: usize, generators: &[Perm]) -> Vec<Vec<Mono>> {
121 let basis: BTreeSet<Mono> = monomials_up_to_degree(num_vars, degree).into_iter().collect();
122 let mut seen: BTreeSet<Mono> = BTreeSet::new();
123 let mut orbits = Vec::new();
124 for &m in &basis {
125 if seen.contains(&m) {
126 continue;
127 }
128 let mut orbit = BTreeSet::new();
129 orbit.insert(m);
130 let mut stack = vec![m];
131 while let Some(x) = stack.pop() {
132 for g in generators {
133 let y = apply_perm_to_mono(g, x);
134 if basis.contains(&y) && orbit.insert(y) {
135 stack.push(y);
136 }
137 }
138 }
139 for &x in &orbit {
140 seen.insert(x);
141 }
142 orbits.push(orbit.into_iter().collect());
143 }
144 orbits
145}
146
147pub struct SymmetricInstance {
155 pub num_vars: usize,
156 pub gens: Vec<Poly>,
157 pub sym: Vec<Perm>,
158 pub atoms: Box<dyn Fn(u32) -> Vec<(u8, u32)>>,
159 pub anchors: Vec<Structure>,
160}
161
162impl SymmetricInstance {
163 pub fn structure(&self, mono: Mono) -> Structure {
165 let mut vars = Vec::new();
166 let mut bits = mono;
167 while bits != 0 {
168 let v = bits.trailing_zeros();
169 vars.push((self.atoms)(v));
170 bits &= bits - 1;
171 }
172 vars
173 }
174
175 pub fn type_of(&self, mono: Mono) -> Structure {
177 canonical_structure(&self.structure(mono))
178 }
179}
180
181fn fixed_shape_anchor(vars: &[Vec<(u8, u32)>]) -> Structure {
185 vars.iter()
186 .map(|atoms| {
187 let mut a = atoms.clone();
188 a.push((255, 0));
189 a
190 })
191 .collect()
192}
193
194pub fn php_instance_clause(m: usize) -> SymmetricInstance {
198 let (cnf, _) = crate::families::php(m);
199 let holes = m - 1;
200 let atom = move |v: u32| vec![(0u8, v / holes as u32), (1u8, v % holes as u32)];
201 let mut gens = Vec::new();
202 let mut anchors = Vec::new();
203 for c in &cnf.clauses {
204 gens.push(clause_polynomial(c));
205 if c.iter().all(|l| l.is_positive()) {
206 let pigeon = c[0].var() / holes as u32;
207 anchors.push(vec![vec![(0u8, pigeon), (255, 0)]]);
208 } else {
209 anchors.push(fixed_shape_anchor(
210 &c.iter().map(|l| atom(l.var())).collect::<Vec<_>>(),
211 ));
212 }
213 }
214 SymmetricInstance {
215 num_vars: cnf.num_vars,
216 gens,
217 sym: crate::hypercube::php_perm_symmetries(m),
218 atoms: Box::new(atom),
219 anchors,
220 }
221}
222
223pub fn php_instance_linear(m: usize) -> SymmetricInstance {
228 let (cnf, _) = crate::families::php(m);
229 let holes = m - 1;
230 let atom = move |v: u32| vec![(0u8, v / holes as u32), (1u8, v % holes as u32)];
231 let mut gens: Vec<Poly> = Vec::new();
232 let mut anchors: Vec<Structure> = Vec::new();
233 for p in 0..m {
234 let mut lin: Poly = [0u64].into_iter().collect();
235 for h in 0..holes {
236 lin.insert(1u64 << (p * holes + h));
237 }
238 gens.push(lin);
239 anchors.push(vec![vec![(0u8, p as u32), (255, 0)]]);
240 }
241 for c in &cnf.clauses {
242 if c.iter().all(|l| !l.is_positive()) {
243 gens.push(clause_polynomial(c)); anchors.push(fixed_shape_anchor(
245 &c.iter().map(|l| atom(l.var())).collect::<Vec<_>>(),
246 ));
247 }
248 }
249 SymmetricInstance {
250 num_vars: cnf.num_vars,
251 gens,
252 sym: crate::hypercube::php_perm_symmetries(m),
253 atoms: Box::new(atom),
254 anchors,
255 }
256}
257
258pub fn count_instance_linear(n: usize, q: usize) -> SymmetricInstance {
262 let (cnf, _) = crate::families::mod_counting(n, q);
263 let groups = crate::families::mod_counting_groups(n, q);
264 let gens = crate::polycalc::exactly_one_linear_generators(&groups);
265 let edges = crate::families::mod_counting_edges(n, q);
266 let edge_index: HashMap<Vec<usize>, usize> =
267 edges.iter().enumerate().map(|(i, e)| (e.clone(), i)).collect();
268 let mut sym = Vec::new();
270 for i in 0..n - 1 {
271 let images: Vec<Lit> = (0..cnf.num_vars as u32)
272 .map(|e| {
273 let mut swapped: Vec<usize> = edges[e as usize]
274 .iter()
275 .map(|&p| if p == i { i + 1 } else if p == i + 1 { i } else { p })
276 .collect();
277 swapped.sort_unstable();
278 Lit::pos(edge_index[&swapped] as u32)
279 })
280 .collect();
281 sym.push(Perm::from_images(images));
282 }
283 let atom = {
286 let edges = edges.clone();
287 move |v: u32| -> Vec<(u8, u32)> {
288 edges[v as usize].iter().map(|&p| (0u8, p as u32)).collect()
289 }
290 };
291 let mut anchors: Vec<Structure> = (0..n).map(|i| vec![vec![(0u8, i as u32), (255, 0)]]).collect();
292 for g in gens.iter().skip(n) {
293 let &pair_mono = g.iter().next().expect("a pair generator is a single monomial");
294 let mut vars = Vec::new();
295 let mut bits = pair_mono;
296 while bits != 0 {
297 vars.push(atom(bits.trailing_zeros()));
298 bits &= bits - 1;
299 }
300 anchors.push(fixed_shape_anchor(&vars));
301 }
302 let atoms_edges = edges.clone();
303 SymmetricInstance {
304 num_vars: cnf.num_vars,
305 gens,
306 sym,
307 atoms: Box::new(move |v| atoms_edges[v as usize].iter().map(|&p| (0, p as u32)).collect()),
308 anchors,
309 }
310}
311
312pub struct CollapsedDual {
319 pub types: Vec<Structure>,
321 pub rows: BTreeSet<u64>,
323}
324
325impl CollapsedDual {
326 pub fn solve(&self) -> Option<u64> {
329 let nt = self.types.len();
330 let mut eqs: Vec<(Vec<u64>, bool)> = self.rows.iter().map(|&r| (vec![r], false)).collect();
331 eqs.push((vec![1u64], true)); gf2_solve(&eqs, nt).map(|x| x[0])
333 }
334
335 pub fn lift(&self, inst: &SymmetricInstance, degree: usize, solution: u64) -> Vec<Mono> {
337 let on: BTreeSet<&Structure> = self
338 .types
339 .iter()
340 .enumerate()
341 .filter(|(i, _)| (solution >> i) & 1 == 1)
342 .map(|(_, t)| t)
343 .collect();
344 monomials_up_to_degree(inst.num_vars, degree)
345 .into_iter()
346 .filter(|&m| on.contains(&inst.type_of(m)))
347 .collect()
348 }
349}
350
351pub fn collapsed_dual_system(inst: &SymmetricInstance, degree: usize) -> CollapsedDual {
355 let orbits = monomial_orbits_bounded(inst.num_vars, degree, &inst.sym);
356 let mut types: Vec<Structure> = Vec::new();
357 let mut type_index: BTreeMap<Structure, usize> = BTreeMap::new();
358 let mut mono_type: HashMap<Mono, usize> = HashMap::new();
359 let empty_type = inst.type_of(0);
361 types.push(empty_type.clone());
362 type_index.insert(empty_type, 0);
363 for orbit in &orbits {
364 let t = inst.type_of(orbit[0]);
365 let idx = *type_index.entry(t.clone()).or_insert_with(|| {
366 types.push(t.clone());
367 types.len() - 1
368 });
369 for &m in orbit {
370 mono_type.insert(m, idx);
371 }
372 }
373 assert!(types.len() <= 64, "the type bitmask carries ≤ 64 orbit types (degree too high?)");
374
375 let mut rows: BTreeSet<u64> = BTreeSet::new();
376 for orbit in &orbits {
377 let mult = orbit[0]; for g in &inst.gens {
379 let prod = poly_mul_mono(g, mult);
380 if prod.is_empty() || poly_degree(&prod) > degree {
381 continue;
382 }
383 let mut row = 0u64;
384 for &t in &prod {
385 row ^= 1u64 << mono_type[&t]; }
387 if row != 0 {
388 rows.insert(row);
389 }
390 }
391 }
392 CollapsedDual { types, rows }
393}
394
395pub fn invariant_witness_exists_direct(inst: &SymmetricInstance, degree: usize) -> Option<Vec<Mono>> {
402 let orbits = monomial_orbits_bounded(inst.num_vars, degree, &inst.sym);
403 let orbit_of: HashMap<Mono, usize> = orbits
404 .iter()
405 .enumerate()
406 .flat_map(|(i, o)| o.iter().map(move |&m| (m, i)))
407 .collect();
408 let no = orbits.len();
409 let words = no.div_ceil(64).max(1);
410 let mut eqs: Vec<(Vec<u64>, bool)> = Vec::new();
411 for g in &inst.gens {
412 if g.is_empty() {
413 continue;
414 }
415 for &m in &monomials_up_to_degree(inst.num_vars, degree) {
416 let prod = poly_mul_mono(g, m);
417 if prod.is_empty() || poly_degree(&prod) > degree {
418 continue;
419 }
420 let mut mask = vec![0u64; words];
421 for &t in &prod {
422 let oi = orbit_of[&t];
423 mask[oi / 64] ^= 1u64 << (oi % 64); }
425 if mask.iter().any(|&w| w != 0) {
426 eqs.push((mask, false));
427 }
428 }
429 }
430 let empty_orbit = orbit_of[&0u64];
431 let mut target = vec![0u64; words];
432 target[empty_orbit / 64] |= 1u64 << (empty_orbit % 64);
433 eqs.push((target, true));
434 let sol = gf2_solve(&eqs, no)?;
435 let mut witness = Vec::new();
436 for (i, orbit) in orbits.iter().enumerate() {
437 if (sol[i / 64] >> (i % 64)) & 1 == 1 {
438 witness.extend(orbit.iter().copied());
439 }
440 }
441 Some(witness)
442}
443
444pub fn count_instance_linear_marked(n: usize, q: usize) -> SymmetricInstance {
452 let base = count_instance_linear(n, q);
453 let edges = crate::families::mod_counting_edges(n, q);
454 let edge_index: HashMap<Vec<usize>, usize> =
455 edges.iter().enumerate().map(|(i, e)| (e.clone(), i)).collect();
456 let mut sym = Vec::new();
458 for i in 1..n - 1 {
459 let images: Vec<Lit> = (0..base.num_vars as u32)
460 .map(|e| {
461 let mut swapped: Vec<usize> = edges[e as usize]
462 .iter()
463 .map(|&p| if p == i { i + 1 } else if p == i + 1 { i } else { p })
464 .collect();
465 swapped.sort_unstable();
466 Lit::pos(edge_index[&swapped] as u32)
467 })
468 .collect();
469 sym.push(Perm::from_images(images));
470 }
471 let mark = |p: usize| -> (u8, u32) { if p == 0 { (1, 0) } else { (0, p as u32) } };
472 let mut anchors: Vec<Structure> = (0..n).map(|i| vec![vec![mark(i), (255, 0)]]).collect();
475 for g in base.gens.iter().skip(n) {
476 let &pair_mono = g.iter().next().expect("a pair generator is a single monomial");
477 let mut vars = Vec::new();
478 let mut bits = pair_mono;
479 while bits != 0 {
480 let e = bits.trailing_zeros() as usize;
481 let mut atoms: Vec<(u8, u32)> = edges[e].iter().map(|&p| mark(p)).collect();
482 atoms.push((255, 0));
483 atoms.sort_unstable();
484 vars.push(atoms);
485 bits &= bits - 1;
486 }
487 anchors.push(vars);
488 }
489 let atoms_edges = edges;
490 SymmetricInstance {
491 num_vars: base.num_vars,
492 gens: base.gens,
493 sym,
494 atoms: Box::new(move |v| atoms_edges[v as usize].iter().map(|&p| mark(p)).collect()),
495 anchors,
496 }
497}
498
499pub fn php_instance_linear_marked_hole(m: usize) -> SymmetricInstance {
502 let base = php_instance_linear(m);
503 let holes = m - 1;
504 let num_vars = base.num_vars;
505 let var = |p: usize, h: usize| p * holes + h;
506 let mark = move |v: u32| -> Vec<(u8, u32)> {
507 let (p, h) = (v / holes as u32, v % holes as u32);
508 if h == 0 {
509 vec![(0, p), (2, 0)]
510 } else {
511 vec![(0, p), (1, h)]
512 }
513 };
514 let mut sym = Vec::new();
516 for p in 0..m - 1 {
517 let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
518 for h in 0..holes {
519 images.swap(var(p, h), var(p + 1, h));
520 }
521 sym.push(Perm::from_images(images));
522 }
523 for h in 1..holes.saturating_sub(1) {
524 let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
525 for p in 0..m {
526 images.swap(var(p, h), var(p, h + 1));
527 }
528 sym.push(Perm::from_images(images));
529 }
530 let mut anchors: Vec<Structure> = (0..m).map(|p| vec![vec![(0u8, p as u32), (255, 0)]]).collect();
532 for g in base.gens.iter().skip(m) {
533 let &pair = g.iter().next().expect("an AMO generator is a single monomial");
534 let mut vars = Vec::new();
535 let mut bits = pair;
536 while bits != 0 {
537 let mut atoms = mark(bits.trailing_zeros());
538 atoms.push((255, 0));
539 atoms.sort_unstable();
540 vars.push(atoms);
541 bits &= bits - 1;
542 }
543 anchors.push(vars);
544 }
545 SymmetricInstance { num_vars, gens: base.gens, sym, atoms: Box::new(mark), anchors }
546}
547
548pub fn php_instance_linear_marked_pigeon(m: usize) -> SymmetricInstance {
551 let base = php_instance_linear(m);
552 let holes = m - 1;
553 let num_vars = base.num_vars;
554 let var = |p: usize, h: usize| p * holes + h;
555 let mark = move |v: u32| -> Vec<(u8, u32)> {
556 let (p, h) = (v / holes as u32, v % holes as u32);
557 if p == 0 {
558 vec![(3, 0), (1, h)]
559 } else {
560 vec![(0, p), (1, h)]
561 }
562 };
563 let mut sym = Vec::new();
564 for p in 1..m - 1 {
565 let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
566 for h in 0..holes {
567 images.swap(var(p, h), var(p + 1, h));
568 }
569 sym.push(Perm::from_images(images));
570 }
571 for h in 0..holes.saturating_sub(1) {
572 let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
573 for p in 0..m {
574 images.swap(var(p, h), var(p, h + 1));
575 }
576 sym.push(Perm::from_images(images));
577 }
578 let mut anchors: Vec<Structure> = (0..m)
579 .map(|p| {
580 let sort = if p == 0 { 3u8 } else { 0u8 };
581 let id = if p == 0 { 0u32 } else { p as u32 };
582 vec![vec![(sort, id), (255, 0)]]
583 })
584 .collect();
585 for g in base.gens.iter().skip(m) {
586 let &pair = g.iter().next().expect("an AMO generator is a single monomial");
587 let mut vars = Vec::new();
588 let mut bits = pair;
589 while bits != 0 {
590 let mut atoms = mark(bits.trailing_zeros());
591 atoms.push((255, 0));
592 atoms.sort_unstable();
593 vars.push(atoms);
594 bits &= bits - 1;
595 }
596 anchors.push(vars);
597 }
598 SymmetricInstance { num_vars, gens: base.gens, sym, atoms: Box::new(mark), anchors }
599}
600
601pub type RowLabel = Structure;
605
606pub fn labeled_dual_counts(
609 inst: &SymmetricInstance,
610 degree: usize,
611) -> BTreeMap<RowLabel, BTreeMap<Structure, u64>> {
612 let orbits = monomial_orbits_bounded(inst.num_vars, degree, &inst.sym);
613 let mut type_cache: HashMap<Mono, Structure> = HashMap::new();
614 let mut out: BTreeMap<RowLabel, BTreeMap<Structure, u64>> = BTreeMap::new();
615 for orbit in &orbits {
616 let mult = orbit[0];
617 let mult_structure = inst.structure(mult);
618 for (gi, g) in inst.gens.iter().enumerate() {
619 let prod = poly_mul_mono(g, mult);
620 if prod.is_empty() || poly_degree(&prod) > degree {
621 continue;
622 }
623 let mut joint = mult_structure.clone();
624 joint.extend(inst.anchors[gi].iter().cloned());
625 let label = canonical_structure(&joint);
626 let mut counts: BTreeMap<Structure, u64> = BTreeMap::new();
627 for &t in &prod {
628 let ty = type_cache
629 .entry(t)
630 .or_insert_with(|| inst.type_of(t))
631 .clone();
632 *counts.entry(ty).or_insert(0) += 1;
633 }
634 match out.entry(label) {
635 std::collections::btree_map::Entry::Vacant(e) => {
636 e.insert(counts);
637 }
638 std::collections::btree_map::Entry::Occupied(e) => {
639 assert_eq!(
640 e.get(),
641 &counts,
642 "same joint label ⟹ same entry counts (joint-orbit invariance, fail-closed)"
643 );
644 }
645 }
646 }
647 }
648 out
649}
650
651#[derive(Clone, Debug, PartialEq)]
656pub struct FittedCount {
657 pub base: usize,
658 pub diffs: Vec<i128>,
659}
660
661impl FittedCount {
662 pub fn fit(base: usize, values: &[i128], max_degree: usize) -> Option<FittedCount> {
667 let mut table: Vec<i128> = values.to_vec();
668 let mut diffs: Vec<i128> = Vec::new();
669 let mut level = 0usize;
670 while !table.is_empty() {
671 diffs.push(table[0]);
672 if level > max_degree && table.iter().any(|&v| v != 0) {
673 return None; }
675 table = table.windows(2).map(|w| w[1] - w[0]).collect();
676 level += 1;
677 }
678 diffs.truncate(max_degree + 1);
679 Some(FittedCount { base, diffs })
680 }
681
682 pub fn eval(&self, m: usize) -> i128 {
684 let a = (m - self.base) as u128;
685 let mut c: u128 = 1; let mut total: i128 = 0;
687 for (k, &d) in self.diffs.iter().enumerate() {
688 if k > 0 {
689 if a < k as u128 {
690 break;
691 }
692 c = c * (a - (k as u128 - 1)) / k as u128;
693 }
694 total += d * c as i128;
695 }
696 total
697 }
698
699 pub fn parity(&self, m: usize) -> bool {
702 let a = (m - self.base) as u64;
703 self.diffs
704 .iter()
705 .enumerate()
706 .filter(|(k, &d)| d % 2 != 0 && (*k as u64) & a == *k as u64)
707 .count()
708 % 2
709 == 1
710 }
711
712 pub fn parity_period(&self) -> usize {
715 let top = self
716 .diffs
717 .iter()
718 .enumerate()
719 .filter(|(_, &d)| d % 2 != 0)
720 .map(|(k, _)| k)
721 .max();
722 match top {
723 None => 1,
724 Some(k) => (k + 1).next_power_of_two(),
725 }
726 }
727}
728
729pub struct StabilizedSystem {
732 pub degree: usize,
733 pub cols: Vec<Structure>,
735 pub entries: BTreeMap<RowLabel, Vec<FittedCount>>,
737 pub onset: usize,
739 pub period: usize,
741}
742
743impl StabilizedSystem {
744 pub fn invariant_witness_exists_at(&self, m: usize) -> bool {
748 let nc = self.cols.len();
749 let mut eqs: Vec<(Vec<u64>, bool)> = Vec::new();
750 for fits in self.entries.values() {
751 let mut row = 0u64;
752 for (ci, f) in fits.iter().enumerate() {
753 if f.parity(m) {
754 row |= 1u64 << ci;
755 }
756 }
757 if row != 0 {
758 eqs.push((vec![row], false));
759 }
760 }
761 eqs.push((vec![1u64], true)); gf2_solve(&eqs, nc).is_some()
763 }
764}
765
766pub struct ForAllVerdict {
769 pub system: StabilizedSystem,
770 pub by_residue: Vec<bool>,
773 pub validated: Vec<(usize, bool)>,
776}
777
778pub fn decide_invariant_witness_for_all_scales(
789 make: &dyn Fn(usize) -> SymmetricInstance,
790 window: &[usize],
791 degree: usize,
792 max_count_degree: usize,
793) -> ForAllVerdict {
794 assert!(window.windows(2).all(|w| w[1] == w[0] + 1), "the window is consecutive scales");
795 assert!(window.len() > max_count_degree, "enough window points to pin the polynomials");
796 let onset = window[0];
797
798 let mut per_scale: Vec<BTreeMap<RowLabel, BTreeMap<Structure, u64>>> = Vec::new();
800 let mut col_set: BTreeSet<Structure> = BTreeSet::new();
801 let mut instances: Vec<SymmetricInstance> = Vec::new();
802 for &m in window {
803 let inst = make(m);
804 let counts = labeled_dual_counts(&inst, degree);
805 for cols in counts.values() {
806 col_set.extend(cols.keys().cloned());
807 }
808 per_scale.push(counts);
809 instances.push(inst);
810 }
811 let empty_type = instances[0].type_of(0);
812 let mut cols: Vec<Structure> = vec![empty_type.clone()];
813 cols.extend(col_set.into_iter().filter(|t| *t != empty_type));
814 assert!(cols.len() <= 64, "the type bitmask carries ≤ 64 columns");
815
816 let labels: BTreeSet<RowLabel> = per_scale.iter().flat_map(|s| s.keys().cloned()).collect();
818 for (i, s) in per_scale.iter().enumerate() {
819 for l in &labels {
820 assert!(
821 s.contains_key(l),
822 "scale {}: a row label is unrealized — the window starts before the onset",
823 window[i]
824 );
825 }
826 }
827
828 let mut entries: BTreeMap<RowLabel, Vec<FittedCount>> = BTreeMap::new();
830 let mut period = 1usize;
831 for l in &labels {
832 let mut fits = Vec::with_capacity(cols.len());
833 for c in &cols {
834 let values: Vec<i128> = per_scale
835 .iter()
836 .map(|s| *s[l].get(c).unwrap_or(&0) as i128)
837 .collect();
838 let f = FittedCount::fit(onset, &values, max_count_degree)
839 .expect("every entry count is a bounded-degree integer polynomial in the scale");
840 period = period.max(f.parity_period()); fits.push(f);
842 }
843 entries.insert(l.clone(), fits);
844 }
845 let system = StabilizedSystem { degree, cols, entries, onset, period };
846
847 let mut validated = Vec::new();
849 for (i, &m) in window.iter().enumerate() {
850 let fitted = system.invariant_witness_exists_at(m);
851 let direct = invariant_witness_exists_direct(&instances[i], degree);
852 assert_eq!(
853 fitted,
854 direct.is_some(),
855 "scale {m}: the fitted system agrees with the direct orbit-collapsed solver"
856 );
857 if let Some(w) = direct {
858 assert!(
859 check_ns_lower_bound_polys(instances[i].num_vars, &instances[i].gens, degree, &w),
860 "scale {m}: the direct invariant witness re-checks"
861 );
862 }
863 validated.push((m, fitted));
864 }
865
866 let beyond = window.last().unwrap() + 1;
868 let by_residue: Vec<bool> = (0..system.period)
869 .map(|r| {
870 let mut m = beyond;
871 while m % system.period != r {
872 m += 1;
873 }
874 system.invariant_witness_exists_at(m)
875 })
876 .collect();
877 ForAllVerdict { system, by_residue, validated }
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883
884 #[test]
893 fn php_monomial_orbit_types_align_across_m_by_bipartite_graph_type() {
894 for d in [1usize, 2, 3] {
895 let mut prev: Option<BTreeSet<Structure>> = None;
896 for m in [d + 1, d + 2, d + 3] {
897 let inst = php_instance_clause(m);
898 let orbits = monomial_orbits_bounded(inst.num_vars, d, &inst.sym);
899 let type_set: BTreeSet<Structure> =
900 orbits.iter().map(|o| inst.type_of(o[0])).collect();
901 assert_eq!(
903 type_set.len(),
904 orbits.len(),
905 "PHP({m}) d={d}: distinct orbits get distinct canonical types"
906 );
907 for orbit in &orbits {
908 let t = inst.type_of(orbit[0]);
909 for &mono in orbit.iter().take(8) {
910 assert_eq!(
911 inst.type_of(mono),
912 t,
913 "PHP({m}) d={d}: the canonical type is constant on the orbit"
914 );
915 }
916 }
917 if let Some(p) = &prev {
918 assert_eq!(
919 p, &type_set,
920 "PHP d={d}: the type set is IDENTICAL across scales m={m}−1, {m}"
921 );
922 }
923 prev = Some(type_set);
924 }
925 }
926 for d in [1usize, 2] {
928 let mut prev: Option<BTreeSet<Structure>> = None;
929 for n in [7usize, 8] {
930 let inst = count_instance_linear(n, 3);
931 let orbits = monomial_orbits_bounded(inst.num_vars, d, &inst.sym);
932 let type_set: BTreeSet<Structure> =
933 orbits.iter().map(|o| inst.type_of(o[0])).collect();
934 assert_eq!(type_set.len(), orbits.len(), "Count_3({n}) d={d}: orbit↔type bijection");
935 if let Some(p) = &prev {
936 assert_eq!(p, &type_set, "Count_3 d={d}: type set identical at n=7, 8");
937 }
938 prev = Some(type_set);
939 }
940 }
941 }
942
943 #[test]
949 fn an_invariant_functional_checked_on_orbit_representatives_is_checked_on_all_generators() {
950 let mut seed = 0xA11C_E5EEDu64;
951 let mut lcg = move || {
952 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
953 seed >> 33
954 };
955 for m in [3usize, 4] {
956 let inst = php_instance_clause(m);
957 let d = 3;
958 let orbits = monomial_orbits_bounded(inst.num_vars, d, &inst.sym);
959 for _trial in 0..8 {
960 let l: BTreeSet<Mono> = orbits
962 .iter()
963 .filter(|_| lcg() & 1 == 1)
964 .flat_map(|o| o.iter().copied())
965 .collect();
966 let pair = |p: &Poly| p.iter().filter(|t| l.contains(t)).count() % 2;
967 for g in &inst.gens {
968 for sigma in &inst.sym {
969 let image: Poly =
970 g.iter().map(|&t| apply_perm_to_mono(sigma, t)).collect();
971 assert_eq!(
972 pair(g),
973 pair(&image),
974 "PHP({m}): ⟨L, σ(p)⟩ = ⟨L, p⟩ for invariant L"
975 );
976 }
977 }
978 }
979 }
980 }
981
982 #[test]
992 fn collapsed_dual_system_agrees_with_full_symmetric_ns_on_php_small_m() {
993 for m in [3usize, 4, 5] {
995 for (label, inst) in
996 [("clause", php_instance_clause(m)), ("linear", php_instance_linear(m))]
997 {
998 for d in 1..=3usize {
999 let sys = collapsed_dual_system(&inst, d);
1000 let collapsed = sys.solve();
1001 let direct = invariant_witness_exists_direct(&inst, d);
1002 assert_eq!(
1003 collapsed.is_some(),
1004 direct.is_some(),
1005 "PHP({m}) {label} d={d}: type-system and direct solver agree"
1006 );
1007 if let Some(sol) = collapsed {
1008 let witness = sys.lift(&inst, d, sol);
1009 assert!(
1010 check_ns_lower_bound_polys(inst.num_vars, &inst.gens, d, &witness),
1011 "PHP({m}) {label} d={d}: the lifted invariant witness re-checks"
1012 );
1013 }
1014 if let Some(w) = direct {
1015 assert!(
1016 check_ns_lower_bound_polys(inst.num_vars, &inst.gens, d, &w),
1017 "PHP({m}) {label} d={d}: the direct invariant witness re-checks"
1018 );
1019 }
1020 }
1021 }
1022 }
1023 for (n, invariant_exists) in [(7usize, true), (8, false)] {
1025 let inst = count_instance_linear(n, 3);
1026 let sys = collapsed_dual_system(&inst, 2);
1027 let collapsed = sys.solve();
1028 let direct = invariant_witness_exists_direct(&inst, 2);
1029 assert_eq!(collapsed.is_some(), direct.is_some(), "Count_3({n}): paths agree");
1030 assert_eq!(
1031 collapsed.is_some(),
1032 invariant_exists,
1033 "Count_3({n}) d=2: invariant witness iff n ≡ 3 (mod 4) — the Lucas schedule"
1034 );
1035 if let Some(sol) = collapsed {
1036 let witness = sys.lift(&inst, 2, sol);
1037 assert!(
1038 check_ns_lower_bound_polys(inst.num_vars, &inst.gens, 2, &witness),
1039 "Count_3({n}): the lifted witness re-checks"
1040 );
1041 } else {
1042 let general = ns_lower_bound_witness_polys(inst.num_vars, &inst.gens, 2);
1044 assert!(
1045 general.is_some(),
1046 "Count_3({n}): the general witness survives where every invariant one dies"
1047 );
1048 }
1049 }
1050 }
1051
1052 #[test]
1060 fn collapsed_entry_counts_are_integer_polynomials_in_m_with_an_interpolation_certificate() {
1061 let window: Vec<usize> = (4..=8).collect();
1063 let mut per_scale = Vec::new();
1064 let mut labels: BTreeSet<RowLabel> = BTreeSet::new();
1065 let mut cols: BTreeSet<Structure> = BTreeSet::new();
1066 for &m in &window {
1067 let counts = labeled_dual_counts(&php_instance_linear(m), 2);
1068 labels.extend(counts.keys().cloned());
1069 for c in counts.values() {
1070 cols.extend(c.keys().cloned());
1071 }
1072 per_scale.push(counts);
1073 }
1074 let mut fitted = 0usize;
1075 for l in &labels {
1076 for c in &cols {
1077 let values: Vec<i128> = per_scale
1078 .iter()
1079 .enumerate()
1080 .map(|(i, s)| {
1081 *s.get(l)
1082 .unwrap_or_else(|| panic!("label unrealized at m={}", window[i]))
1083 .get(c)
1084 .unwrap_or(&0) as i128
1085 })
1086 .collect();
1087 let f = FittedCount::fit(window[0], &values, 2)
1088 .expect("every PHP entry is a degree-≤2 polynomial, verified on 2 spare points");
1089 for (i, &m) in window.iter().enumerate() {
1090 assert_eq!(f.eval(m), values[i], "the fit reproduces every window point");
1091 }
1092 fitted += 1;
1093 }
1094 }
1095 assert!(fitted > 20, "a non-trivial system was fitted ({fitted} entries)");
1096
1097 let cwindow: Vec<usize> = (6..=8).collect();
1099 let mut cscale = Vec::new();
1100 for &n in &cwindow {
1101 cscale.push(labeled_dual_counts(&count_instance_linear(n, 3), 2));
1102 }
1103 let clabels: BTreeSet<RowLabel> = cscale.iter().flat_map(|s| s.keys().cloned()).collect();
1104 let ccols: BTreeSet<Structure> = cscale
1105 .iter()
1106 .flat_map(|s| s.values().flat_map(|c| c.keys().cloned()))
1107 .collect();
1108 let mut found_quadratic = false;
1109 for l in &clabels {
1110 for c in &ccols {
1111 let values: Vec<i128> = cscale
1112 .iter()
1113 .map(|s| s.get(l).map(|m| *m.get(c).unwrap_or(&0)).unwrap_or(0) as i128)
1114 .collect();
1115 let f = FittedCount::fit(cwindow[0], &values, 2)
1116 .expect("every Count_3 entry fits at degree ≤ 2");
1117 if f.diffs == vec![1, 2, 1] {
1118 found_quadratic = true; assert_eq!(f.parity_period(), 4, "the quadratic entry has Lucas period 4");
1120 }
1121 }
1122 }
1123 assert!(
1124 found_quadratic,
1125 "the machinery locates the hand-derived C(n−4,2) quadratic behind the mod-4 schedule"
1126 );
1127 }
1128
1129 #[test]
1135 fn parity_of_binomial_entries_is_eventually_periodic_by_lucas() {
1136 fn binom_exact(a: u64, k: u64) -> u128 {
1138 if k > a {
1139 return 0;
1140 }
1141 let mut c: u128 = 1;
1142 for i in 0..k {
1143 c = c * (a - i) as u128 / (i + 1) as u128;
1144 }
1145 c
1146 }
1147 for a in 0u64..=40 {
1148 for k in 0u64..=12 {
1149 assert_eq!(
1150 binom_exact(a, k) % 2 == 1,
1151 k & a == k,
1152 "Lucas: C({a},{k}) parity by the digit condition"
1153 );
1154 }
1155 }
1156 let mut seed = 0xB1_A5EDu64;
1158 let mut lcg = move || {
1159 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1160 (seed >> 33) as i128
1161 };
1162 for _ in 0..50 {
1163 let base = 4 + (lcg() % 5) as usize;
1164 let coeffs: Vec<i128> = (0..=3).map(|_| lcg() % 7 - 3).collect();
1165 let poly = |m: usize| -> i128 {
1166 let a = (m - base) as u64;
1167 coeffs
1168 .iter()
1169 .enumerate()
1170 .map(|(k, &c)| c * binom_exact(a, k as u64) as i128)
1171 .sum()
1172 };
1173 let values: Vec<i128> = (base..base + 6).map(&poly).collect();
1174 let f = FittedCount::fit(base, &values, 3).expect("a degree-3 polynomial fits");
1175 let period = f.parity_period();
1176 assert!(period.is_power_of_two() && period <= 4, "period is a small power of two");
1177 for m in base..base + 96 {
1178 assert_eq!(f.eval(m), poly(m), "evaluation reproduces the polynomial everywhere");
1179 assert_eq!(
1180 f.parity(m),
1181 f.parity(m + period),
1182 "parity repeats with the declared period (m={m})"
1183 );
1184 }
1185 }
1186 }
1187
1188 #[test]
1198 fn fixed_degree_symmetric_ns_verdict_for_php_is_decided_for_all_m_by_finite_computation() {
1199 let php = decide_invariant_witness_for_all_scales(
1201 &|m| php_instance_linear(m),
1202 &(4..=8).collect::<Vec<_>>(),
1203 2,
1204 2,
1205 );
1206 eprintln!(
1207 "PHP-linear d=2: onset={} period={} by_residue={:?} validated={:?}",
1208 php.system.onset, php.system.period, php.by_residue, php.validated
1209 );
1210 assert_eq!(php.system.period, 2, "PHP-linear entries are degree-≤1 in m — Lucas period 2");
1215 assert_eq!(
1216 php.by_residue,
1217 vec![false, false],
1218 "PHP-linear d=2: the invariant dual is EMPTY at every scale m ≥ 4"
1219 );
1220
1221 let php_clause = decide_invariant_witness_for_all_scales(
1224 &|m| php_instance_clause(m),
1225 &(4..=8).collect::<Vec<_>>(),
1226 2,
1227 2,
1228 );
1229 eprintln!(
1230 "PHP-clause d=2: onset={} period={} by_residue={:?} validated={:?}",
1231 php_clause.system.onset,
1232 php_clause.system.period,
1233 php_clause.by_residue,
1234 php_clause.validated
1235 );
1236 assert!(
1237 php_clause.by_residue.iter().all(|&v| v),
1238 "PHP-clause d=2: the invariant witness exists at EVERY scale m ≥ 4 — the encoding \
1239 chooses whether symmetry can see the bound"
1240 );
1241
1242 let count = decide_invariant_witness_for_all_scales(
1246 &|n| count_instance_linear(n, 3),
1247 &(6..=8).collect::<Vec<_>>(),
1248 2,
1249 2,
1250 );
1251 eprintln!(
1252 "Count_3 d=2: onset={} period={} by_residue={:?} validated={:?}",
1253 count.system.onset, count.system.period, count.by_residue, count.validated
1254 );
1255 assert_eq!(count.system.period, 4, "Count_3's schedule is mod 4 — the located quadratic");
1257 assert_eq!(
1261 count.by_residue,
1262 vec![false, false, false, true],
1263 "Count_3 d=2: the invariant witness lives exactly on n ≡ 3 (mod 4)"
1264 );
1265 }
1266
1267 #[test]
1281 fn the_off_schedule_witnesses_are_probed_for_stabilizer_invariance() {
1282 for n in [6usize, 7, 8] {
1284 let marked = count_instance_linear_marked(n, 3);
1285 let sys = collapsed_dual_system(&marked, 2);
1286 let collapsed = sys.solve();
1287 let direct = invariant_witness_exists_direct(&marked, 2);
1288 assert_eq!(collapsed.is_some(), direct.is_some(), "Count_3({n}) marked: paths agree");
1289 assert_eq!(
1290 collapsed.is_some(),
1291 n % 4 == 3,
1292 "Count_3({n}): one marked point does not extend the mod-4 schedule (locked)"
1293 );
1294 if let Some(sol) = collapsed {
1295 let w = sys.lift(&marked, 2, sol);
1296 assert!(
1297 check_ns_lower_bound_polys(marked.num_vars, &marked.gens, 2, &w),
1298 "Count_3({n}) marked: the lifted witness re-checks"
1299 );
1300 }
1301 eprintln!(
1302 "MARKED | Count_3({n}) d=2 (n mod 4 = {}): Stab(point)-invariant witness = {}",
1303 n % 4,
1304 collapsed.is_some()
1305 );
1306 }
1307 for m in [4usize, 5] {
1310 for (which, inst) in [
1311 ("hole", php_instance_linear_marked_hole(m)),
1312 ("pigeon", php_instance_linear_marked_pigeon(m)),
1313 ] {
1314 let sys = collapsed_dual_system(&inst, 2);
1315 let collapsed = sys.solve();
1316 let direct = invariant_witness_exists_direct(&inst, 2);
1317 assert_eq!(
1318 collapsed.is_some(),
1319 direct.is_some(),
1320 "PHP-linear({m}) marked-{which}: paths agree"
1321 );
1322 assert!(
1323 collapsed.is_none(),
1324 "PHP-linear({m}) marked-{which}: one marking sees nothing — the witnesses are \
1325 ≥ 2-deep (locked)"
1326 );
1327 eprintln!(
1328 "MARKED | PHP-linear({m}) d=2 marked-{which}: stabilizer-invariant witness = {}",
1329 collapsed.is_some()
1330 );
1331 }
1332 }
1333 }
1334
1335 #[test]
1342 fn the_symmetric_primal_dual_gap_over_gf2_is_measured() {
1343 let mut gap_cells = Vec::new();
1344 let mut cases: Vec<(String, SymmetricInstance, usize)> = Vec::new();
1345 for m in [3usize, 4, 5] {
1346 for d in 1..=3usize {
1347 cases.push((format!("PHP-linear({m}) d={d}"), php_instance_linear(m), d));
1348 }
1349 }
1350 for n in [7usize, 8] {
1351 cases.push((format!("Count_3({n}) d=2"), count_instance_linear(n, 3), 2));
1352 }
1353 for (label, inst, d) in &cases {
1354 let invariant = invariant_witness_exists_direct(inst, *d).is_some();
1355 let general = ns_lower_bound_witness_polys(inst.num_vars, &inst.gens, *d).is_some();
1356 assert!(
1357 !invariant || general,
1358 "{label}: an invariant witness IS a witness — (true, false) is impossible"
1359 );
1360 if !invariant && general {
1361 gap_cells.push(label.clone());
1362 }
1363 eprintln!("{label}: invariant={invariant} general={general}");
1364 }
1365 assert!(
1366 gap_cells.contains(&"Count_3(8) d=2".to_string()),
1367 "the char-2 gap is real: Count_3(8) at degree 2 has only asymmetric witnesses"
1368 );
1369 for cell in ["PHP-linear(4) d=2", "PHP-linear(5) d=2", "PHP-linear(5) d=3"] {
1372 assert!(
1373 gap_cells.contains(&cell.to_string()),
1374 "{cell}: a gap cell — the witness exists and is necessarily asymmetric"
1375 );
1376 }
1377 }
1378}