1use crate::cdcl::Lit;
15use crate::proof::Perm;
16
17pub(crate) fn clause_key(c: &[Lit]) -> Vec<u32> {
20 let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
21 k.sort_unstable();
22 k.dedup();
23 k
24}
25
26pub fn perm_is_automorphism(clauses: &[Vec<Lit>], sigma: &Perm) -> bool {
37 let nv = sigma.num_vars();
38 let moved: Vec<bool> =
39 (0..nv).map(|v| sigma.apply(Lit::pos(v as u32)) != Lit::pos(v as u32)).collect();
40 if moved.iter().all(|&m| !m) {
42 return true;
43 }
44 let set: std::collections::HashSet<Vec<u32>> = clauses.iter().map(|c| clause_key(c)).collect();
45 for c in clauses {
46 let touches_support =
47 c.iter().any(|l| (l.var() as usize) < nv && moved[l.var() as usize]);
48 if touches_support && !set.contains(&clause_key(&sigma.apply_clause(c))) {
49 return false;
50 }
51 }
52 true
53}
54
55pub struct AutomorphismIndex {
67 nv: usize,
68 clauses: Vec<Vec<Lit>>,
69 keys: std::collections::HashSet<Vec<u32>>,
70 index_by_key: std::collections::HashMap<Vec<u32>, usize>,
74 by_var: Vec<Vec<usize>>,
75 base: Vec<Option<bool>>,
80 base_conflict: bool,
83 temp_gen: Vec<u32>,
87 temp_val: Vec<bool>,
88 gen: u32,
89 visited_gen: Vec<u32>,
91 vgen: u32,
92}
93
94impl AutomorphismIndex {
95 pub fn new(nv: usize) -> Self {
97 AutomorphismIndex {
98 nv,
99 clauses: Vec::new(),
100 keys: std::collections::HashSet::new(),
101 index_by_key: std::collections::HashMap::new(),
102 by_var: vec![Vec::new(); nv],
103 base: vec![None; nv],
104 base_conflict: false,
105 temp_gen: vec![0; nv],
106 temp_val: vec![false; nv],
107 gen: 0,
108 visited_gen: Vec::new(),
109 vgen: 0,
110 }
111 }
112
113 pub fn with_clauses(nv: usize, clauses: &[Vec<Lit>]) -> Self {
115 let mut ix = Self::new(nv);
116 for c in clauses {
117 ix.insert(c.clone());
118 }
119 ix
120 }
121
122 pub fn insert(&mut self, clause: Vec<Lit>) {
125 let idx = self.clauses.len();
126 let key = clause_key(&clause);
127 self.keys.insert(key.clone());
128 self.index_by_key.insert(key, idx);
129 let mut touched: Vec<usize> = clause.iter().map(|l| l.var() as usize).filter(|&v| v < self.nv).collect();
130 touched.sort_unstable();
131 touched.dedup();
132 for v in touched {
133 self.by_var[v].push(idx);
134 }
135 self.visited_gen.push(0);
136 self.clauses.push(clause);
137 self.base_propagate(idx);
140 }
141
142 fn base_propagate(&mut self, start: usize) {
145 if self.base_conflict {
146 return;
147 }
148 let mut wl = vec![start];
149 let mut wi = 0;
150 while wi < wl.len() {
151 let ci = wl[wi];
152 wi += 1;
153 let mut satisfied = false;
154 let mut unit: Option<Lit> = None;
155 let mut more_than_one = false;
156 for &l in &self.clauses[ci] {
157 let val = self.base[l.var() as usize].map(|b| b == l.is_positive());
158 match val {
159 Some(true) => {
160 satisfied = true;
161 break;
162 }
163 Some(false) => {}
164 None => match unit {
165 None => unit = Some(l),
166 Some(u) if u == l => {}
167 Some(u) if u == l.negated() => {
168 satisfied = true;
169 break;
170 }
171 Some(_) => more_than_one = true,
172 },
173 }
174 }
175 if satisfied {
176 continue;
177 }
178 match unit {
179 None => {
180 self.base_conflict = true;
181 return;
182 }
183 Some(u) if !more_than_one => {
184 self.base[u.var() as usize] = Some(u.is_positive());
185 for &cj in &self.by_var[u.var() as usize] {
186 wl.push(cj);
187 }
188 }
189 _ => {}
190 }
191 }
192 }
193
194 pub fn is_automorphism(&mut self, sigma: &Perm) -> bool {
198 self.vgen = self.vgen.wrapping_add(1);
199 if self.vgen == 0 {
200 for g in &mut self.visited_gen {
201 *g = 0;
202 }
203 self.vgen = 1;
204 }
205 let involution = (0..self.nv as u32).all(|v| {
209 let pv = Lit::pos(v);
210 sigma.apply(sigma.apply(pv)) == pv
211 });
212 let mut keybuf: Vec<u32> = Vec::new();
216 for v in 0..self.nv {
217 if sigma.apply(Lit::pos(v as u32)) == Lit::pos(v as u32) {
218 continue;
219 }
220 for k in 0..self.by_var[v].len() {
221 let ci = self.by_var[v][k];
222 if self.visited_gen[ci] == self.vgen {
223 continue;
224 }
225 self.visited_gen[ci] = self.vgen;
226 keybuf.clear();
227 for &l in &self.clauses[ci] {
228 let m = sigma.apply(l);
229 keybuf.push(m.var() * 2 + u32::from(!m.is_positive()));
230 }
231 keybuf.sort_unstable();
232 keybuf.dedup();
233 if !self.keys.contains(keybuf.as_slice()) {
234 return false;
235 }
236 if involution {
239 if let Some(&partner) = self.index_by_key.get(keybuf.as_slice()) {
240 self.visited_gen[partner] = self.vgen;
241 }
242 }
243 }
244 }
245 true
246 }
247
248 pub fn propagate_to_conflict(&mut self, _num_vars: usize, assume: &[Lit]) -> bool {
256 if self.base_conflict {
257 return true;
258 }
259 self.gen = self.gen.wrapping_add(1);
260 if self.gen == 0 {
261 for g in &mut self.temp_gen {
262 *g = 0;
263 }
264 self.gen = 1;
265 }
266 let mut queue: Vec<u32> = Vec::new();
267 for &l in assume {
270 let v = l.var() as usize;
271 let cur = if self.temp_gen[v] == self.gen { Some(self.temp_val[v]) } else { self.base[v] };
272 match cur {
273 Some(b) if b == l.is_positive() => {}
274 Some(_) => return true,
275 None => {
276 self.temp_gen[v] = self.gen;
277 self.temp_val[v] = l.is_positive();
278 queue.push(v as u32);
279 }
280 }
281 }
282 let mut qi = 0;
283 while qi < queue.len() {
284 let v = queue[qi] as usize;
285 qi += 1;
286 for k in 0..self.by_var[v].len() {
287 let ci = self.by_var[v][k];
288 let mut satisfied = false;
289 let mut unit: Option<Lit> = None;
290 let mut more_than_one = false;
291 for &l in &self.clauses[ci] {
292 let vi = l.var() as usize;
293 let raw = if self.temp_gen[vi] == self.gen { Some(self.temp_val[vi]) } else { self.base[vi] };
294 match raw.map(|b| b == l.is_positive()) {
295 Some(true) => {
296 satisfied = true;
297 break;
298 }
299 Some(false) => {}
300 None => match unit {
301 None => unit = Some(l),
302 Some(u) if u == l => {}
303 Some(u) if u == l.negated() => {
304 satisfied = true;
305 break;
306 }
307 Some(_) => more_than_one = true,
308 },
309 }
310 }
311 if satisfied {
312 continue;
313 }
314 match unit {
315 None => return true, Some(u) if !more_than_one => {
317 let uv = u.var() as usize;
318 self.temp_gen[uv] = self.gen;
319 self.temp_val[uv] = u.is_positive();
320 queue.push(uv as u32);
321 }
322 _ => {}
323 }
324 }
325 }
326 false
327 }
328}
329
330pub fn find_generators(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Perm> {
351 if num_vars == 0 {
352 return Vec::new();
353 }
354 let (adj, init_colors) = build_graph(num_vars, clauses);
355 let adj_sorted: Vec<Vec<usize>> = adj
356 .iter()
357 .map(|a| {
358 let mut s = a.clone();
359 s.sort_unstable();
360 s
361 })
362 .collect();
363 let base = refine(&adj, &init_colors);
364 let mut ctx = Search {
365 adj: &adj,
366 adj_sorted: &adj_sorted,
367 init_colors: &init_colors,
368 num_vars,
369 clauses,
370 parent: (0..adj.len()).collect(),
371 first_leaf: None,
372 gens: Vec::new(),
373 seen: std::collections::HashSet::new(),
374 budget: 500_000,
375 };
376 ctx.search(base);
377 ctx.gens
378}
379
380struct Search<'a> {
382 adj: &'a [Vec<usize>],
383 adj_sorted: &'a [Vec<usize>],
384 init_colors: &'a [u32],
385 num_vars: usize,
386 clauses: &'a [Vec<Lit>],
387 parent: Vec<usize>,
389 first_leaf: Option<Vec<u32>>,
391 gens: Vec<Perm>,
392 seen: std::collections::HashSet<Vec<u32>>,
394 budget: usize,
396}
397
398impl Search<'_> {
399 fn find(&mut self, mut x: usize) -> usize {
400 while self.parent[x] != x {
401 self.parent[x] = self.parent[self.parent[x]];
402 x = self.parent[x];
403 }
404 x
405 }
406
407 fn union(&mut self, a: usize, b: usize) {
408 let (ra, rb) = (self.find(a), self.find(b));
409 if ra != rb {
410 self.parent[ra] = rb;
411 }
412 }
413
414 fn search(&mut self, coloring: Vec<u32>) {
415 if self.budget == 0 {
416 return;
417 }
418 self.budget -= 1;
419 match target_cell(&coloring) {
420 None => self.visit_leaf(coloring),
421 Some(cell) => {
422 let mut explored: Vec<usize> = Vec::new();
425 for (i, &u) in cell.iter().enumerate() {
426 if i > 0 {
427 let ru = self.find(u);
428 if explored.iter().any(|&r| self.find(r) == ru) {
429 continue;
430 }
431 }
432 explored.push(u);
433 let child = individualize(self.adj, &coloring, u);
434 self.search(child);
435 }
436 }
437 }
438 }
439
440 fn visit_leaf(&mut self, leaf: Vec<u32>) {
441 let ref0 = match &self.first_leaf {
442 None => {
443 self.first_leaf = Some(leaf);
444 return;
445 }
446 Some(r) => r.clone(),
447 };
448 let Some((vperm, lperm)) = self.candidate(&ref0, &leaf) else { return };
449 let key = perm_key(self.num_vars, &lperm);
450 if !lperm.is_identity() && !self.seen.contains(&key) && perm_is_automorphism(self.clauses, &lperm) {
451 self.seen.insert(key);
452 for u in 0..vperm.len() {
453 self.union(u, vperm[u]);
454 }
455 self.gens.push(lperm);
456 }
457 }
458
459 fn candidate(&self, ref0: &[u32], leaf: &[u32]) -> Option<(Vec<usize>, Perm)> {
463 let v = ref0.len();
464 let mut leaf_inv = vec![0usize; v];
465 for (u, &r) in leaf.iter().enumerate() {
466 leaf_inv[r as usize] = u;
467 }
468 let g: Vec<usize> = (0..v).map(|u| leaf_inv[ref0[u] as usize]).collect();
469 for u in 0..v {
470 if self.init_colors[u] != self.init_colors[g[u]] {
471 return None;
472 }
473 let mut mapped: Vec<usize> = self.adj[u].iter().map(|&w| g[w]).collect();
474 mapped.sort_unstable();
475 if mapped != self.adj_sorted[g[u]] {
476 return None;
477 }
478 }
479 let nlit = 2 * self.num_vars;
480 let mut images = Vec::with_capacity(self.num_vars);
481 for var in 0..self.num_vars {
482 let gv = g[2 * var];
483 if gv >= nlit {
484 return None;
485 }
486 images.push(Lit::new((gv / 2) as u32, gv % 2 == 0));
487 }
488 Some((g, Perm::from_images(images)))
489 }
490}
491
492fn literal_perm_to_points(num_vars: usize, sigma: &Perm) -> crate::permgroup::Perm {
496 let idx = |l: Lit| 2 * l.var() as usize + usize::from(!l.is_positive());
497 let mut p = vec![0usize; 2 * num_vars];
498 for v in 0..num_vars as u32 {
499 for l in [Lit::pos(v), Lit::neg(v)] {
500 p[idx(l)] = idx(sigma.apply(l));
501 }
502 }
503 p
504}
505
506pub fn automorphism_group(num_vars: usize, clauses: &[Vec<Lit>]) -> crate::permgroup::Bsgs {
514 let gens: Vec<crate::permgroup::Perm> = find_generators(num_vars, clauses)
515 .iter()
516 .filter(|g| !g.is_identity())
517 .map(|g| literal_perm_to_points(num_vars, g))
518 .collect();
519 crate::permgroup::schreier_sims(2 * num_vars, &gens)
520}
521
522pub fn aut_order(num_vars: usize, clauses: &[Vec<Lit>]) -> u128 {
524 automorphism_group(num_vars, clauses).order()
525}
526
527fn build_graph(num_vars: usize, clauses: &[Vec<Lit>]) -> (Vec<Vec<usize>>, Vec<u32>) {
529 let nlit = 2 * num_vars;
530 let vtotal = nlit + clauses.len();
531 let mut adj = vec![Vec::new(); vtotal];
532 let mut color = vec![0u32; vtotal];
533 for var in 0..num_vars {
534 let (a, b) = (2 * var, 2 * var + 1);
535 adj[a].push(b);
536 adj[b].push(a);
537 }
538 for (ci, clause) in clauses.iter().enumerate() {
539 let cv = nlit + ci;
540 color[cv] = 1 + clause.len() as u32;
541 for &l in clause {
542 let lv = (l.var() * 2 + u32::from(!l.is_positive())) as usize;
543 adj[cv].push(lv);
544 adj[lv].push(cv);
545 }
546 }
547 for a in adj.iter_mut() {
548 a.sort_unstable();
549 a.dedup();
550 }
551 (adj, color)
552}
553
554fn refine(adj: &[Vec<usize>], colors: &[u32]) -> Vec<u32> {
557 let v = adj.len();
558 let mut cur = colors.to_vec();
559 loop {
560 let sigs: Vec<(u32, Vec<u32>)> = (0..v)
561 .map(|u| {
562 let mut nb: Vec<u32> = adj[u].iter().map(|&w| cur[w]).collect();
563 nb.sort_unstable();
564 (cur[u], nb)
565 })
566 .collect();
567 let mut order: Vec<usize> = (0..v).collect();
568 order.sort_by(|&a, &b| sigs[a].cmp(&sigs[b]));
569 let mut new = vec![0u32; v];
570 let mut rank = 0u32;
571 for i in 0..v {
572 if i > 0 && sigs[order[i]] != sigs[order[i - 1]] {
573 rank += 1;
574 }
575 new[order[i]] = rank;
576 }
577 if new == cur {
578 return new;
579 }
580 cur = new;
581 }
582}
583
584fn individualize(adj: &[Vec<usize>], colors: &[u32], u: usize) -> Vec<u32> {
586 let mut c = colors.to_vec();
587 c[u] = u32::MAX;
588 refine(adj, &c)
589}
590
591fn target_cell(colors: &[u32]) -> Option<Vec<usize>> {
594 let maxc = *colors.iter().max().unwrap();
595 for c in 0..=maxc {
596 let members: Vec<usize> = (0..colors.len()).filter(|&u| colors[u] == c).collect();
597 if members.len() > 1 {
598 return Some(members);
599 }
600 }
601 None
602}
603
604fn perm_key(num_vars: usize, perm: &Perm) -> Vec<u32> {
606 (0..num_vars as u32)
607 .map(|v| {
608 let l = perm.apply(Lit::pos(v));
609 l.var() * 2 + u32::from(!l.is_positive())
610 })
611 .collect()
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use crate::cdcl::Lit;
618 use crate::families;
619 use crate::proof::Perm;
620
621 fn swap_pigeon_rows(n: usize, p0: usize, p1: usize) -> Perm {
624 let holes = n - 1;
625 let images: Vec<Lit> = (0..n * holes)
626 .map(|v| {
627 let (p, h) = (v / holes, v % holes);
628 let np = if p == p0 {
629 p1
630 } else if p == p1 {
631 p0
632 } else {
633 p
634 };
635 Lit::pos((np * holes + h) as u32)
636 })
637 .collect();
638 Perm::from_images(images)
639 }
640
641 #[test]
642 fn identity_is_always_an_automorphism() {
643 let (cnf, _) = families::php(4);
644 assert!(perm_is_automorphism(&cnf.clauses, &Perm::identity(cnf.num_vars)));
645 }
646
647 #[test]
648 fn swapping_pigeon_rows_is_an_automorphism_of_php() {
649 let (cnf, _) = families::php(4);
650 for (p0, p1) in [(0, 1), (1, 2), (0, 3), (2, 3)] {
651 let sigma = swap_pigeon_rows(4, p0, p1);
652 assert!(
653 perm_is_automorphism(&cnf.clauses, &sigma),
654 "swapping pigeons {p0},{p1} must preserve PHP(4)"
655 );
656 }
657 }
658
659 #[test]
660 fn support_restricted_check_matches_brute_force_set_equality() {
661 let mut state = 0xA5A5_5A5A_DEAD_BEEFu64;
666 let mut next = || {
667 state = state.wrapping_add(0x9E3779B97F4A7C15);
668 let mut z = state;
669 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
670 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
671 z ^ (z >> 31)
672 };
673 let num_vars = 5usize;
674 let brute = |clauses: &[Vec<Lit>], sigma: &Perm| -> bool {
675 use std::collections::BTreeSet;
676 let orig: BTreeSet<Vec<u32>> = clauses.iter().map(|c| clause_key(c)).collect();
677 let mapped: BTreeSet<Vec<u32>> =
678 clauses.iter().map(|c| clause_key(&sigma.apply_clause(c))).collect();
679 orig == mapped
680 };
681 let mut accepts = 0;
682 for _ in 0..20_000 {
683 let nclauses = next() as usize % 6;
684 let clauses: Vec<Vec<Lit>> = (0..nclauses)
685 .map(|_| {
686 let len = 1 + (next() as usize % 3);
687 let mut c = Vec::new();
688 for _ in 0..len {
689 let v = (next() as u32) % num_vars as u32;
690 let lit = Lit::new(v, next() & 1 == 0);
691 if !c.contains(&lit) && !c.contains(&lit.negated()) {
692 c.push(lit);
693 }
694 }
695 c
696 })
697 .filter(|c| !c.is_empty())
698 .collect();
699 let sigma = {
700 let mut order: Vec<u32> = (0..num_vars as u32).collect();
701 for i in (1..num_vars).rev() {
702 let j = next() as usize % (i + 1);
703 order.swap(i, j);
704 }
705 Perm::from_images((0..num_vars).map(|v| Lit::new(order[v], next() & 1 == 0)).collect())
706 };
707 let fast = perm_is_automorphism(&clauses, &sigma);
708 assert_eq!(fast, brute(&clauses, &sigma), "fast vs brute disagree: clauses={clauses:?}");
709 if fast {
710 accepts += 1;
711 }
712 }
713 assert!(accepts > 0, "the differential must exercise genuine acceptances, not just rejects");
714 }
715
716 #[test]
717 fn incremental_index_matches_stateless_automorphism_check() {
718 let mut state = 0x1234_5678_9ABC_DEF0u64;
722 let mut next = || {
723 state = state.wrapping_add(0x9E3779B97F4A7C15);
724 let mut z = state;
725 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
726 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
727 z ^ (z >> 31)
728 };
729 let num_vars = 5usize;
730 let mut agree = 0;
731 for _ in 0..20_000 {
732 let nclauses = next() as usize % 7;
733 let clauses: Vec<Vec<Lit>> = (0..nclauses)
734 .map(|_| {
735 let len = 1 + (next() as usize % 3);
736 let mut c = Vec::new();
737 for _ in 0..len {
738 let v = (next() as u32) % num_vars as u32;
739 let lit = Lit::new(v, next() & 1 == 0);
740 if !c.contains(&lit) && !c.contains(&lit.negated()) {
741 c.push(lit);
742 }
743 }
744 c
745 })
746 .filter(|c| !c.is_empty())
747 .collect();
748 let mut ix = AutomorphismIndex::new(num_vars);
750 for c in &clauses {
751 ix.insert(c.clone());
752 }
753 let sigma = {
754 let mut order: Vec<u32> = (0..num_vars as u32).collect();
755 for i in (1..num_vars).rev() {
756 let j = next() as usize % (i + 1);
757 order.swap(i, j);
758 }
759 Perm::from_images((0..num_vars).map(|v| Lit::new(order[v], next() & 1 == 0)).collect())
760 };
761 assert_eq!(
762 ix.is_automorphism(&sigma),
763 perm_is_automorphism(&clauses, &sigma),
764 "incremental index disagrees with stateless check on {clauses:?}"
765 );
766 agree += 1;
767 }
768 assert_eq!(agree, 20_000);
769 }
770
771 #[test]
772 fn occurrence_propagation_matches_full_scan_propagation() {
773 let mut state = 0x0F0F_F0F0_1357_9BDFu64;
778 let mut next = || {
779 state = state.wrapping_add(0x9E3779B97F4A7C15);
780 let mut z = state;
781 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
782 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
783 z ^ (z >> 31)
784 };
785 let num_vars = 6usize;
786 let reference = |clauses: &[Vec<Lit>], assume: &[Lit]| -> bool {
787 let mut assign: Vec<Option<bool>> = vec![None; num_vars];
788 for &l in assume {
789 if !crate::rup::set_true(&mut assign, l) {
790 return true;
791 }
792 }
793 crate::rup::propagate(clauses, &mut assign)
794 };
795 let mut conflicts = 0;
796 for _ in 0..20_000 {
797 let nclauses = next() as usize % 8;
798 let clauses: Vec<Vec<Lit>> = (0..nclauses)
799 .map(|_| {
800 let len = 1 + (next() as usize % 4);
801 (0..len).map(|_| Lit::new((next() as u32) % num_vars as u32, next() & 1 == 0)).collect()
802 })
803 .filter(|c: &Vec<Lit>| !c.is_empty())
804 .collect();
805 let mut ix = AutomorphismIndex::new(num_vars);
806 for c in &clauses {
807 ix.insert(c.clone());
808 }
809 let nassume = next() as usize % 4;
810 let assume: Vec<Lit> =
811 (0..nassume).map(|_| Lit::new((next() as u32) % num_vars as u32, next() & 1 == 0)).collect();
812 let got = ix.propagate_to_conflict(num_vars, &assume);
813 assert_eq!(got, reference(&clauses, &assume), "clauses={clauses:?} assume={assume:?}");
814 if got {
815 conflicts += 1;
816 }
817 }
818 assert!(conflicts > 0, "the differential must exercise genuine conflicts");
819 }
820
821 #[test]
822 fn a_non_symmetry_is_rejected() {
823 let (cnf, _) = families::php(3);
826 let holes = 2;
827 let images: Vec<Lit> = (0..cnf.num_vars)
828 .map(|v| {
829 let (p, h) = (v / holes, v % holes);
830 let np = if p == 0 { 1 } else { p };
831 Lit::pos((np * holes + h) as u32)
832 })
833 .collect();
834 assert!(!perm_is_automorphism(&cnf.clauses, &Perm::from_images(images)));
835 }
836
837 use std::collections::HashSet;
840
841 fn p(v: u32) -> Lit {
842 Lit::pos(v)
843 }
844 fn neg(v: u32) -> Lit {
845 Lit::neg(v)
846 }
847
848 fn all_var_perms(n: usize) -> Vec<Vec<usize>> {
850 fn rec(cur: &mut Vec<usize>, rem: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
851 if rem.is_empty() {
852 out.push(cur.clone());
853 return;
854 }
855 for i in 0..rem.len() {
856 let x = rem.remove(i);
857 cur.push(x);
858 rec(cur, rem, out);
859 cur.pop();
860 rem.insert(i, x);
861 }
862 }
863 let mut out = Vec::new();
864 rec(&mut Vec::new(), &mut (0..n).collect(), &mut out);
865 out
866 }
867
868 fn brute_force_group(num_vars: usize, clauses: &[Vec<Lit>]) -> HashSet<Vec<u32>> {
871 let mut set = HashSet::new();
872 for vp in all_var_perms(num_vars) {
873 for phase in 0..(1u32 << num_vars) {
874 let images: Vec<Lit> =
875 (0..num_vars).map(|v| Lit::new(vp[v] as u32, (phase >> v) & 1 == 0)).collect();
876 let perm = Perm::from_images(images);
877 if perm_is_automorphism(clauses, &perm) {
878 set.insert(perm_key(num_vars, &perm));
879 }
880 }
881 }
882 set
883 }
884
885 fn generated_group(num_vars: usize, gens: &[Perm]) -> HashSet<Vec<u32>> {
887 let id = Perm::identity(num_vars);
888 let mut set = HashSet::new();
889 set.insert(perm_key(num_vars, &id));
890 let mut frontier = vec![id];
891 while let Some(x) = frontier.pop() {
892 for g in gens {
893 let y = g.compose(&x);
894 let k = perm_key(num_vars, &y);
895 if set.insert(k) {
896 frontier.push(y);
897 }
898 }
899 }
900 set
901 }
902
903 #[test]
904 fn finds_the_full_php3_symmetry_group() {
905 let (cnf, _) = families::php(3);
906 let gens = find_generators(cnf.num_vars, &cnf.clauses);
907 let group = generated_group(cnf.num_vars, &gens);
908 assert_eq!(group.len(), 12, "discovered generators must generate the full S_3 × S_2");
910 }
911
912 #[test]
913 fn discovered_generators_match_brute_force_across_cases() {
914 let php2 = families::php(2).0;
916 let php3 = families::php(3).0;
917 let cases: Vec<(usize, Vec<Vec<Lit>>)> = vec![
918 (php2.num_vars, php2.clauses),
919 (php3.num_vars, php3.clauses),
920 (2, vec![vec![p(0), p(1)], vec![neg(0), neg(1)]]), (3, vec![vec![p(0)], vec![p(0), p(1)]]), (3, vec![vec![p(0), p(1), p(2)]]), ];
924 for (num_vars, clauses) in cases {
925 let gens = find_generators(num_vars, &clauses);
926 let found = generated_group(num_vars, &gens);
927 let truth = brute_force_group(num_vars, &clauses);
928 assert_eq!(found, truth, "finder must generate exactly Aut(F) for {clauses:?}");
929 }
930 }
931
932 #[test]
933 fn every_discovered_generator_is_a_verified_automorphism() {
934 let (cnf, _) = families::php(4);
935 let gens = find_generators(cnf.num_vars, &cnf.clauses);
936 assert!(!gens.is_empty(), "PHP(4) is highly symmetric — generators must be found");
937 for g in &gens {
938 assert!(perm_is_automorphism(&cnf.clauses, g), "every returned generator is sound");
939 }
940 }
941
942 #[test]
943 fn an_asymmetric_formula_yields_no_nontrivial_generators() {
944 let clauses = vec![vec![p(0)], vec![p(0), p(1)], vec![neg(1), p(2)]];
945 let gens = find_generators(3, &clauses);
946 assert!(gens.iter().all(|g| g.is_identity()), "no non-trivial symmetry to find");
947 }
948
949 #[test]
954 fn the_bsgs_backend_computes_the_automorphism_group_order() {
955 let fact = |k: u128| (1..=k).product::<u128>();
956 for n in 3..=5usize {
957 let (cnf, _) = crate::families::php(n);
958 assert_eq!(
959 aut_order(cnf.num_vars, &cnf.clauses),
960 fact(n as u128) * fact((n - 1) as u128),
961 "|Aut(PHP({n}))| = n!·(n-1)!"
962 );
963 }
964 for &(n, k) in &[(4usize, 3usize), (5, 3)] {
965 let (cnf, _) = crate::families::clique_coloring(n, k);
966 assert_eq!(
967 aut_order(cnf.num_vars, &cnf.clauses),
968 fact(n as u128) * fact(k as u128),
969 "|Aut(clique_coloring({n},{k}))| = n!·k!"
970 );
971 }
972 }
973
974 #[test]
977 fn the_bsgs_backend_decides_automorphism_membership() {
978 let (cnf, _) = crate::families::php(3); let nv = cnf.num_vars;
980 let bsgs = automorphism_group(nv, &cnf.clauses);
981 for g in find_generators(nv, &cnf.clauses) {
982 assert!(bsgs.contains(&literal_perm_to_points(nv, &g)), "a detected generator is a member");
983 }
984 assert!(bsgs.contains(&(0..2 * nv).collect::<Vec<_>>()), "the identity is a member");
985 let mut bad: Vec<usize> = (0..2 * nv).collect();
987 bad.swap(0, 2); bad.swap(1, 3); assert!(!bsgs.contains(&bad), "a non-automorphism variable swap must be rejected");
990 }
991}