1use crate::cdcl::{Lit, SolveResult, Solver};
16use crate::permgroup::Perm;
17
18pub fn is_lex_leader(group: &[Perm], a: &[bool]) -> bool {
21 let lit_group: Vec<Vec<Lit>> = group.iter().map(|g| perm_to_litsym(g)).collect();
22 is_lex_leader_lit(&lit_group, a)
23}
24
25pub fn is_lex_leader_lit(group: &[Vec<Lit>], a: &[bool]) -> bool {
28 let eval = |l: &Lit| if l.is_positive() { a[l.var() as usize] } else { !a[l.var() as usize] };
29 group.iter().all(|img| {
30 for j in 0..a.len() {
31 let (x, y) = (a[j], eval(&img[j]));
32 if x != y {
33 return !x && y;
34 }
35 }
36 true
37 })
38}
39
40pub fn lex_leader_sbp(num_vars: usize, group: &[Perm]) -> (Vec<Vec<Lit>>, usize) {
47 let lit_group: Vec<Vec<Lit>> = group.iter().map(|g| perm_to_litsym(g)).collect();
48 lex_leader_sbp_lit(num_vars, &lit_group)
49}
50
51pub fn lex_leader_sbp_lit(num_vars: usize, group: &[Vec<Lit>]) -> (Vec<Vec<Lit>>, usize) {
55 let mut clauses = Vec::new();
56 let mut aux = num_vars;
57 for img in group {
58 if (0..num_vars).all(|j| img[j] == Lit::pos(j as u32)) {
59 continue; }
61 encode_lex_le(num_vars, img, &mut aux, &mut clauses);
62 }
63 (clauses, aux)
64}
65
66fn perm_to_litsym(g: &Perm) -> Vec<Lit> {
68 g.iter().map(|&j| Lit::pos(j as u32)).collect()
69}
70
71pub fn affine_lex_leader_sbp(num_vars: usize, maps: &[Vec<(Vec<usize>, bool)>]) -> (Vec<Vec<Lit>>, usize) {
79 let mut clauses = Vec::new();
80 let mut aux = num_vars;
81 for map in maps {
82 let mut img: Vec<Lit> = (0..num_vars).map(|j| Lit::pos(j as u32)).collect();
83 for (j, (xset, b)) in map.iter().enumerate() {
84 if j >= num_vars {
85 break;
86 }
87 if xset.len() == 1 && xset[0] == j && !b {
88 continue; }
90 img[j] = tseitin_xor(&mut aux, xset, *b, &mut clauses);
91 }
92 if (0..num_vars).all(|j| img[j] == Lit::pos(j as u32)) {
93 continue; }
95 encode_lex_le(num_vars, &img, &mut aux, &mut clauses);
96 }
97 (clauses, aux)
98}
99
100fn tseitin_xor(aux: &mut usize, xset: &[usize], b: bool, clauses: &mut Vec<Vec<Lit>>) -> Lit {
104 let mut fresh = |aux: &mut usize| {
105 let v = *aux as u32;
106 *aux += 1;
107 Lit::pos(v)
108 };
109 let mut acc = Lit::pos(xset[0] as u32);
110 for &v in &xset[1..] {
111 let vv = Lit::pos(v as u32);
112 let t = fresh(aux);
113 clauses.push(vec![t, acc.negated(), vv]);
114 clauses.push(vec![t, acc, vv.negated()]);
115 clauses.push(vec![t.negated(), acc, vv]);
116 clauses.push(vec![t.negated(), acc.negated(), vv.negated()]);
117 acc = t;
118 }
119 if b {
120 acc.negated()
121 } else {
122 acc
123 }
124}
125
126fn encode_lex_le(num_vars: usize, img: &[Lit], aux: &mut usize, clauses: &mut Vec<Vec<Lit>>) {
132 let mut fresh = |aux: &mut usize| {
133 let v = *aux as u32;
134 *aux += 1;
135 Lit::pos(v)
136 };
137 let positions: Vec<usize> = (0..num_vars).filter(|&j| img[j] != Lit::pos(j as u32)).collect();
138 let mut prev_e: Option<Lit> = None; for (k, &j) in positions.iter().enumerate() {
140 let aj = Lit::pos(j as u32);
141 let cj = img[j];
142 match prev_e {
144 None => clauses.push(vec![aj.negated(), cj]),
145 Some(e) => clauses.push(vec![e.negated(), aj.negated(), cj]),
146 }
147 if k + 1 == positions.len() {
148 break; }
150 let eq = fresh(aux);
152 clauses.push(vec![eq.negated(), aj.negated(), cj]);
153 clauses.push(vec![eq.negated(), aj, cj.negated()]);
154 clauses.push(vec![eq, aj.negated(), cj.negated()]);
155 clauses.push(vec![eq, aj, cj]);
156 let e_next = fresh(aux);
158 match prev_e {
159 None => {
160 clauses.push(vec![e_next.negated(), eq]);
161 clauses.push(vec![e_next, eq.negated()]);
162 }
163 Some(e) => {
164 clauses.push(vec![e_next.negated(), e]);
165 clauses.push(vec![e_next.negated(), eq]);
166 clauses.push(vec![e_next, e.negated(), eq.negated()]);
167 }
168 }
169 prev_e = Some(e_next);
170 }
171}
172
173pub fn variable_automorphism_generators(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<Vec<Perm>> {
177 let lit_gens = crate::symmetry_detect::find_generators(num_vars, clauses);
178 let mut var_gens: Vec<Perm> = Vec::new();
179 for g in &lit_gens {
180 if g.is_identity() {
181 continue;
182 }
183 let mut vp = vec![0usize; num_vars];
184 for v in 0..num_vars as u32 {
185 let img = g.apply(Lit::pos(v));
186 if !img.is_positive() {
187 return None; }
189 vp[v as usize] = img.var() as usize;
190 }
191 var_gens.push(vp);
192 }
193 Some(var_gens)
194}
195
196pub fn variable_automorphism_group(num_vars: usize, clauses: &[Vec<Lit>], cap: usize) -> Option<Vec<Perm>> {
201 let gens = variable_automorphism_generators(num_vars, clauses)?;
202 crate::permgroup::schreier_sims(num_vars, &gens).elements(cap)
203}
204
205fn lit_idx(l: Lit) -> usize {
207 2 * l.var() as usize + usize::from(!l.is_positive())
208}
209
210pub fn litsym_to_points(img: &[Lit], num_vars: usize) -> Perm {
213 let mut p = vec![0usize; 2 * num_vars];
214 for (j, &l) in img.iter().enumerate() {
215 p[lit_idx(Lit::pos(j as u32))] = lit_idx(l);
216 p[lit_idx(Lit::neg(j as u32))] = lit_idx(l.negated());
217 }
218 p
219}
220
221pub fn litsym_from_points(p: &[usize], num_vars: usize) -> Vec<Lit> {
224 (0..num_vars)
225 .map(|j| {
226 let q = p[2 * j];
227 Lit::new((q / 2) as u32, q % 2 == 0)
228 })
229 .collect()
230}
231
232pub fn literal_automorphism_generators(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<Lit>> {
236 crate::symmetry_detect::find_generators(num_vars, clauses)
237 .iter()
238 .filter(|g| !g.is_identity())
239 .map(|g| (0..num_vars as u32).map(|v| g.apply(Lit::pos(v))).collect())
240 .collect()
241}
242
243fn simplify_under(clauses: &[Vec<Lit>], fixed: &[Lit]) -> Vec<Vec<Lit>> {
247 let true_lits: std::collections::HashSet<(u32, bool)> =
248 fixed.iter().map(|l| (l.var(), l.is_positive())).collect();
249 let mut out = Vec::new();
250 'clause: for c in clauses {
251 let mut nc = Vec::new();
252 for &l in c {
253 if true_lits.contains(&(l.var(), l.is_positive())) {
254 continue 'clause; }
256 if true_lits.contains(&(l.var(), !l.is_positive())) {
257 continue; }
259 nc.push(l);
260 }
261 out.push(nc);
262 }
263 out
264}
265
266pub fn conditional_symmetry_generators(num_vars: usize, clauses: &[Vec<Lit>], fixed: &[Lit]) -> Vec<Vec<Lit>> {
272 literal_automorphism_generators(num_vars, &simplify_under(clauses, fixed))
273}
274
275pub fn literal_automorphism_group(num_vars: usize, clauses: &[Vec<Lit>], cap: usize) -> Option<Vec<Vec<Lit>>> {
279 let gens = literal_automorphism_generators(num_vars, clauses);
280 let point_gens: Vec<Perm> = gens.iter().map(|s| litsym_to_points(s, num_vars)).collect();
281 let elems = crate::permgroup::schreier_sims(2 * num_vars, &point_gens).elements(cap)?;
282 Some(elems.iter().map(|p| litsym_from_points(p, num_vars)).collect())
283}
284
285fn count_projected_models(total_vars: usize, num_orig: usize, clauses: &[Vec<Lit>]) -> usize {
289 let mut seen: std::collections::BTreeSet<Vec<bool>> = std::collections::BTreeSet::new();
290 loop {
291 let mut s = Solver::new(total_vars);
292 for c in clauses {
293 s.add_clause(c.clone());
294 }
295 for proj in &seen {
296 s.add_clause((0..num_orig).map(|v| Lit::new(v as u32, !proj[v])).collect());
297 }
298 match s.solve() {
299 SolveResult::Sat(m) => {
300 seen.insert((0..num_orig).map(|v| m[v]).collect());
301 }
302 SolveResult::Unsat => break,
303 }
304 }
305 seen.len()
306}
307
308pub fn count_models_modulo_symmetry(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<usize> {
314 let group = literal_automorphism_group(num_vars, clauses, 100_000)?;
315 let (sbp, total) = lex_leader_sbp_lit(num_vars, &group);
316 let mut broken = clauses.to_vec();
317 broken.extend(sbp);
318 Some(count_projected_models(total, num_vars, &broken))
319}
320
321pub fn hierarchical_break(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<(Vec<Vec<Lit>>, usize)> {
330 let gens = variable_automorphism_generators(num_vars, clauses)?;
331 if gens.is_empty() {
332 return None;
333 }
334 let bsgs = crate::permgroup::schreier_sims(num_vars, &gens);
335 let blocks = crate::permgroup::minimal_block_system(num_vars, &gens)?; let (k, m) = (blocks.len(), blocks[0].len());
337 let mut structured: Vec<Vec<Lit>> = Vec::new();
338
339 for i in 0..k.saturating_sub(1) {
341 let mut p: Vec<usize> = (0..num_vars).collect();
342 for j in 0..m {
343 p[blocks[i][j]] = blocks[i + 1][j];
344 p[blocks[i + 1][j]] = blocks[i][j];
345 }
346 if bsgs.contains(&p) {
347 structured.push(perm_to_litsym(&p));
348 }
349 }
350 for j in 0..m.saturating_sub(1) {
352 let mut p: Vec<usize> = (0..num_vars).collect();
353 for b in &blocks {
354 p[b[j]] = b[j + 1];
355 p[b[j + 1]] = b[j];
356 }
357 if bsgs.contains(&p) {
358 structured.push(perm_to_litsym(&p));
359 }
360 }
361 if structured.is_empty() {
362 return None;
363 }
364 Some(lex_leader_sbp_lit(num_vars, &structured))
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::cdcl::{SolveResult, Solver};
371 use std::collections::BTreeSet;
372
373 fn models(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<bool>> {
375 (0u64..(1u64 << num_vars))
376 .filter_map(|x| {
377 let a: Vec<bool> = (0..num_vars).map(|i| (x >> i) & 1 == 1).collect();
378 clauses
379 .iter()
380 .all(|c| c.iter().any(|l| a[l.var() as usize] == l.is_positive()))
381 .then_some(a)
382 })
383 .collect()
384 }
385
386 fn orbit_count(group: &[Perm], models: &[Vec<bool>]) -> usize {
388 let mut seen: BTreeSet<Vec<bool>> = BTreeSet::new();
389 let mut count = 0;
390 for m in models {
391 if seen.contains(m) {
392 continue;
393 }
394 count += 1;
395 for g in group {
396 seen.insert((0..m.len()).map(|j| m[g[j]]).collect());
397 }
398 }
399 count
400 }
401
402 fn count_projected_models(total_vars: usize, num_orig: usize, clauses: &[Vec<Lit>]) -> usize {
405 let mut seen: BTreeSet<Vec<bool>> = BTreeSet::new();
406 loop {
407 let mut s = Solver::new(total_vars);
408 for c in clauses {
409 s.add_clause(c.clone());
410 }
411 for proj in &seen {
412 s.add_clause((0..num_orig).map(|v| Lit::new(v as u32, !proj[v])).collect());
413 }
414 match s.solve() {
415 SolveResult::Sat(m) => {
416 seen.insert((0..num_orig).map(|v| m[v]).collect());
417 }
418 SolveResult::Unsat => break,
419 }
420 }
421 seen.len()
422 }
423
424 fn all_s_n(n: usize) -> Vec<Perm> {
425 let mut out = Vec::new();
426 let mut p: Perm = (0..n).collect();
427 loop {
428 out.push(p.clone());
429 let Some(i) = (0..n.saturating_sub(1)).rev().find(|&i| p[i] < p[i + 1]) else { break };
430 let j = (i + 1..n).rev().find(|&j| p[j] > p[i]).unwrap();
431 p.swap(i, j);
432 p[i + 1..].reverse();
433 }
434 out
435 }
436
437 #[test]
441 fn lex_leaders_are_one_per_orbit_under_s_n() {
442 for n in 2..=5usize {
443 let group = all_s_n(n);
444 let all: Vec<Vec<bool>> = (0u64..(1u64 << n)).map(|x| (0..n).map(|i| (x >> i) & 1 == 1).collect()).collect();
445 let leaders = all.iter().filter(|a| is_lex_leader(&group, a)).count();
446 assert_eq!(leaders, n + 1, "S_{n} on the cube has n+1 weight-orbits, one leader each");
447 assert_eq!(leaders, orbit_count(&group, &all), "leaders == orbit count");
448 }
449 }
450
451 #[test]
455 fn the_cnf_sbp_accepts_exactly_the_lex_leaders() {
456 for n in 2..=4usize {
457 let group = all_s_n(n);
458 let (sbp, total) = lex_leader_sbp(n, &group);
459 let semantic =
460 (0u64..(1u64 << n)).filter(|&x| is_lex_leader(&group, &(0..n).map(|i| (x >> i) & 1 == 1).collect::<Vec<_>>())).count();
461 assert_eq!(count_projected_models(total, n, &sbp), semantic, "CNF SBP accepts exactly the leaders");
462 assert_eq!(semantic, n + 1, "and there are n+1 of them");
463 }
464 }
465
466 #[test]
470 fn affine_lex_leader_encodes_the_lex_predicate() {
471 let n = 3usize;
472 let map = vec![(vec![0usize, 1], false), (vec![1], false), (vec![2, 0], false)];
474 let (sbp, total) = affine_lex_leader_sbp(n, &[map]);
475 let alpha = |x: u64| -> u64 {
476 let a0 = (x & 1) ^ ((x >> 1) & 1);
477 let a1 = (x >> 1) & 1;
478 let a2 = ((x >> 2) & 1) ^ (x & 1);
479 a0 | (a1 << 1) | (a2 << 2)
480 };
481 let lex_le = |x: u64, y: u64| -> bool {
482 for j in 0..n {
483 let (xj, yj) = ((x >> j) & 1, (y >> j) & 1);
484 if xj != yj {
485 return xj < yj;
486 }
487 }
488 true
489 };
490 for x in 0u64..(1 << n) {
491 let accepted = (0u64..(1u64 << (total - n))).any(|aux| {
492 let full = x | (aux << n);
493 sbp.iter().all(|c| c.iter().any(|l| ((full >> l.var()) & 1 == 1) == l.is_positive()))
494 });
495 assert_eq!(accepted, lex_le(x, alpha(x)), "SBP must accept x={x:03b} iff x ≤_lex α(x)={:03b}", alpha(x));
496 }
497 }
498
499 #[test]
504 fn partial_generator_breaking_is_sound_but_weaker_than_complete() {
505 let n = 3;
506 let full = all_s_n(n); let gens: Vec<Perm> = vec![vec![1, 0, 2], vec![1, 2, 0]]; let (complete, ct) = lex_leader_sbp(n, &full);
509 let (partial, pt) = lex_leader_sbp(n, &gens);
510 let complete_survivors = count_projected_models(ct, n, &complete);
511 let partial_survivors = count_projected_models(pt, n, &partial);
512 assert_eq!(complete_survivors, n + 1, "complete keeps exactly one per orbit (n+1 weight classes)");
513 assert!(
514 partial_survivors >= complete_survivors && partial_survivors <= (1 << n),
515 "partial keeps a superset (≥ complete, ≤ all): {partial_survivors} vs {complete_survivors}"
516 );
517 let f = vec![vec![Lit::pos(0), Lit::pos(1), Lit::pos(2)]];
519 let mut with_partial = f.clone();
520 with_partial.extend(partial);
521 assert!(count_projected_models(pt, n, &with_partial) >= 1, "partial preserves satisfiability");
522 }
523
524 #[test]
530 fn stabilizer_chain_break_is_between_complete_and_generators() {
531 let n = 4;
532 let gens: Vec<Perm> = vec![vec![1, 0, 2, 3], vec![1, 2, 3, 0]]; let bsgs = crate::permgroup::schreier_sims(n, &gens);
534 let complete = bsgs.elements(100_000).unwrap();
535 let mut chain = gens.clone();
536 chain.extend(bsgs.transversal_elements());
537 let survivors = |group: &[Perm]| {
538 let (sbp, t) = lex_leader_sbp(n, group);
539 count_projected_models(t, n, &sbp)
540 };
541 let (c, ch, g) = (survivors(&complete), survivors(&chain), survivors(&gens));
542 assert_eq!(c, n + 1, "complete keeps exactly one per orbit (n+1 weight classes)");
543 assert!(c <= ch && ch <= g, "complete ≤ stabilizer-chain ≤ generators: {c} ≤ {ch} ≤ {g}");
544 assert!(g <= (1usize << n), "all sound (≤ total assignments)");
545 }
546
547 #[test]
552 fn value_phase_symmetry_is_broken() {
553 let f = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::pos(1)]];
554 assert!(
555 variable_automorphism_generators(2, &f).is_none_or(|g| g.is_empty()),
556 "the phase-free variable scheme is blind to the value symmetry"
557 );
558 let gens = literal_automorphism_generators(2, &f);
559 assert!(!gens.is_empty(), "the value symmetry x₀ ↦ ¬x₀ is detected as a literal symmetry");
560 for s in &gens {
562 assert_eq!(litsym_from_points(&litsym_to_points(s, 2), 2), *s, "litsym ↔ points round-trips");
563 }
564 let (sbp, total) = lex_leader_sbp_lit(2, &gens);
565 let mut broken = f.clone();
566 broken.extend(sbp);
567 assert_eq!(count_projected_models(total, 2, &broken), 1, "value symmetry broken to one model");
568 assert_eq!(models(2, &f).len(), 2, "F has two models, one orbit under the flip");
570 }
571
572 #[test]
577 fn complete_lex_leader_keeps_one_model_per_orbit_end_to_end() {
578 let (cnf, _) = crate::families::clique_coloring(3, 3);
580 let group = variable_automorphism_group(cnf.num_vars, &cnf.clauses, 100_000)
581 .expect("clique colouring has a phase-free, small automorphism group");
582 let ms = models(cnf.num_vars, &cnf.clauses);
583 let orbits = orbit_count(&group, &ms);
584 let (sbp, total) = lex_leader_sbp(cnf.num_vars, &group);
585 let mut broken = cnf.clauses.clone();
586 broken.extend(sbp);
587 let surviving = count_projected_models(total, cnf.num_vars, &broken);
588 assert_eq!(surviving, orbits, "complete SBP leaves exactly one model per orbit");
589 assert!(orbits >= 1 && surviving < ms.len(), "and it strictly breaks the symmetry");
590
591 let (php, _) = crate::families::php(3);
593 let pg = variable_automorphism_group(php.num_vars, &php.clauses, 100_000).expect("PHP group");
594 let (psbp, ptotal) = lex_leader_sbp(php.num_vars, &pg);
595 let mut pbroken = php.clauses.clone();
596 pbroken.extend(psbp);
597 assert_eq!(count_projected_models(ptotal, php.num_vars, &pbroken), 0, "UNSAT stays UNSAT");
598 }
599
600 #[test]
605 fn conditional_symmetry_emerges_under_a_partial_assignment() {
606 let f = vec![
607 vec![Lit::neg(0), Lit::pos(1), Lit::pos(2)], vec![Lit::neg(0), Lit::neg(1), Lit::neg(2)], vec![Lit::pos(0), Lit::pos(1)], ];
611 let swaps_12 = |gens: &[Vec<Lit>]| {
612 gens.iter().any(|img| img[1] == Lit::pos(2) && img[2] == Lit::pos(1))
613 };
614 assert!(!swaps_12(&literal_automorphism_generators(3, &f)), "F has no global x1↔x2 symmetry");
616 let local = conditional_symmetry_generators(3, &f, &[Lit::pos(0)]);
618 assert!(swaps_12(&local), "the residual under x0=true has the x1↔x2 symmetry: {local:?}");
619 }
620
621 #[test]
626 fn hierarchical_block_wise_breaking_is_sound_and_polynomial() {
627 let (cnf, _) = crate::families::clique_coloring(3, 3);
628 let nv = cnf.num_vars;
629 let (sbp, total) = hierarchical_break(nv, &cnf.clauses).expect("clique colouring is a grid symmetry");
630 let ms = models(nv, &cnf.clauses);
631 let var_group = variable_automorphism_group(nv, &cnf.clauses, 100_000).unwrap();
632 let orbits = orbit_count(&var_group, &ms);
633 let mut broken = cnf.clauses.clone();
634 broken.extend(sbp);
635 let surviving = count_projected_models(total, nv, &broken);
636 assert!(surviving >= orbits, "hierarchical break is sound: ≥ one model per orbit ({surviving} ≥ {orbits})");
637 assert!(surviving < ms.len(), "and it breaks the symmetry: fewer survivors than the {} models", ms.len());
638 }
639
640 #[test]
645 fn php_symmetry_is_an_imprimitive_grid() {
646 let n = 4;
647 let (cnf, _) = crate::families::php(n);
648 let gens = variable_automorphism_generators(cnf.num_vars, &cnf.clauses).expect("phase-free");
649 assert!(
650 !crate::permgroup::is_primitive(cnf.num_vars, &gens),
651 "PHP's symmetry is imprimitive — it is a grid, not an atom"
652 );
653 let blocks =
654 crate::permgroup::minimal_block_system(cnf.num_vars, &gens).expect("PHP has a block system");
655 assert!(blocks.iter().all(|b| b.len() == n - 1), "blocks are pigeon-rows of size n-1: {blocks:?}");
656 assert_eq!(blocks.len(), n, "there are n pigeon-rows");
657 for b in &blocks {
658 let pigeon = b[0] / (n - 1);
659 assert!(b.iter().all(|&v| v / (n - 1) == pigeon), "each block is exactly one pigeon's row: {b:?}");
660 }
661 }
662
663 #[test]
668 fn count_modulo_symmetry_equals_burnside_and_brute_orbit_count() {
669 let (cnf, _) = crate::families::clique_coloring(3, 3);
670 let nv = cnf.num_vars;
671 let group = literal_automorphism_group(nv, &cnf.clauses, 100_000).unwrap();
672 let ms = models(nv, &cnf.clauses);
673 let image = |img: &[Lit], a: &[bool]| -> Vec<bool> {
674 (0..a.len())
675 .map(|j| if img[j].is_positive() { a[img[j].var() as usize] } else { !a[img[j].var() as usize] })
676 .collect()
677 };
678 let brute = {
680 let mut seen: BTreeSet<Vec<bool>> = BTreeSet::new();
681 let mut c = 0;
682 for m in &ms {
683 if seen.contains(m) {
684 continue;
685 }
686 c += 1;
687 for s in &group {
688 seen.insert(image(s, m));
689 }
690 }
691 c
692 };
693 let fixed: usize = group.iter().map(|s| ms.iter().filter(|&m| image(s, m) == *m).count()).sum();
695 let burnside = fixed / group.len();
696 let counted = count_models_modulo_symmetry(nv, &cnf.clauses).unwrap();
697 assert_eq!(counted, brute, "complete-SBP count == brute orbit count");
698 assert_eq!(counted, burnside, "== Burnside count");
699 assert_eq!(counted, 1, "clique_coloring(3,3): all six proper colourings are one orbit");
700 assert_eq!(ms.len(), 6, "and there are six of them");
701 }
702}