1use std::collections::{BTreeMap, BTreeSet, HashMap};
19
20pub type Perm = Vec<usize>;
22
23fn identity(n: usize) -> Perm {
24 (0..n).collect()
25}
26fn is_identity(p: &[usize]) -> bool {
27 p.iter().enumerate().all(|(i, &v)| i == v)
28}
29fn compose(g: &[usize], h: &[usize]) -> Perm {
31 g.iter().map(|&x| h[x]).collect()
32}
33fn invert(g: &[usize]) -> Perm {
34 let mut inv = vec![0usize; g.len()];
35 for (x, &gx) in g.iter().enumerate() {
36 inv[gx] = x;
37 }
38 inv
39}
40
41fn orbit_transversal(base: &[usize], strong: &[Perm], level: usize) -> HashMap<usize, Perm> {
44 let degree = strong.first().map(|p| p.len()).unwrap_or(base.len());
45 let stab: Vec<&Perm> =
46 strong.iter().filter(|g| (0..level).all(|j| g[base[j]] == base[j])).collect();
47 let mut trans: HashMap<usize, Perm> = HashMap::new();
48 trans.insert(base[level], identity(degree));
49 let mut queue = vec![base[level]];
50 while let Some(p) = queue.pop() {
51 let up = trans[&p].clone();
52 for s in &stab {
53 let q = s[p];
54 if !trans.contains_key(&q) {
55 trans.insert(q, compose(&up, s)); queue.push(q);
57 }
58 }
59 }
60 trans
61}
62
63fn sift(base: &[usize], strong: &[Perm], mut g: Perm) -> (Perm, usize) {
67 for (i, &beta) in base.iter().enumerate() {
68 let trans = orbit_transversal(base, strong, i);
69 let img = g[beta];
70 match trans.get(&img) {
71 None => return (g, i),
72 Some(t) => g = compose(&g, &invert(t)), }
74 }
75 (g, base.len())
76}
77
78fn extend_with(base: &mut Vec<usize>, strong: &mut Vec<Perm>, g: Perm) -> bool {
81 let (res, lvl) = sift(base, strong, g);
82 if is_identity(&res) {
83 return false;
84 }
85 if lvl == base.len() {
86 let moved = (0..res.len()).find(|&x| res[x] != x).expect("a non-identity moves a point");
87 base.push(moved);
88 }
89 strong.push(res);
90 true
91}
92
93pub fn orbits(degree: usize, generators: &[Perm]) -> Vec<Vec<usize>> {
96 let mut seen = vec![false; degree];
97 let mut out = Vec::new();
98 for start in 0..degree {
99 if seen[start] {
100 continue;
101 }
102 seen[start] = true;
103 let mut orbit = vec![start];
104 let mut i = 0;
105 while i < orbit.len() {
106 let p = orbit[i];
107 i += 1;
108 for g in generators {
109 let q = g[p];
110 if !seen[q] {
111 seen[q] = true;
112 orbit.push(q);
113 }
114 }
115 }
116 orbit.sort_unstable();
117 out.push(orbit);
118 }
119 out
120}
121
122fn uf_find(parent: &mut [usize], mut x: usize) -> usize {
123 while parent[x] != x {
124 parent[x] = parent[parent[x]];
125 x = parent[x];
126 }
127 x
128}
129
130fn block_containing(degree: usize, gens: &[Perm], alpha: usize, beta: usize) -> Vec<usize> {
135 let mut parent: Vec<usize> = (0..degree).collect();
136 let mut queue: Vec<(usize, usize)> = Vec::new();
137 let (ra, rb) = (uf_find(&mut parent, alpha), uf_find(&mut parent, beta));
138 if ra != rb {
139 parent[ra] = rb;
140 queue.push((alpha, beta));
141 }
142 while let Some((x, y)) = queue.pop() {
143 for g in gens {
144 let (gx, gy) = (g[x], g[y]);
145 let (rx, ry) = (uf_find(&mut parent, gx), uf_find(&mut parent, gy));
146 if rx != ry {
147 parent[rx] = ry;
148 queue.push((gx, gy));
149 }
150 }
151 }
152 (0..degree).map(|x| uf_find(&mut parent, x)).collect()
153}
154
155pub fn minimal_block_system(degree: usize, gens: &[Perm]) -> Option<Vec<Vec<usize>>> {
161 if degree < 2 || orbits(degree, gens).len() != 1 {
162 return None; }
164 let mut best: Option<Vec<usize>> = None;
165 let mut best_size = degree;
166 for beta in 1..degree {
167 let ids = block_containing(degree, gens, 0, beta);
168 let size = ids.iter().filter(|&&b| b == ids[0]).count();
169 if 1 < size && size < degree && size < best_size {
170 best_size = size;
171 best = Some(ids);
172 }
173 }
174 best.map(|ids| {
175 let mut by_block: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
176 for (x, &b) in ids.iter().enumerate() {
177 by_block.entry(b).or_default().push(x);
178 }
179 by_block.into_values().collect()
180 })
181}
182
183pub fn is_primitive(degree: usize, gens: &[Perm]) -> bool {
187 degree >= 2 && orbits(degree, gens).len() == 1 && minimal_block_system(degree, gens).is_none()
188}
189
190pub fn orbitals(degree: usize, gens: &[Perm]) -> Vec<Vec<(usize, usize)>> {
195 let mut seen = vec![false; degree * degree];
196 let idx = |i: usize, j: usize| i * degree + j;
197 let mut out = Vec::new();
198 for i in 0..degree {
199 for j in 0..degree {
200 if seen[idx(i, j)] {
201 continue;
202 }
203 seen[idx(i, j)] = true;
204 let mut orbit = vec![(i, j)];
205 let mut k = 0;
206 while k < orbit.len() {
207 let (a, b) = orbit[k];
208 k += 1;
209 for g in gens {
210 let (ga, gb) = (g[a], g[b]);
211 if !seen[idx(ga, gb)] {
212 seen[idx(ga, gb)] = true;
213 orbit.push((ga, gb));
214 }
215 }
216 }
217 out.push(orbit);
218 }
219 }
220 out
221}
222
223pub fn rank(degree: usize, gens: &[Perm]) -> usize {
226 orbitals(degree, gens).len()
227}
228
229fn orbital_graph_connected(degree: usize, orbital: &[(usize, usize)]) -> bool {
231 let mut parent: Vec<usize> = (0..degree).collect();
232 for &(i, j) in orbital {
233 let (ri, rj) = (uf_find(&mut parent, i), uf_find(&mut parent, j));
234 parent[ri] = rj;
235 }
236 let r0 = uf_find(&mut parent, 0);
237 (0..degree).all(|v| uf_find(&mut parent, v) == r0)
238}
239
240pub fn is_primitive_via_orbitals(degree: usize, gens: &[Perm]) -> bool {
244 if degree < 2 || orbits(degree, gens).len() != 1 {
245 return false;
246 }
247 orbitals(degree, gens)
248 .iter()
249 .filter(|orb| orb.iter().any(|&(i, j)| i != j)) .all(|orb| orbital_graph_connected(degree, orb))
251}
252
253fn distinct_tuples(degree: usize, k: usize) -> Vec<Vec<usize>> {
255 let mut out = Vec::new();
256 let mut cur = Vec::with_capacity(k);
257 let mut used = vec![false; degree];
258 fn rec(degree: usize, k: usize, cur: &mut Vec<usize>, used: &mut [bool], out: &mut Vec<Vec<usize>>) {
259 if cur.len() == k {
260 out.push(cur.clone());
261 return;
262 }
263 for x in 0..degree {
264 if !used[x] {
265 used[x] = true;
266 cur.push(x);
267 rec(degree, k, cur, used, out);
268 cur.pop();
269 used[x] = false;
270 }
271 }
272 }
273 rec(degree, k, &mut cur, &mut used, &mut out);
274 out
275}
276
277pub fn orbits_on_tuples(degree: usize, gens: &[Perm], k: usize) -> Vec<Vec<Vec<usize>>> {
281 if k == 0 || k > degree {
282 return Vec::new();
283 }
284 let tuples = distinct_tuples(degree, k);
285 let index: HashMap<Vec<usize>, usize> =
286 tuples.iter().enumerate().map(|(i, t)| (t.clone(), i)).collect();
287 let mut seen = vec![false; tuples.len()];
288 let mut out = Vec::new();
289 for start in 0..tuples.len() {
290 if seen[start] {
291 continue;
292 }
293 seen[start] = true;
294 let mut orbit = vec![tuples[start].clone()];
295 let mut i = 0;
296 while i < orbit.len() {
297 let cur = orbit[i].clone();
298 i += 1;
299 for g in gens {
300 let img: Vec<usize> = cur.iter().map(|&x| g[x]).collect();
301 let idx = index[&img];
302 if !seen[idx] {
303 seen[idx] = true;
304 orbit.push(img);
305 }
306 }
307 }
308 out.push(orbit);
309 }
310 out
311}
312
313pub fn transitivity_degree(degree: usize, gens: &[Perm], max_t: usize) -> usize {
318 let mut t = 0;
319 for k in 1..=max_t.min(degree) {
320 if orbits_on_tuples(degree, gens, k).len() == 1 {
321 t = k;
322 } else {
323 break; }
325 }
326 t
327}
328
329fn commutator(g: &[usize], h: &[usize]) -> Perm {
331 compose(&compose(&invert(g), &invert(h)), &compose(g, h))
332}
333
334fn normal_closure(degree: usize, sub: &[Perm], gens: &[Perm]) -> Vec<Perm> {
338 let mut closure: Vec<Perm> = sub.iter().filter(|p| !is_identity(p)).cloned().collect();
339 if closure.is_empty() {
340 return closure;
341 }
342 let mut bsgs = schreier_sims(degree, &closure);
343 let mut i = 0;
344 while i < closure.len() {
345 let s = closure[i].clone();
346 i += 1;
347 for g in gens {
348 for conj in [compose(&compose(&invert(g), &s), g), compose(&compose(g, &s), &invert(g))] {
349 if !is_identity(&conj) && !bsgs.contains(&conj) {
350 closure.push(conj);
351 bsgs = schreier_sims(degree, &closure);
352 }
353 }
354 }
355 }
356 closure
357}
358
359fn commutator_subgroup(degree: usize, gens_a: &[Perm], gens_b: &[Perm]) -> Vec<Perm> {
363 let mut comms = Vec::new();
364 for a in gens_a {
365 for b in gens_b {
366 let c = commutator(a, b);
367 if !is_identity(&c) {
368 comms.push(c);
369 }
370 }
371 }
372 normal_closure(degree, &comms, gens_a)
373}
374
375pub fn derived_subgroup(degree: usize, gens: &[Perm]) -> Vec<Perm> {
379 commutator_subgroup(degree, gens, gens)
380}
381
382pub fn is_nilpotent(degree: usize, gens: &[Perm]) -> bool {
385 nilpotency_class(degree, gens).is_some()
386}
387
388pub fn is_abelian(_degree: usize, gens: &[Perm]) -> bool {
390 gens.iter().all(|g| gens.iter().all(|h| compose(g, h) == compose(h, g)))
391}
392
393pub fn derived_length(degree: usize, gens: &[Perm]) -> Option<usize> {
397 let mut cur: Vec<Perm> = gens.to_vec();
398 let mut len = 0;
399 loop {
400 let order = schreier_sims(degree, &cur).order();
401 if order == 1 {
402 return Some(len);
403 }
404 let d = derived_subgroup(degree, &cur);
405 if schreier_sims(degree, &d).order() == order {
406 return None; }
408 cur = d;
409 len += 1;
410 }
411}
412
413pub fn is_solvable(degree: usize, gens: &[Perm]) -> bool {
415 derived_length(degree, gens).is_some()
416}
417
418pub fn nilpotency_class(degree: usize, gens: &[Perm]) -> Option<usize> {
422 let mut gamma: Vec<Perm> = gens.to_vec();
423 let mut class = 0;
424 loop {
425 let order = schreier_sims(degree, &gamma).order();
426 if order == 1 {
427 return Some(class);
428 }
429 let next = commutator_subgroup(degree, gens, &gamma);
430 if schreier_sims(degree, &next).order() == order {
431 return None; }
433 gamma = next;
434 class += 1;
435 }
436}
437
438pub fn conjugacy_classes(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<Vec<Perm>>> {
443 let elements = schreier_sims(degree, gens).elements(cap)?;
444 let mut remaining: BTreeSet<Perm> = elements.iter().cloned().collect();
445 let mut classes = Vec::new();
446 while let Some(g) = remaining.iter().next().cloned() {
447 let mut class: BTreeSet<Perm> = BTreeSet::new();
448 for x in &elements {
449 class.insert(compose(&compose(&invert(x), &g), x)); }
451 for c in &class {
452 remaining.remove(c);
453 }
454 classes.push(class.into_iter().collect::<Vec<_>>());
455 }
456 Some(classes)
457}
458
459pub fn center_order(degree: usize, gens: &[Perm], cap: usize) -> Option<u128> {
462 conjugacy_classes(degree, gens, cap)
463 .map(|classes| classes.iter().filter(|c| c.len() == 1).count() as u128)
464}
465
466fn element_order(g: &[usize]) -> usize {
468 if is_identity(g) {
469 return 1;
470 }
471 let mut p = compose(g, g);
472 let mut k = 2;
473 while !is_identity(&p) {
474 p = compose(&p, g);
475 k += 1;
476 }
477 k
478}
479
480fn gcd(mut a: u128, mut b: u128) -> u128 {
481 while b != 0 {
482 (a, b) = (b, a % b);
483 }
484 a
485}
486
487fn lcm(a: u128, b: u128) -> u128 {
488 if a == 0 || b == 0 {
489 0
490 } else {
491 a / gcd(a, b) * b
492 }
493}
494
495pub fn element_orders(degree: usize, gens: &[Perm], cap: usize) -> Option<BTreeSet<usize>> {
497 let elements = schreier_sims(degree, gens).elements(cap)?;
498 Some(elements.iter().map(|g| element_order(g)).collect())
499}
500
501pub fn exponent(degree: usize, gens: &[Perm], cap: usize) -> Option<u128> {
504 let elements = schreier_sims(degree, gens).elements(cap)?;
505 Some(elements.iter().fold(1u128, |e, g| lcm(e, element_order(g) as u128)))
506}
507
508pub fn upper_central_series(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<u128>> {
512 let elements = schreier_sims(degree, gens).elements(cap)?;
513 let mut z: BTreeSet<Perm> = BTreeSet::from([identity(degree)]);
514 let mut orders = vec![1u128];
515 loop {
516 let next: BTreeSet<Perm> = elements
517 .iter()
518 .filter(|g| elements.iter().all(|x| z.contains(&commutator(g, x))))
519 .cloned()
520 .collect();
521 if next.len() == z.len() {
522 break; }
524 orders.push(next.len() as u128);
525 z = next;
526 }
527 Some(orders)
528}
529
530pub fn upper_central_length(degree: usize, gens: &[Perm], cap: usize) -> Option<usize> {
534 let orders = upper_central_series(degree, gens, cap)?;
535 (orders.last() == Some(&schreier_sims(degree, gens).order())).then_some(orders.len() - 1)
536}
537
538fn cycle_type(g: &[usize]) -> Vec<usize> {
541 let mut seen = vec![false; g.len()];
542 let mut lengths = Vec::new();
543 for start in 0..g.len() {
544 if seen[start] {
545 continue;
546 }
547 let mut len = 0;
548 let mut x = start;
549 while !seen[x] {
550 seen[x] = true;
551 x = g[x];
552 len += 1;
553 }
554 lengths.push(len);
555 }
556 lengths.sort_unstable();
557 lengths
558}
559
560pub fn cycle_index(degree: usize, gens: &[Perm], cap: usize) -> Option<BTreeMap<Vec<usize>, u128>> {
564 let elements = schreier_sims(degree, gens).elements(cap)?;
565 let mut dist: BTreeMap<Vec<usize>, u128> = BTreeMap::new();
566 for g in &elements {
567 *dist.entry(cycle_type(g)).or_insert(0) += 1;
568 }
569 Some(dist)
570}
571
572pub fn polya_count(degree: usize, gens: &[Perm], m: usize, cap: usize) -> Option<u128> {
577 let elements = schreier_sims(degree, gens).elements(cap)?;
578 let order = elements.len() as u128;
579 let total: u128 = elements.iter().map(|g| (m as u128).pow(cycle_type(g).len() as u32)).sum();
580 Some(total / order)
581}
582
583pub fn pattern_inventory(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<u128>> {
588 let elements = schreier_sims(degree, gens).elements(cap)?;
589 let order = elements.len() as u128;
590 let mut total = vec![0u128; degree + 1];
591 for g in &elements {
592 let mut poly = vec![0u128; degree + 1];
594 poly[0] = 1;
595 for &len in &cycle_type(g) {
596 let prev = poly.clone();
597 for i in 0..=degree {
598 poly[i] = prev[i] + if i >= len { prev[i - len] } else { 0 };
599 }
600 }
601 for i in 0..=degree {
602 total[i] += poly[i];
603 }
604 }
605 Some(total.iter().map(|&c| c / order).collect())
606}
607
608pub fn abelianization(degree: usize, gens: &[Perm], cap: usize) -> Option<(u128, u128)> {
612 let elements = schreier_sims(degree, gens).elements(cap)?;
613 let derived: BTreeSet<Perm> =
614 schreier_sims(degree, &derived_subgroup(degree, gens)).elements(cap)?.into_iter().collect();
615 let coset_rep = |g: &Perm| -> Perm { derived.iter().map(|x| compose(g, x)).min().unwrap() };
617 let cosets: BTreeSet<Perm> = elements.iter().map(|g| coset_rep(g)).collect();
618 let order = cosets.len() as u128;
619 let coset_order = |r: &Perm| -> usize {
621 let mut p = r.clone();
622 let mut k = 1;
623 while !derived.contains(&p) {
624 p = compose(&p, r);
625 k += 1;
626 }
627 k
628 };
629 let exponent = cosets.iter().fold(1u128, |e, r| lcm(e, coset_order(r) as u128));
630 Some((order, exponent))
631}
632
633fn subgroup_closure(degree: usize, seed: &BTreeSet<Perm>) -> BTreeSet<Perm> {
635 let mut set = seed.clone();
636 set.insert(identity(degree));
637 loop {
638 let snapshot: Vec<Perm> = set.iter().cloned().collect();
639 let before = set.len();
640 for a in &snapshot {
641 for b in &snapshot {
642 set.insert(compose(a, b));
643 }
644 }
645 if set.len() == before {
646 break;
647 }
648 }
649 set
650}
651
652fn all_subgroups(degree: usize, gens: &[Perm], cap: usize) -> Option<BTreeSet<BTreeSet<Perm>>> {
657 let elements = schreier_sims(degree, gens).elements(cap)?;
658 let trivial: BTreeSet<Perm> = BTreeSet::from([identity(degree)]);
659 let mut subgroups: BTreeSet<BTreeSet<Perm>> = BTreeSet::from([trivial.clone()]);
660 let mut queue = vec![trivial];
661 while let Some(h) = queue.pop() {
662 for g in &elements {
663 if h.contains(g) {
664 continue;
665 }
666 let mut seed = h.clone();
667 seed.insert(g.clone());
668 let sub = subgroup_closure(degree, &seed);
669 if subgroups.insert(sub.clone()) {
670 queue.push(sub);
671 }
672 }
673 }
674 Some(subgroups)
675}
676
677pub fn subgroup_count(degree: usize, gens: &[Perm], cap: usize) -> Option<usize> {
678 all_subgroups(degree, gens, cap).map(|s| s.len())
679}
680
681fn is_normal_set(h: &BTreeSet<Perm>, gens: &[Perm]) -> bool {
683 gens.iter().all(|g| h.iter().all(|x| h.contains(&compose(&compose(&invert(g), x), g))))
684}
685
686fn maximal_normal_subgroup(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<Perm>> {
689 let order = schreier_sims(degree, gens).order();
690 let subgroups = all_subgroups(degree, gens, cap)?;
691 subgroups
692 .iter()
693 .filter(|h| (h.len() as u128) < order && is_normal_set(h, gens))
694 .max_by_key(|h| h.len())
695 .map(|h| h.iter().cloned().collect())
696}
697
698pub fn composition_factor_orders(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<u128>> {
703 let order = schreier_sims(degree, gens).order();
704 if order == 1 {
705 return Some(Vec::new());
706 }
707 if is_simple(degree, gens, cap)? {
708 return Some(vec![order]);
709 }
710 let n = maximal_normal_subgroup(degree, gens, cap)?;
711 let n_order = schreier_sims(degree, &n).order();
712 let mut factors = composition_factor_orders(degree, &n, cap)?;
713 factors.push(order / n_order); factors.sort_unstable();
715 Some(factors)
716}
717
718fn distinct_primes(mut n: u128) -> Vec<u128> {
720 let mut primes = Vec::new();
721 let mut p = 2u128;
722 while p * p <= n {
723 if n % p == 0 {
724 primes.push(p);
725 while n % p == 0 {
726 n /= p;
727 }
728 }
729 p += 1;
730 }
731 if n > 1 {
732 primes.push(n);
733 }
734 primes
735}
736
737fn prime_power_part(n: u128, p: u128) -> u128 {
739 let mut pa = 1;
740 let mut m = n;
741 while m % p == 0 {
742 pa *= p;
743 m /= p;
744 }
745 pa
746}
747
748pub fn sylow_counts(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<(u128, usize)>> {
753 let order = schreier_sims(degree, gens).order();
754 let subgroups = all_subgroups(degree, gens, cap)?;
755 Some(
756 distinct_primes(order)
757 .into_iter()
758 .map(|p| {
759 let pa = prime_power_part(order, p);
760 let n_p = subgroups.iter().filter(|h| h.len() as u128 == pa).count();
761 (p, n_p)
762 })
763 .collect(),
764 )
765}
766
767fn class_index_map(classes: &[Vec<Perm>]) -> BTreeMap<Perm, usize> {
769 let mut idx = BTreeMap::new();
770 for (i, class) in classes.iter().enumerate() {
771 for g in class {
772 idx.insert(g.clone(), i);
773 }
774 }
775 idx
776}
777
778pub fn class_multiplication_coefficients(
784 degree: usize,
785 gens: &[Perm],
786 cap: usize,
787) -> Option<Vec<Vec<Vec<u128>>>> {
788 let classes = conjugacy_classes(degree, gens, cap)?;
789 let idx = class_index_map(&classes);
790 let k = classes.len();
791 let mut a = vec![vec![vec![0u128; k]; k]; k];
792 for (kk, class_k) in classes.iter().enumerate() {
793 let z = &class_k[0];
794 for (i, class_i) in classes.iter().enumerate() {
795 for x in class_i {
796 let j = idx[&compose(&invert(x), z)]; a[i][j][kk] += 1;
798 }
799 }
800 }
801 Some(a)
802}
803
804pub fn real_class_count(degree: usize, gens: &[Perm], cap: usize) -> Option<usize> {
807 let classes = conjugacy_classes(degree, gens, cap)?;
808 let idx = class_index_map(&classes);
809 Some(classes.iter().enumerate().filter(|(i, c)| idx[&invert(&c[0])] == *i).count())
810}
811
812fn perm_pow(g: &[usize], mut t: usize) -> Perm {
814 let mut result = identity(g.len());
815 let mut base = g.to_vec();
816 while t > 0 {
817 if t & 1 == 1 {
818 result = compose(&result, &base);
819 }
820 base = compose(&base, &base);
821 t >>= 1;
822 }
823 result
824}
825
826pub fn galois_class_orbits(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<Vec<usize>>> {
831 let classes = conjugacy_classes(degree, gens, cap)?;
832 let idx = class_index_map(&classes);
833 let e = exponent(degree, gens, cap)? as usize;
834 let k = classes.len();
835 let mut parent: Vec<usize> = (0..k).collect();
836 fn find(parent: &mut [usize], mut x: usize) -> usize {
837 while parent[x] != x {
838 parent[x] = parent[parent[x]];
839 x = parent[x];
840 }
841 x
842 }
843 for t in 1..e.max(2) {
844 if gcd(t as u128, e as u128) != 1 {
845 continue;
846 }
847 for r in 0..k {
848 let img = idx[&perm_pow(&classes[r][0], t)];
849 let (a, b) = (find(&mut parent, r), find(&mut parent, img));
850 parent[a] = b;
851 }
852 }
853 let mut groups: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
854 for r in 0..k {
855 let root = find(&mut parent, r);
856 groups.entry(root).or_default().push(r);
857 }
858 Some(groups.into_values().collect())
859}
860
861pub fn rational_class_count(degree: usize, gens: &[Perm], cap: usize) -> Option<usize> {
868 Some(galois_class_orbits(degree, gens, cap)?.iter().filter(|o| o.len() == 1).count())
869}
870
871pub fn automorphism_group_order(degree: usize, gens: &[Perm], cap: usize) -> Option<u128> {
878 let seed: BTreeSet<Perm> = gens.iter().cloned().collect();
879 let elements: Vec<Perm> = subgroup_closure(degree, &seed).into_iter().collect();
880 let n = elements.len();
881 if n > cap {
882 return None;
883 }
884 let idx: BTreeMap<Perm, usize> = elements.iter().enumerate().map(|(i, e)| (e.clone(), i)).collect();
885 let id_idx = idx[&identity(degree)];
886 let mul: Vec<Vec<usize>> =
887 (0..n).map(|i| (0..n).map(|j| idx[&compose(&elements[i], &elements[j])]).collect()).collect();
888 let ord: Vec<usize> = elements.iter().map(|e| element_order(e)).collect();
889 let mut gen_idx: Vec<usize> = Vec::new();
891 for g in gens {
892 let gi = idx[g];
893 if gi != id_idx && !gen_idx.contains(&gi) {
894 gen_idx.push(gi);
895 }
896 }
897 if gen_idx.is_empty() {
898 return Some(1); }
900 let candidates: Vec<Vec<usize>> =
902 gen_idx.iter().map(|&gi| (0..n).filter(|&e| ord[e] == ord[gi]).collect::<Vec<_>>()).collect();
903 if candidates.iter().map(|c| c.len() as u128).product::<u128>() > 2_000_000 {
904 return None;
905 }
906 let m = gen_idx.len();
907 let mut count = 0u128;
908 let mut choice = vec![0usize; m];
909 loop {
910 let img: Vec<usize> = (0..m).map(|t| candidates[t][choice[t]]).collect();
911 let mut phi = vec![usize::MAX; n];
913 phi[id_idx] = id_idx;
914 let mut queue = vec![id_idx];
915 let mut head = 0;
916 let mut ok = true;
917 'bfs: while head < queue.len() {
918 let u = queue[head];
919 head += 1;
920 for t in 0..m {
921 let ug = mul[u][gen_idx[t]];
922 let target = mul[phi[u]][img[t]];
923 if phi[ug] == usize::MAX {
924 phi[ug] = target;
925 queue.push(ug);
926 } else if phi[ug] != target {
927 ok = false;
928 break 'bfs;
929 }
930 }
931 }
932 if ok && phi.iter().all(|&x| x != usize::MAX) {
934 let mut seen = vec![false; n];
935 if phi.iter().all(|&x| !std::mem::replace(&mut seen[x], true)) {
936 count += 1;
937 }
938 }
939 let mut t = 0;
940 while t < m {
941 choice[t] += 1;
942 if choice[t] < candidates[t].len() {
943 break;
944 }
945 choice[t] = 0;
946 t += 1;
947 }
948 if t == m {
949 break;
950 }
951 }
952 Some(count)
953}
954
955pub fn outer_automorphism_order(degree: usize, gens: &[Perm], cap: usize) -> Option<u128> {
959 let aut = automorphism_group_order(degree, gens, cap)?;
960 let order = schreier_sims(degree, gens).order();
961 let center = center_order(degree, gens, cap)?;
962 Some(aut / (order / center)) }
964
965pub fn table_of_marks(degree: usize, gens: &[Perm], cap: usize) -> Option<(Vec<u128>, Vec<Vec<u128>>)> {
976 let elements: Vec<Perm> =
977 subgroup_closure(degree, &gens.iter().cloned().collect()).into_iter().collect();
978 let subs = all_subgroups(degree, gens, cap)?;
979 let conjugate = |h: &BTreeSet<Perm>, g: &Perm| -> BTreeSet<Perm> {
981 let gi = invert(g);
982 h.iter().map(|x| compose(&compose(&gi, x), g)).collect()
983 };
984 let mut reps: Vec<BTreeSet<Perm>> = Vec::new();
986 let mut seen: BTreeSet<BTreeSet<Perm>> = BTreeSet::new();
987 for h in &subs {
988 if seen.contains(h) {
989 continue;
990 }
991 for g in &elements {
992 seen.insert(conjugate(h, g));
993 }
994 reps.push(h.clone());
995 }
996 reps.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
998 let k = reps.len();
999 let orders: Vec<u128> = reps.iter().map(|h| h.len() as u128).collect();
1000 let mut marks = vec![vec![0u128; k]; k];
1001 for i in 0..k {
1002 for j in 0..k {
1003 if reps[i].len() > reps[j].len() {
1004 continue; }
1006 let count = elements
1007 .iter()
1008 .filter(|g| conjugate(&reps[i], g).iter().all(|x| reps[j].contains(x)))
1009 .count() as u128;
1010 marks[i][j] = count / reps[j].len() as u128;
1011 }
1012 }
1013 Some((orders, marks))
1014}
1015
1016pub fn burnside_ring_product(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<Vec<Vec<i128>>>> {
1027 let (_orders, marks) = table_of_marks(degree, gens, cap)?;
1028 let k = marks.len();
1029 let solve = |p: &[u128]| -> Option<Vec<i128>> {
1032 let mut c = vec![0i128; k];
1033 for i in (0..k).rev() {
1034 let mut acc = p[i] as i128;
1035 for l in (i + 1)..k {
1036 acc -= marks[i][l] as i128 * c[l];
1037 }
1038 let diag = marks[i][i] as i128;
1039 if diag == 0 || acc % diag != 0 {
1040 return None; }
1042 c[i] = acc / diag;
1043 }
1044 Some(c)
1045 };
1046 let mut n = vec![vec![vec![0i128; k]; k]; k];
1047 for a in 0..k {
1048 for b in 0..k {
1049 let p: Vec<u128> = (0..k).map(|i| marks[i][a] * marks[i][b]).collect();
1050 let c = solve(&p)?;
1051 for l in 0..k {
1052 n[a][b][l] = c[l];
1053 }
1054 }
1055 }
1056 Some(n)
1057}
1058
1059fn lattice_mobius_to_top(degree: usize, gens: &[Perm], cap: usize) -> Option<(Vec<u128>, Vec<i128>)> {
1063 let subs: Vec<BTreeSet<Perm>> = all_subgroups(degree, gens, cap)?.into_iter().collect();
1064 let n = subs.len();
1065 let top_size = subs.iter().map(|h| h.len()).max().unwrap_or(0); let mut order: Vec<usize> = (0..n).collect();
1067 order.sort_by_key(|&i| std::cmp::Reverse(subs[i].len())); let mut mu = vec![0i128; n];
1069 for &i in &order {
1070 if subs[i].len() == top_size {
1071 mu[i] = 1; } else {
1073 let s: i128 = (0..n)
1074 .filter(|&j| subs[i].len() < subs[j].len() && subs[i].is_subset(&subs[j]))
1075 .map(|j| mu[j])
1076 .sum();
1077 mu[i] = -s;
1078 }
1079 }
1080 Some((subs.iter().map(|h| h.len() as u128).collect(), mu))
1081}
1082
1083pub fn mobius_number(degree: usize, gens: &[Perm], cap: usize) -> Option<i128> {
1088 let (orders, mu) = lattice_mobius_to_top(degree, gens, cap)?;
1089 (0..orders.len()).find(|&i| orders[i] == 1).map(|i| mu[i])
1090}
1091
1092pub fn generating_tuple_count(degree: usize, gens: &[Perm], cap: usize, k: u32) -> Option<i128> {
1097 let (orders, mu) = lattice_mobius_to_top(degree, gens, cap)?;
1098 Some((0..orders.len()).map(|i| mu[i] * (orders[i] as i128).pow(k)).sum())
1099}
1100
1101fn subgroup_class_reps(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<BTreeSet<Perm>>> {
1104 let elements: Vec<Perm> =
1105 subgroup_closure(degree, &gens.iter().cloned().collect()).into_iter().collect();
1106 let subs = all_subgroups(degree, gens, cap)?;
1107 let conjugate = |h: &BTreeSet<Perm>, g: &Perm| -> BTreeSet<Perm> {
1108 let gi = invert(g);
1109 h.iter().map(|x| compose(&compose(&gi, x), g)).collect()
1110 };
1111 let mut reps: Vec<BTreeSet<Perm>> = Vec::new();
1112 let mut seen: BTreeSet<BTreeSet<Perm>> = BTreeSet::new();
1113 for h in &subs {
1114 if seen.contains(h) {
1115 continue;
1116 }
1117 for g in &elements {
1118 seen.insert(conjugate(h, g));
1119 }
1120 reps.push(h.clone());
1121 }
1122 reps.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
1123 Some(reps)
1124}
1125
1126pub fn permutation_character_decomposition(
1139 degree: usize,
1140 gens: &[Perm],
1141 cap: usize,
1142) -> Option<(Vec<u128>, Vec<u128>, Vec<Vec<u128>>)> {
1143 let ct = character_table(degree, gens, cap)?;
1144 let p = ct.prime;
1145 let k = ct.degrees.len();
1146 let classes = conjugacy_classes(degree, gens, cap)?;
1147 let idx = class_index_map(&classes);
1148 let reps = subgroup_class_reps(degree, gens, cap)?;
1149 let order: u128 = ct.degrees.iter().map(|d| d * d).sum();
1150
1151 let mut m = vec![vec![0u128; k]; reps.len()];
1152 for (i, h) in reps.iter().enumerate() {
1153 let mut inter = vec![0u128; k];
1155 for x in h {
1156 inter[idx[x]] += 1;
1157 }
1158 let inv_h = mod_inv((h.len() as u128 % p as u128) as u64, p);
1159 for s in 0..k {
1160 let mut acc = 0u64;
1161 for r in 0..k {
1162 let term = (inter[r] % p as u128) as u64;
1163 acc = ((acc as u128 + term as u128 * ct.values[s][r] as u128) % p as u128) as u64;
1164 }
1165 m[i][s] = (acc as u128 * inv_h as u128 % p as u128) as u128;
1166 }
1167 }
1168
1169 let trivial_irr = ct.values.iter().position(|row| row.iter().all(|&x| x == 1))?;
1171 for (i, h) in reps.iter().enumerate() {
1172 if m[i][trivial_irr] != 1 {
1173 return None; }
1175 let dim: u128 = (0..k).map(|s| m[i][s] * ct.degrees[s]).sum();
1176 if dim != order / h.len() as u128 {
1177 return None; }
1179 for s in 0..k {
1180 if m[i][s] > ct.degrees[s] {
1181 return None; }
1183 }
1184 }
1185 if m[0] != ct.degrees {
1186 return None; }
1188 let mut e_triv = vec![0u128; k];
1189 e_triv[trivial_irr] = 1;
1190 if *m.last()? != e_triv {
1191 return None; }
1193 let orders: Vec<u128> = reps.iter().map(|h| h.len() as u128).collect();
1194 Some((orders, ct.degrees, m))
1195}
1196
1197fn mod_pow(mut base: u64, mut exp: u64, p: u64) -> u64 {
1209 let mut r = 1u128;
1210 let mut b = (base % p) as u128;
1211 base = 0; let _ = base;
1213 while exp > 0 {
1214 if exp & 1 == 1 {
1215 r = (r * b) % p as u128;
1216 }
1217 b = (b * b) % p as u128;
1218 exp >>= 1;
1219 }
1220 r as u64
1221}
1222
1223pub(crate) fn mod_inv(a: u64, p: u64) -> u64 {
1225 mod_pow(a % p, p - 2, p)
1226}
1227
1228pub(crate) fn is_prime(n: u64) -> bool {
1230 if n < 2 {
1231 return false;
1232 }
1233 if n % 2 == 0 {
1234 return n == 2;
1235 }
1236 let mut d = 3u64;
1237 while d * d <= n {
1238 if n % d == 0 {
1239 return false;
1240 }
1241 d += 2;
1242 }
1243 true
1244}
1245
1246fn isqrt(n: u128) -> u128 {
1248 if n < 2 {
1249 return n;
1250 }
1251 let mut x = (n as f64).sqrt() as u128;
1252 while x * x > n {
1253 x -= 1;
1254 }
1255 while (x + 1) * (x + 1) <= n {
1256 x += 1;
1257 }
1258 x
1259}
1260
1261pub(crate) fn gf_mat_vec(m: &[Vec<u64>], v: &[u64], p: u64) -> Vec<u64> {
1263 m.iter()
1264 .map(|row| {
1265 let mut acc = 0u128;
1266 for (a, b) in row.iter().zip(v) {
1267 acc += (*a as u128) * (*b as u128);
1268 }
1269 (acc % p as u128) as u64
1270 })
1271 .collect()
1272}
1273
1274pub(crate) fn gf_nullspace(mut a: Vec<Vec<u64>>, ncols: usize, p: u64) -> Vec<Vec<u64>> {
1276 let nrows = a.len();
1277 let mut where_pivot = vec![usize::MAX; ncols]; let mut row = 0usize;
1279 for col in 0..ncols {
1280 if row >= nrows {
1281 break;
1282 }
1283 let Some(sel) = (row..nrows).find(|&r| a[r][col] % p != 0) else { continue };
1284 a.swap(row, sel);
1285 let inv = mod_inv(a[row][col], p);
1286 for c in 0..ncols {
1287 a[row][c] = ((a[row][c] as u128 * inv as u128) % p as u128) as u64;
1288 }
1289 for r in 0..nrows {
1290 if r != row && a[r][col] != 0 {
1291 let f = a[r][col] as u128;
1292 for c in 0..ncols {
1293 let sub = (f * a[row][c] as u128) % p as u128;
1294 a[r][c] = ((a[r][c] as u128 + p as u128 - sub) % p as u128) as u64;
1295 }
1296 }
1297 }
1298 where_pivot[col] = row;
1299 row += 1;
1300 }
1301 let mut basis = Vec::new();
1302 for fc in 0..ncols {
1303 if where_pivot[fc] != usize::MAX {
1304 continue; }
1306 let mut x = vec![0u64; ncols];
1307 x[fc] = 1;
1308 for (col, &pr) in where_pivot.iter().enumerate() {
1309 if pr != usize::MAX {
1310 x[col] = (p - a[pr][fc] % p) % p; }
1312 }
1313 basis.push(x);
1314 }
1315 basis
1316}
1317
1318#[derive(Clone, Debug, PartialEq, Eq)]
1320pub struct CharacterTable {
1321 pub prime: u64,
1323 pub class_sizes: Vec<u128>,
1325 pub inverse_class: Vec<usize>,
1327 pub power_map2: Vec<usize>,
1330 pub identity_class: usize,
1332 pub class_reps: Vec<Perm>,
1335 pub degrees: Vec<u128>,
1337 pub values: Vec<Vec<u64>>,
1339}
1340
1341pub fn character_table(degree: usize, gens: &[Perm], cap: usize) -> Option<CharacterTable> {
1350 let classes = conjugacy_classes(degree, gens, cap)?;
1351 let k = classes.len();
1352 if k == 0 || k > 64 {
1354 return None;
1355 }
1356 let order = schreier_sims(degree, gens).order();
1357 if order == 0 || order > 100_000 {
1358 return None;
1359 }
1360 let idx = class_index_map(&classes);
1361 let class_sizes: Vec<u128> = classes.iter().map(|c| c.len() as u128).collect();
1362 let inverse_class: Vec<usize> = classes.iter().map(|c| idx[&invert(&c[0])]).collect();
1363 let power_map2: Vec<usize> = classes.iter().map(|c| idx[&compose(&c[0], &c[0])]).collect();
1364 let class_reps: Vec<Perm> = classes.iter().map(|c| c[0].clone()).collect();
1365 let id_perm = identity(degree);
1366 let identity_class = classes.iter().position(|c| c.iter().any(|g| *g == id_perm))?;
1367
1368 let e = exponent(degree, gens, cap)? as u64;
1371 let order_u = order as u64;
1372 let p = {
1373 let mut m = order_u / e + 1;
1374 let mut found = None;
1375 for _ in 0..1_000_000 {
1376 let cand = e.checked_mul(m)?.checked_add(1)?;
1377 if cand > order_u && is_prime(cand) {
1378 found = Some(cand);
1379 break;
1380 }
1381 m += 1;
1382 }
1383 found?
1384 };
1385
1386 let a = class_multiplication_coefficients(degree, gens, cap)?;
1388 let mmats: Vec<Vec<Vec<u64>>> = (0..k)
1389 .map(|i| {
1390 let mut m = vec![vec![0u64; k]; k];
1391 for j in 0..k {
1392 for kk in 0..k {
1393 m[kk][j] = (a[i][j][kk] % p as u128) as u64;
1394 }
1395 }
1396 m
1397 })
1398 .collect();
1399
1400 let mut subspaces: Vec<Vec<Vec<u64>>> = vec![(0..k)
1402 .map(|i| {
1403 let mut ei = vec![0u64; k];
1404 ei[i] = 1;
1405 ei
1406 })
1407 .collect()];
1408 for mi in &mmats {
1409 if subspaces.iter().all(|s| s.len() == 1) {
1410 break;
1411 }
1412 let mut next: Vec<Vec<Vec<u64>>> = Vec::new();
1413 for s in &subspaces {
1414 if s.len() == 1 {
1415 next.push(s.clone());
1416 continue;
1417 }
1418 let bn = s.len();
1419 let mb: Vec<Vec<u64>> = s.iter().map(|b| gf_mat_vec(mi, b, p)).collect();
1420 let mut pieces: Vec<Vec<Vec<u64>>> = Vec::new();
1421 let mut covered = 0usize;
1422 for lam in 0..p {
1423 let mut rows = vec![vec![0u64; bn]; k];
1425 for r in 0..k {
1426 for (j, bj) in s.iter().enumerate() {
1427 let shifted = (lam as u128 * bj[r] as u128) % p as u128;
1428 rows[r][j] = ((mb[j][r] as u128 + p as u128 - shifted) % p as u128) as u64;
1429 }
1430 }
1431 let ns = gf_nullspace(rows, bn, p);
1432 if ns.is_empty() {
1433 continue;
1434 }
1435 let eig: Vec<Vec<u64>> = ns
1436 .iter()
1437 .map(|c| {
1438 let mut x = vec![0u64; k];
1439 for (j, &cj) in c.iter().enumerate() {
1440 if cj != 0 {
1441 for r in 0..k {
1442 x[r] = ((x[r] as u128 + cj as u128 * s[j][r] as u128) % p as u128) as u64;
1443 }
1444 }
1445 }
1446 x
1447 })
1448 .collect();
1449 covered += eig.len();
1450 pieces.push(eig);
1451 if covered == bn {
1452 break;
1453 }
1454 }
1455 if covered == bn {
1456 next.extend(pieces);
1457 } else {
1458 next.push(s.clone()); }
1460 }
1461 subspaces = next;
1462 }
1463 if subspaces.iter().any(|s| s.len() != 1) {
1464 return None; }
1466
1467 let order_p = (order % p as u128) as u64;
1469 let max_deg = isqrt(order);
1470 let mut rows: Vec<(u128, Vec<u64>)> = Vec::with_capacity(k);
1471 for s in &subspaces {
1472 let v = &s[0];
1473 let t = v.iter().position(|&x| x != 0)?;
1474 let inv_vt = mod_inv(v[t], p);
1475 let omega: Vec<u64> = mmats
1476 .iter()
1477 .map(|mi| {
1478 let mv = gf_mat_vec(mi, v, p);
1479 ((mv[t] as u128 * inv_vt as u128) % p as u128) as u64
1480 })
1481 .collect();
1482 let mut denom = 0u64;
1484 for r in 0..k {
1485 let hr = (class_sizes[r] % p as u128) as u64;
1486 let term = (omega[r] as u128 * omega[inverse_class[r]] as u128 % p as u128) as u64;
1487 let contrib = (term as u128 * mod_inv(hr, p) as u128 % p as u128) as u64;
1488 denom = (denom + contrib) % p;
1489 }
1490 if denom == 0 {
1491 return None;
1492 }
1493 let d2 = (order_p as u128 * mod_inv(denom, p) as u128 % p as u128) as u64;
1494 let mut deg = None;
1496 let mut d = 1u128;
1497 while d <= max_deg {
1498 if order % d == 0 && ((d * d) % p as u128) as u64 == d2 {
1499 deg = Some(d);
1500 break;
1501 }
1502 d += 1;
1503 }
1504 let deg = deg?;
1505 let vals: Vec<u64> = (0..k)
1506 .map(|r| {
1507 let hr = (class_sizes[r] % p as u128) as u64;
1508 let num = (deg % p as u128) as u64 as u128 * omega[r] as u128 % p as u128;
1509 (num * mod_inv(hr, p) as u128 % p as u128) as u64
1510 })
1511 .collect();
1512 rows.push((deg, vals));
1513 }
1514 rows.sort();
1515 let degrees: Vec<u128> = rows.iter().map(|(d, _)| *d).collect();
1516 let values: Vec<Vec<u64>> = rows.into_iter().map(|(_, v)| v).collect();
1517
1518 if degrees.iter().map(|d| d * d).sum::<u128>() != order {
1520 return None;
1521 }
1522 if !values.iter().any(|row| row.iter().all(|&x| x == 1)) {
1523 return None; }
1525 for s in 0..k {
1526 for t in 0..k {
1527 let mut acc = 0u64;
1528 for r in 0..k {
1529 let hr = (class_sizes[r] % p as u128) as u64;
1530 let prod = values[s][r] as u128 * values[t][inverse_class[r]] as u128 % p as u128;
1531 acc = ((acc as u128 + hr as u128 * prod) % p as u128) as u64;
1532 }
1533 let want = if s == t { order_p } else { 0 };
1534 if acc != want {
1535 return None;
1536 }
1537 }
1538 }
1539
1540 Some(CharacterTable {
1541 prime: p,
1542 class_sizes,
1543 inverse_class,
1544 power_map2,
1545 identity_class,
1546 class_reps,
1547 degrees,
1548 values,
1549 })
1550}
1551
1552pub fn frobenius_schur_from_table(t: &CharacterTable) -> Option<Vec<i8>> {
1560 let p = t.prime;
1561 let order: u128 = t.degrees.iter().map(|d| d * d).sum();
1562 let inv_order = mod_inv((order % p as u128) as u64, p);
1563 let k = t.degrees.len();
1564 let involutions_plus_id: u128 = (0..k)
1566 .filter(|&r| t.power_map2[r] == t.identity_class)
1567 .map(|r| t.class_sizes[r])
1568 .sum();
1569 let mut nu = Vec::with_capacity(k);
1570 for s in 0..k {
1571 let mut acc = 0u64;
1572 for r in 0..k {
1573 let hr = (t.class_sizes[r] % p as u128) as u64;
1574 let chi_sq = t.values[s][t.power_map2[r]];
1575 acc = ((acc as u128 + hr as u128 * chi_sq as u128) % p as u128) as u64;
1576 }
1577 let val = ((acc as u128 * inv_order as u128) % p as u128) as u64;
1578 let ind: i8 = if val == 0 {
1579 0
1580 } else if val == 1 {
1581 1
1582 } else if val == p - 1 {
1583 -1
1584 } else {
1585 return None; };
1587 nu.push(ind);
1588 }
1589 let sum: i128 = nu.iter().zip(&t.degrees).map(|(&v, &d)| v as i128 * d as i128).sum();
1591 if sum != involutions_plus_id as i128 {
1592 return None;
1593 }
1594 Some(nu)
1595}
1596
1597pub fn frobenius_schur_indicators(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<i8>> {
1601 frobenius_schur_from_table(&character_table(degree, gens, cap)?)
1602}
1603
1604pub fn permutation_character(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<u128>> {
1609 let classes = conjugacy_classes(degree, gens, cap)?;
1610 Some(
1611 classes
1612 .iter()
1613 .map(|c| c[0].iter().enumerate().filter(|(i, &x)| *i == x).count() as u128)
1614 .collect(),
1615 )
1616}
1617
1618pub fn isotypic_from_table(degree: usize, gens: &[Perm], t: &CharacterTable) -> Option<Vec<u128>> {
1626 let p = t.prime;
1627 if p as u128 <= degree as u128 {
1628 return None; }
1630 let order: u128 = t.degrees.iter().map(|d| d * d).sum();
1631 let inv_order = mod_inv((order % p as u128) as u64, p);
1632 let k = t.degrees.len();
1633 let pi: Vec<u64> = t
1635 .class_reps
1636 .iter()
1637 .map(|g| (g.iter().enumerate().filter(|(i, &x)| *i == x).count() as u64) % p)
1638 .collect();
1639 let mut mult = Vec::with_capacity(k);
1640 for s in 0..k {
1641 let mut acc = 0u64;
1642 for r in 0..k {
1643 let hr = (t.class_sizes[r] % p as u128) as u64;
1644 let term = (hr as u128 * pi[r] as u128 % p as u128) as u64;
1645 let contrib = (term as u128 * t.values[s][t.inverse_class[r]] as u128) % p as u128;
1646 acc = ((acc as u128 + contrib) % p as u128) as u64;
1647 }
1648 let m = ((acc as u128 * inv_order as u128) % p as u128) as u128;
1649 mult.push(m);
1650 }
1651 if mult.iter().zip(&t.degrees).map(|(m, d)| m * d).sum::<u128>() != degree as u128 {
1653 return None; }
1655 if mult.iter().map(|m| m * m).sum::<u128>() != rank(degree, gens) as u128 {
1656 return None; }
1658 let num_orbits = orbits(degree, gens).len() as u128;
1659 let trivial_row = t.values.iter().position(|row| row.iter().all(|&x| x == 1))?;
1660 if mult[trivial_row] != num_orbits {
1661 return None; }
1663 Some(mult)
1664}
1665
1666pub fn isotypic_multiplicities(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<u128>> {
1670 isotypic_from_table(degree, gens, &character_table(degree, gens, cap)?)
1671}
1672
1673pub fn tensor_from_table(t: &CharacterTable) -> Option<Vec<Vec<Vec<u128>>>> {
1683 let p = t.prime;
1684 let order: u128 = t.degrees.iter().map(|d| d * d).sum();
1685 let inv_order = mod_inv((order % p as u128) as u64, p);
1686 let k = t.degrees.len();
1687 let mut n = vec![vec![vec![0u128; k]; k]; k];
1688 for i in 0..k {
1689 for j in 0..k {
1690 for kk in 0..k {
1691 let mut acc = 0u64;
1692 for r in 0..k {
1693 let hr = (t.class_sizes[r] % p as u128) as u64;
1694 let mut prod = hr as u128;
1695 prod = prod * t.values[i][r] as u128 % p as u128;
1696 prod = prod * t.values[j][r] as u128 % p as u128;
1697 prod = prod * t.values[kk][t.inverse_class[r]] as u128 % p as u128;
1698 acc = ((acc as u128 + prod) % p as u128) as u64;
1699 }
1700 n[i][j][kk] = (acc as u128 * inv_order as u128 % p as u128) as u128;
1701 }
1702 }
1703 }
1704 let trivial = t.values.iter().position(|row| row.iter().all(|&x| x == 1))?;
1706 for i in 0..k {
1707 for j in 0..k {
1708 if (0..k).map(|kk| n[i][j][kk] * t.degrees[kk]).sum::<u128>() != t.degrees[i] * t.degrees[j] {
1710 return None;
1711 }
1712 for kk in 0..k {
1714 if n[i][j][kk] != n[j][i][kk] {
1715 return None;
1716 }
1717 }
1718 }
1719 for kk in 0..k {
1721 let expect = u128::from(kk == i);
1722 if n[trivial][i][kk] != expect {
1723 return None;
1724 }
1725 }
1726 }
1727 Some(n)
1728}
1729
1730pub fn tensor_decomposition(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<Vec<Vec<u128>>>> {
1733 tensor_from_table(&character_table(degree, gens, cap)?)
1734}
1735
1736pub fn irreducible_degrees(degree: usize, gens: &[Perm], cap: usize) -> Option<Vec<u128>> {
1740 character_table(degree, gens, cap).map(|t| t.degrees)
1741}
1742
1743pub fn is_simple(degree: usize, gens: &[Perm], cap: usize) -> Option<bool> {
1749 let order = schreier_sims(degree, gens).order();
1750 if order <= 1 {
1751 return Some(false); }
1753 let classes = conjugacy_classes(degree, gens, cap)?;
1754 for class in &classes {
1755 let rep = &class[0];
1756 if is_identity(rep) {
1757 continue;
1758 }
1759 let ncl = normal_closure(degree, std::slice::from_ref(rep), gens);
1760 if schreier_sims(degree, &ncl).order() < order {
1761 return Some(false); }
1763 }
1764 Some(true)
1765}
1766
1767#[derive(Clone, Debug)]
1769pub struct Bsgs {
1770 pub degree: usize,
1771 pub base: Vec<usize>,
1772 transversals: Vec<HashMap<usize, Perm>>,
1773}
1774
1775impl Bsgs {
1776 pub fn order(&self) -> u128 {
1778 self.transversals.iter().map(|t| t.len() as u128).product()
1779 }
1780
1781 pub fn elements(&self, cap: usize) -> Option<Vec<Perm>> {
1787 if self.order() > cap as u128 {
1788 return None;
1789 }
1790 let mut elems = vec![identity(self.degree)];
1791 for trans in self.transversals.iter().rev() {
1792 let reps: Vec<&Perm> = trans.values().collect();
1793 let mut next = Vec::with_capacity(elems.len() * reps.len());
1794 for e in &elems {
1795 for r in &reps {
1796 next.push(compose(e, r));
1797 }
1798 }
1799 elems = next;
1800 }
1801 Some(elems)
1802 }
1803
1804 pub fn transversal_elements(&self) -> Vec<Perm> {
1810 self.transversals.iter().flat_map(|t| t.values().cloned()).collect()
1811 }
1812
1813 pub fn contains(&self, g: &[usize]) -> bool {
1816 if g.len() != self.degree {
1817 return false;
1818 }
1819 let mut g = g.to_vec();
1820 for (i, &beta) in self.base.iter().enumerate() {
1821 let img = g[beta];
1822 match self.transversals[i].get(&img) {
1823 None => return false,
1824 Some(t) => g = compose(&g, &invert(t)),
1825 }
1826 }
1827 is_identity(&g)
1828 }
1829}
1830
1831pub fn schreier_sims(degree: usize, generators: &[Perm]) -> Bsgs {
1838 let mut base: Vec<usize> = Vec::new();
1839 let mut strong: Vec<Perm> = Vec::new();
1840 for g in generators {
1841 if !is_identity(g) {
1842 extend_with(&mut base, &mut strong, g.clone());
1843 }
1844 }
1845 loop {
1846 let mut changed = false;
1847 'scan: for i in 0..base.len() {
1848 let trans = orbit_transversal(&base, &strong, i);
1849 let stab: Vec<Perm> =
1850 strong.iter().filter(|g| (0..i).all(|j| g[base[j]] == base[j])).cloned().collect();
1851 for u in trans.values() {
1852 for s in &stab {
1853 let us = compose(u, s);
1854 let img = us[base[i]];
1855 let schreier = compose(&us, &invert(&trans[&img])); if !is_identity(&schreier) && extend_with(&mut base, &mut strong, schreier) {
1857 changed = true;
1858 break 'scan; }
1860 }
1861 }
1862 }
1863 if !changed {
1864 break;
1865 }
1866 }
1867 let transversals = (0..base.len()).map(|i| orbit_transversal(&base, &strong, i)).collect();
1868 Bsgs { degree, base, transversals }
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873 use super::*;
1874 use std::collections::BTreeSet;
1875
1876 fn closure(degree: usize, gens: &[Perm]) -> BTreeSet<Perm> {
1878 let mut set: BTreeSet<Perm> = BTreeSet::new();
1879 set.insert(identity(degree));
1880 for g in gens {
1881 set.insert(g.clone());
1882 }
1883 loop {
1884 let before = set.len();
1885 for a in set.iter().cloned().collect::<Vec<_>>() {
1886 for g in gens {
1887 set.insert(compose(&a, g));
1888 }
1889 }
1890 if set.len() == before {
1891 break;
1892 }
1893 }
1894 set
1895 }
1896
1897 fn all_perms(n: usize) -> Vec<Perm> {
1899 let mut out = Vec::new();
1900 let mut p: Perm = (0..n).collect();
1901 loop {
1902 out.push(p.clone());
1903 let Some(i) = (0..n.saturating_sub(1)).rev().find(|&i| p[i] < p[i + 1]) else { break };
1905 let j = (i + 1..n).rev().find(|&j| p[j] > p[i]).unwrap();
1906 p.swap(i, j);
1907 p[i + 1..].reverse();
1908 }
1909 out
1910 }
1911
1912 fn splitmix(s: &mut u64) -> u64 {
1913 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
1914 let mut z = *s;
1915 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1916 z ^ (z >> 31)
1917 }
1918
1919 fn random_perm(n: usize, state: &mut u64) -> Perm {
1920 let mut p: Perm = (0..n).collect();
1921 for i in (1..n).rev() {
1922 let j = (splitmix(state) % (i as u64 + 1)) as usize;
1923 p.swap(i, j);
1924 }
1925 p
1926 }
1927
1928 #[test]
1931 fn schreier_sims_reproduces_textbook_group_orders() {
1932 let fact = |n: u128| (1..=n).product::<u128>();
1933 for n in 2..=7usize {
1935 let transposition: Perm = {
1936 let mut p = identity(n);
1937 p.swap(0, 1);
1938 p
1939 };
1940 let cycle: Perm = (0..n).map(|i| (i + 1) % n).collect();
1941 assert_eq!(schreier_sims(n, &[transposition, cycle]).order(), fact(n as u128), "|S_{n}| = n!");
1942 }
1943 for n in 3..=7usize {
1945 let mut gens = Vec::new();
1946 for k in 2..n {
1947 let mut p = identity(n);
1949 p[0] = 1;
1950 p[1] = k;
1951 p[k] = 0;
1952 gens.push(p);
1953 }
1954 assert_eq!(schreier_sims(n, &gens).order(), fact(n as u128) / 2, "|A_{n}| = n!/2");
1955 }
1956 for n in 1..=12usize {
1958 let cycle: Perm = (0..n).map(|i| (i + 1) % n).collect();
1959 assert_eq!(schreier_sims(n, &[cycle]).order(), n as u128, "|C_{n}| = n");
1960 }
1961 for n in 3..=10usize {
1963 let rot: Perm = (0..n).map(|i| (i + 1) % n).collect();
1964 let refl: Perm = (0..n).map(|i| (n - i) % n).collect();
1965 assert_eq!(schreier_sims(n, &[rot, refl]).order(), 2 * n as u128, "|D_{n}| = 2n");
1966 }
1967 assert_eq!(schreier_sims(5, &[]).order(), 1, "the empty generating set gives the trivial group");
1969 }
1970
1971 #[test]
1976 fn order_and_membership_match_brute_force_exhaustively() {
1977 let mut state = 0xC0FF_EE42u64;
1978 for _ in 0..120 {
1979 let degree = 3 + (splitmix(&mut state) % 4) as usize; let ngens = 1 + (splitmix(&mut state) % 3) as usize; let gens: Vec<Perm> = (0..ngens).map(|_| random_perm(degree, &mut state)).collect();
1982 let group = closure(degree, &gens);
1983 let bsgs = schreier_sims(degree, &gens);
1984 assert_eq!(
1985 bsgs.order(),
1986 group.len() as u128,
1987 "|G| must equal the brute-force closure size; gens = {gens:?}"
1988 );
1989 for p in all_perms(degree) {
1990 assert_eq!(
1991 bsgs.contains(&p),
1992 group.contains(&p),
1993 "membership must match brute force for {p:?}; gens = {gens:?}"
1994 );
1995 }
1996 }
1997 }
1998
1999 #[test]
2002 fn coset_membership_decides_non_abelian_cosets() {
2003 let mut state = 0x5EED_0A5Eu64;
2004 let degree = 5;
2005 let cycle: Perm = (0..degree).map(|i| (i + 1) % degree).collect();
2007 let transposition: Perm = {
2008 let mut p = identity(degree);
2009 p.swap(0, 1);
2010 p
2011 };
2012 let sub_cycle: Perm = vec![0, 2, 3, 4, 1];
2014 let sub_swap: Perm = vec![0, 2, 1, 3, 4];
2015 let _ = (&cycle, &transposition);
2016 let group = closure(degree, &[sub_cycle.clone(), sub_swap.clone()]);
2017 let bsgs = schreier_sims(degree, &[sub_cycle, sub_swap]);
2018 assert_eq!(bsgs.order(), group.len() as u128, "the S_4 subgroup order");
2019 for _ in 0..200 {
2020 let rep = random_perm(degree, &mut state);
2021 let g = random_perm(degree, &mut state);
2022 let in_coset = bsgs.contains(&compose(&invert(&rep), &g));
2024 let brute = group.contains(&compose(&invert(&rep), &g));
2025 assert_eq!(in_coset, brute, "coset decision must match brute force: rep={rep:?} g={g:?}");
2026 }
2027 }
2028
2029 #[test]
2032 fn elements_enumerates_the_whole_group() {
2033 let mut state = 0xE1E_0F00Du64;
2034 for _ in 0..40 {
2035 let degree = 3 + (splitmix(&mut state) % 3) as usize; let ngens = 1 + (splitmix(&mut state) % 3) as usize;
2037 let gens: Vec<Perm> = (0..ngens).map(|_| random_perm(degree, &mut state)).collect();
2038 let group = closure(degree, &gens);
2039 let bsgs = schreier_sims(degree, &gens);
2040 let elems = bsgs.elements(100_000).expect("small group enumerates");
2041 let as_set: BTreeSet<Perm> = elems.iter().cloned().collect();
2042 assert_eq!(elems.len(), as_set.len(), "enumeration has no duplicates");
2043 assert_eq!(as_set, group, "enumeration equals the brute-force closure; gens={gens:?}");
2044 assert!(elems.iter().all(|g| bsgs.contains(g)), "every enumerated element is a member");
2045 }
2046 let n = 8;
2048 let cycle: Perm = (0..n).map(|i| (i + 1) % n).collect();
2049 let swap: Perm = {
2050 let mut p = identity(n);
2051 p.swap(0, 1);
2052 p
2053 };
2054 assert!(schreier_sims(n, &[cycle, swap]).elements(1000).is_none(), "|S_8|=40320 > cap ⟹ None");
2055 }
2056
2057 #[test]
2061 fn transversal_elements_are_polynomial_members() {
2062 let mut state = 0x7AB_5E70u64;
2063 for _ in 0..30 {
2064 let degree = 3 + (splitmix(&mut state) % 4) as usize;
2065 let ngens = 1 + (splitmix(&mut state) % 3) as usize;
2066 let gens: Vec<Perm> = (0..ngens).map(|_| random_perm(degree, &mut state)).collect();
2067 let bsgs = schreier_sims(degree, &gens);
2068 let reps = bsgs.transversal_elements();
2069 assert!(reps.iter().all(|g| bsgs.contains(g)), "every transversal element is a member");
2070 assert!(reps.len() <= degree * degree, "polynomial count (≤ degree²): {}", reps.len());
2071 assert!(reps.len() as u128 >= bsgs.base.len() as u128, "at least one rep per base level");
2073 }
2074 }
2075
2076 #[test]
2079 fn orbits_match_the_group_action() {
2080 let cycle: Perm = vec![1, 2, 3, 0]; let swap: Perm = vec![1, 0, 2, 3]; assert_eq!(orbits(4, &[cycle, swap]), vec![vec![0, 1, 2, 3]], "S_4 is transitive");
2083 let three: Perm = vec![0, 2, 3, 1]; assert_eq!(orbits(4, &[three]), vec![vec![0], vec![1, 2, 3]], "stabilizer of 0");
2085 assert_eq!(orbits(3, &[]), vec![vec![0], vec![1], vec![2]], "trivial group: all singletons");
2086 }
2087
2088 #[test]
2092 fn block_systems_detect_primitivity_and_imprimitivity() {
2093 let s4: Vec<Perm> = vec![vec![1, 0, 2, 3], vec![1, 2, 3, 0]]; assert!(is_primitive(4, &s4), "S_4 natural action is primitive");
2095 assert!(minimal_block_system(4, &s4).is_none());
2096
2097 let c5: Vec<Perm> = vec![vec![1, 2, 3, 4, 0]];
2098 assert!(is_primitive(5, &c5), "C_5 is primitive (5 is prime)");
2099
2100 let c6: Vec<Perm> = vec![vec![1, 2, 3, 4, 5, 0]];
2101 assert!(!is_primitive(6, &c6), "C_6 is imprimitive");
2102 let bs = minimal_block_system(6, &c6).expect("C_6 has a non-trivial block system");
2103 assert!(bs.iter().all(|b| b.len() == 2), "C_6 minimal blocks have size 2: {bs:?}");
2104 assert_eq!(bs.len(), 3, "three blocks");
2105 assert_eq!(bs.iter().map(|b| b.len()).sum::<usize>(), 6, "the blocks partition all points");
2106 }
2107
2108 #[test]
2109 fn orbitals_give_the_rank_and_higmans_primitivity() {
2110 let s_n = |n: usize| -> Vec<Perm> {
2112 (0..n - 1)
2113 .map(|i| {
2114 let mut p: Perm = (0..n).collect();
2115 p.swap(i, i + 1);
2116 p
2117 })
2118 .collect()
2119 };
2120 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2121
2122 let cases: Vec<(usize, Vec<Perm>, usize, bool)> = vec![
2124 (3, s_n(3), 2, true), (4, s_n(4), 2, true), (4, c_n(4), 4, false), (5, c_n(5), 5, true), (6, c_n(6), 6, false), ];
2130
2131 for (deg, gens, want_rank, want_prim) in cases {
2132 assert_eq!(rank(deg, &gens), want_rank, "rank of the group on {deg} points");
2133 let diag = orbitals(deg, &gens).into_iter().filter(|o| o.iter().all(|&(i, j)| i == j)).count();
2135 assert_eq!(diag, 1, "the diagonal is a single orbital");
2136 assert_eq!(is_primitive_via_orbitals(deg, &gens), want_prim, "Higman primitivity");
2138 assert_eq!(
2139 is_primitive_via_orbitals(deg, &gens),
2140 is_primitive(deg, &gens),
2141 "orbital (Higman) and block-system primitivity must agree"
2142 );
2143 }
2144 }
2145
2146 #[test]
2147 fn transitivity_ladder_climbs_the_tuple_orbits() {
2148 let s_n = |n: usize| -> Vec<Perm> {
2149 (0..n - 1)
2150 .map(|i| {
2151 let mut p: Perm = (0..n).collect();
2152 p.swap(i, i + 1);
2153 p
2154 })
2155 .collect()
2156 };
2157 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2158
2159 assert_eq!(transitivity_degree(4, &s_n(4), 3), 3, "S₄ is ≥3-transitive");
2161 assert_eq!(transitivity_degree(3, &s_n(3), 5), 3, "S₃ is exactly 3-transitive on 3 points");
2162 assert_eq!(transitivity_degree(4, &c_n(4), 3), 1, "C₄ is 1-transitive only");
2164 assert_eq!(transitivity_degree(5, &c_n(5), 3), 1, "C₅ is 1-transitive only");
2165
2166 assert_eq!(orbits_on_tuples(4, &s_n(4), 1).len(), orbits(4, &s_n(4)).len());
2168 assert_eq!(orbits_on_tuples(4, &s_n(4), 2).len(), 1, "S₄ is 2-transitive");
2170 assert!(orbits_on_tuples(4, &c_n(4), 2).len() > 1, "C₄ is not 2-transitive");
2171 }
2172
2173 #[test]
2174 fn derived_series_decides_solvability() {
2175 let s_n = |n: usize| -> Vec<Perm> {
2176 (0..n - 1)
2177 .map(|i| {
2178 let mut p: Perm = (0..n).collect();
2179 p.swap(i, i + 1);
2180 p
2181 })
2182 .collect()
2183 };
2184 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2185 let order = |deg: usize, g: &[Perm]| schreier_sims(deg, g).order();
2186
2187 assert!(is_abelian(4, &c_n(4)), "C₄ is abelian");
2189 assert!(is_solvable(4, &c_n(4)));
2190 assert_eq!(order(4, &derived_subgroup(4, &c_n(4))), 1, "[C₄,C₄] is trivial");
2191
2192 assert!(!is_abelian(3, &s_n(3)));
2194 assert_eq!(order(3, &derived_subgroup(3, &s_n(3))), 3, "[S₃,S₃] = A₃ (order 3)");
2195 assert!(is_solvable(3, &s_n(3)), "S₃ is solvable");
2196
2197 assert_eq!(order(4, &derived_subgroup(4, &s_n(4))), 12, "[S₄,S₄] = A₄ (order 12)");
2198 assert!(is_solvable(4, &s_n(4)), "S₄ is solvable");
2199
2200 assert_eq!(order(5, &derived_subgroup(5, &s_n(5))), 60, "[S₅,S₅] = A₅ (order 60)");
2201 assert!(!is_solvable(5, &s_n(5)), "S₅ is NOT solvable — A₅ is perfect");
2202 }
2203
2204 #[test]
2205 fn conjugacy_classes_partition_the_group_and_find_the_centre() {
2206 let s_n = |n: usize| -> Vec<Perm> {
2207 (0..n - 1)
2208 .map(|i| {
2209 let mut p: Perm = (0..n).collect();
2210 p.swap(i, i + 1);
2211 p
2212 })
2213 .collect()
2214 };
2215 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2216
2217 let s3 = conjugacy_classes(3, &s_n(3), 1000).expect("S₃ is enumerable");
2219 assert_eq!(s3.len(), 3, "S₃ has 3 conjugacy classes (= 3 irreps)");
2220 assert_eq!(s3.iter().map(|c| c.len()).sum::<usize>(), 6, "the classes partition S₃");
2221 assert_eq!(center_order(3, &s_n(3), 1000), Some(1), "S₃ has a trivial centre");
2222
2223 let s4 = conjugacy_classes(4, &s_n(4), 1000).expect("S₄ is enumerable");
2225 assert_eq!(s4.len(), 5, "S₄ has 5 conjugacy classes (= 5 irreps)");
2226 assert_eq!(s4.iter().map(|c| c.len()).sum::<usize>(), 24, "the classes partition S₄");
2227 assert_eq!(center_order(4, &s_n(4), 1000), Some(1), "S₄ has a trivial centre");
2228
2229 assert_eq!(conjugacy_classes(6, &c_n(6), 1000).map(|c| c.len()), Some(6), "C₆ has |C₆| classes");
2231 assert_eq!(center_order(6, &c_n(6), 1000), Some(6), "an abelian group is its own centre");
2232 }
2233
2234 #[test]
2235 fn exponent_and_order_spectrum() {
2236 let s_n = |n: usize| -> Vec<Perm> {
2237 (0..n - 1)
2238 .map(|i| {
2239 let mut p: Perm = (0..n).collect();
2240 p.swap(i, i + 1);
2241 p
2242 })
2243 .collect()
2244 };
2245 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2246 let spec = |s: BTreeSet<usize>| -> Vec<usize> { s.into_iter().collect() };
2247
2248 assert_eq!(spec(element_orders(4, &c_n(4), 1000).unwrap()), vec![1, 2, 4]);
2250 assert_eq!(exponent(4, &c_n(4), 1000), Some(4), "C₄ has exponent 4");
2251 assert_eq!(spec(element_orders(6, &c_n(6), 1000).unwrap()), vec![1, 2, 3, 6]);
2253 assert_eq!(exponent(6, &c_n(6), 1000), Some(6));
2254
2255 assert_eq!(spec(element_orders(3, &s_n(3), 1000).unwrap()), vec![1, 2, 3]);
2257 assert_eq!(exponent(3, &s_n(3), 1000), Some(6), "S₃ has exponent 6");
2258 assert_eq!(spec(element_orders(4, &s_n(4), 1000).unwrap()), vec![1, 2, 3, 4]);
2260 assert_eq!(exponent(4, &s_n(4), 1000), Some(12), "S₄ has exponent 12");
2261 }
2262
2263 #[test]
2264 fn cycle_index_drives_polya_counting() {
2265 let s_n = |n: usize| -> Vec<Perm> {
2266 (0..n - 1)
2267 .map(|i| {
2268 let mut p: Perm = (0..n).collect();
2269 p.swap(i, i + 1);
2270 p
2271 })
2272 .collect()
2273 };
2274 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2275
2276 let ci = cycle_index(3, &s_n(3), 1000).unwrap();
2278 assert_eq!(ci.get(&vec![1, 1, 1]), Some(&1));
2279 assert_eq!(ci.get(&vec![1, 2]), Some(&3));
2280 assert_eq!(ci.get(&vec![3]), Some(&2));
2281
2282 assert_eq!(polya_count(4, &c_n(4), 2, 1000), Some(6), "6 binary necklaces of length 4");
2285 assert_eq!(polya_count(3, &s_n(3), 2, 1000), Some(4), "4 binary 3-point assignments up to S₃");
2286 assert_eq!(polya_count(5, &c_n(5), 2, 1000), Some(8), "8 binary necklaces of length 5");
2287
2288 let brute_assignment_orbits = |deg: usize, gens: &[Perm]| -> u128 {
2290 let mut seen = std::collections::HashSet::new();
2291 let mut orbits = 0u128;
2292 for x in 0u64..(1u64 << deg) {
2293 let a: Vec<bool> = (0..deg).map(|i| (x >> i) & 1 == 1).collect();
2294 if seen.contains(&a) {
2295 continue;
2296 }
2297 orbits += 1;
2298 let mut stack = vec![a];
2299 while let Some(cur) = stack.pop() {
2300 if !seen.insert(cur.clone()) {
2301 continue;
2302 }
2303 for g in gens {
2304 let mut pm = vec![false; deg];
2305 for v in 0..deg {
2306 pm[g[v]] = cur[v];
2307 }
2308 if !seen.contains(&pm) {
2309 stack.push(pm);
2310 }
2311 }
2312 }
2313 }
2314 orbits
2315 };
2316 for (deg, gens) in [(4, c_n(4)), (3, s_n(3)), (4, s_n(4))] {
2317 assert_eq!(
2318 polya_count(deg, &gens, 2, 1000),
2319 Some(brute_assignment_orbits(deg, &gens)),
2320 "Pólya(2) equals the brute assignment-orbit count"
2321 );
2322 }
2323
2324 assert_eq!(pattern_inventory(4, &c_n(4), 1000), Some(vec![1, 1, 2, 1, 1]));
2327 assert_eq!(pattern_inventory(3, &s_n(3), 1000), Some(vec![1, 1, 1, 1]));
2329 for (deg, gens) in [(4, c_n(4)), (3, s_n(3)), (4, s_n(4)), (5, c_n(5))] {
2331 let inv = pattern_inventory(deg, &gens, 1000).unwrap();
2332 assert_eq!(inv.iter().sum::<u128>(), polya_count(deg, &gens, 2, 1000).unwrap());
2333 }
2334 }
2335
2336 #[test]
2337 fn abelianisation_is_the_largest_abelian_quotient() {
2338 let s_n = |n: usize| -> Vec<Perm> {
2339 (0..n - 1)
2340 .map(|i| {
2341 let mut p: Perm = (0..n).collect();
2342 p.swap(i, i + 1);
2343 p
2344 })
2345 .collect()
2346 };
2347 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2348 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]]; let d4 = vec![vec![1, 2, 3, 0], vec![0, 3, 2, 1]];
2350
2351 assert_eq!(abelianization(3, &s_n(3), 1000), Some((2, 2)), "S₃ᵃᵇ = C₂");
2353 assert_eq!(abelianization(4, &s_n(4), 1000), Some((2, 2)), "S₄ᵃᵇ = C₂");
2354 assert_eq!(abelianization(6, &c_n(6), 1000), Some((6, 6)), "C₆ᵃᵇ = C₆ (cyclic)");
2356 assert_eq!(abelianization(4, &v4, 1000), Some((4, 2)), "V₄ᵃᵇ = V₄ (order 4, exponent 2, NOT cyclic)");
2357 assert_eq!(abelianization(4, &d4, 1000), Some((4, 2)), "D₄ᵃᵇ = C₂ × C₂");
2359
2360 for (deg, gens) in [(3, s_n(3)), (4, s_n(4)), (6, c_n(6)), (4, v4.clone()), (4, d4.clone())] {
2362 let (ab_order, _) = abelianization(deg, &gens, 1000).unwrap();
2363 let g = schreier_sims(deg, &gens).order();
2364 let d = schreier_sims(deg, &derived_subgroup(deg, &gens)).order();
2365 assert_eq!(ab_order, g / d, "|Gᵃᵇ| = |G| / |[G,G]|");
2366 }
2367 }
2368
2369 #[test]
2370 fn subgroup_lattice_is_counted() {
2371 let s_n = |n: usize| -> Vec<Perm> {
2372 (0..n - 1)
2373 .map(|i| {
2374 let mut p: Perm = (0..n).collect();
2375 p.swap(i, i + 1);
2376 p
2377 })
2378 .collect()
2379 };
2380 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2381 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]];
2382
2383 assert_eq!(subgroup_count(4, &c_n(4), 1000), Some(3), "C₄: 1, C₂, C₄");
2385 assert_eq!(subgroup_count(6, &c_n(6), 1000), Some(4), "C₆: 1, C₂, C₃, C₆");
2386 assert_eq!(subgroup_count(3, &s_n(3), 1000), Some(6), "S₃: 1, three C₂, C₃, S₃");
2387 assert_eq!(subgroup_count(4, &v4, 1000), Some(5), "V₄: 1, three C₂, V₄");
2388 assert_eq!(subgroup_count(4, &s_n(4), 1000), Some(30), "S₄ has 30 subgroups");
2389 }
2390
2391 #[test]
2392 fn simplicity_detects_the_building_block_groups() {
2393 let s_n = |n: usize| -> Vec<Perm> {
2394 (0..n - 1)
2395 .map(|i| {
2396 let mut p: Perm = (0..n).collect();
2397 p.swap(i, i + 1);
2398 p
2399 })
2400 .collect()
2401 };
2402 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2403 let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]];
2405
2406 assert_eq!(is_simple(5, &c_n(5), 1000), Some(true), "C₅ is simple (prime order)");
2408 assert_eq!(is_simple(4, &c_n(4), 1000), Some(false), "C₄ is not simple (has C₂)");
2409 assert_eq!(is_simple(6, &c_n(6), 1000), Some(false), "C₆ is not simple");
2410 assert_eq!(is_simple(3, &s_n(3), 1000), Some(false), "S₃ is not simple");
2412
2413 assert_eq!(schreier_sims(5, &a5).order(), 60, "A₅ has order 60");
2415 assert_eq!(is_simple(5, &a5, 1000), Some(true), "A₅ is simple");
2416 assert!(!is_abelian(5, &a5) && !is_solvable(5, &a5), "A₅ is non-abelian and unsolvable");
2418 }
2419
2420 #[test]
2421 fn composition_factors_are_the_jordan_holder_decomposition() {
2422 let s_n = |n: usize| -> Vec<Perm> {
2423 (0..n - 1)
2424 .map(|i| {
2425 let mut p: Perm = (0..n).collect();
2426 p.swap(i, i + 1);
2427 p
2428 })
2429 .collect()
2430 };
2431 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2432 let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]];
2433
2434 assert_eq!(composition_factor_orders(4, &c_n(4), 1000), Some(vec![2, 2]), "C₄: C₂, C₂");
2436 assert_eq!(composition_factor_orders(6, &c_n(6), 1000), Some(vec![2, 3]), "C₆: C₂, C₃");
2437 assert_eq!(composition_factor_orders(3, &s_n(3), 1000), Some(vec![2, 3]), "S₃: C₂, C₃");
2438 assert_eq!(composition_factor_orders(4, &s_n(4), 1000), Some(vec![2, 2, 2, 3]), "S₄: C₂³, C₃");
2439 assert_eq!(composition_factor_orders(5, &a5, 1000), Some(vec![60]), "A₅ is simple");
2441
2442 for (deg, gens) in [(4, c_n(4)), (6, c_n(6)), (3, s_n(3)), (4, s_n(4)), (5, a5.clone())] {
2444 let factors = composition_factor_orders(deg, &gens, 1000).unwrap();
2445 assert_eq!(factors.iter().product::<u128>(), schreier_sims(deg, &gens).order(), "Π factors = |G|");
2446 }
2447 }
2448
2449 #[test]
2450 fn sylow_counts_satisfy_sylows_theorem() {
2451 let s_n = |n: usize| -> Vec<Perm> {
2452 (0..n - 1)
2453 .map(|i| {
2454 let mut p: Perm = (0..n).collect();
2455 p.swap(i, i + 1);
2456 p
2457 })
2458 .collect()
2459 };
2460 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2461 let a4 = vec![vec![1, 2, 0, 3], vec![0, 2, 3, 1]]; let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]];
2463
2464 assert_eq!(sylow_counts(3, &s_n(3), 1000), Some(vec![(2, 3), (3, 1)]), "S₃: 3 Sylow-2, 1 Sylow-3");
2466 assert_eq!(sylow_counts(4, &s_n(4), 1000), Some(vec![(2, 3), (3, 4)]), "S₄: 3 Sylow-2 (D₄), 4 Sylow-3");
2467 assert_eq!(schreier_sims(4, &a4).order(), 12, "A₄ has order 12");
2468 assert_eq!(sylow_counts(4, &a4, 1000), Some(vec![(2, 1), (3, 4)]), "A₄: V₄ normal, 4 Sylow-3");
2469 assert_eq!(sylow_counts(5, &a5, 1000), Some(vec![(2, 5), (3, 10), (5, 6)]), "A₅: 5/10/6 Sylow subgroups");
2470 assert_eq!(sylow_counts(6, &c_n(6), 1000), Some(vec![(2, 1), (3, 1)]), "C₆: unique Sylow subgroups");
2472
2473 for (deg, gens) in [(3, s_n(3)), (4, s_n(4)), (4, a4.clone()), (5, a5.clone()), (6, c_n(6))] {
2475 for (p, n_p) in sylow_counts(deg, &gens, 1000).unwrap() {
2476 assert_eq!(n_p as u128 % p, 1, "n_{p} ≡ 1 (mod {p})");
2477 }
2478 }
2479 }
2480
2481 #[test]
2482 fn class_algebra_constants_and_real_classes() {
2483 let s_n = |n: usize| -> Vec<Perm> {
2484 (0..n - 1)
2485 .map(|i| {
2486 let mut p: Perm = (0..n).collect();
2487 p.swap(i, i + 1);
2488 p
2489 })
2490 .collect()
2491 };
2492 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2493
2494 for (deg, gens) in [(3, s_n(3)), (4, s_n(4)), (4, c_n(4)), (6, c_n(6))] {
2496 let classes = conjugacy_classes(deg, &gens, 1000).unwrap();
2497 let a = class_multiplication_coefficients(deg, &gens, 1000).unwrap();
2498 let k = classes.len();
2499 for i in 0..k {
2500 for j in 0..k {
2501 let lhs: u128 = (0..k).map(|kk| a[i][j][kk] * classes[kk].len() as u128).sum();
2502 let rhs = classes[i].len() as u128 * classes[j].len() as u128;
2503 assert_eq!(lhs, rhs, "Σ a[{i}][{j}][k]·|Cₖ| = |Cᵢ|·|Cⱼ|");
2504 }
2505 }
2506 }
2507
2508 assert_eq!(real_class_count(3, &s_n(3), 1000), Some(3), "S₃: all 3 classes real");
2510 assert_eq!(real_class_count(4, &s_n(4), 1000), Some(5), "Sₙ: every class is real");
2511 assert_eq!(real_class_count(4, &c_n(4), 1000), Some(2), "C₄: only e and the order-2 class are real");
2512 assert_eq!(real_class_count(6, &c_n(6), 1000), Some(2), "C₆: e and the order-2 class");
2513 }
2514
2515 #[test]
2516 fn character_table_matches_the_classical_tables() {
2517 let s_n = |n: usize| -> Vec<Perm> {
2518 (0..n - 1)
2519 .map(|i| {
2520 let mut p: Perm = (0..n).collect();
2521 p.swap(i, i + 1);
2522 p
2523 })
2524 .collect()
2525 };
2526 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2527 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]]; let d4 = vec![vec![1, 2, 3, 0], vec![0, 3, 2, 1]]; let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]]; let cases: Vec<(usize, Vec<Perm>, u128, Vec<u128>)> = vec![
2533 (4, c_n(4), 4, vec![1, 1, 1, 1]), (6, c_n(6), 6, vec![1, 1, 1, 1, 1, 1]), (4, v4.clone(), 4, vec![1, 1, 1, 1]), (3, s_n(3), 6, vec![1, 1, 2]), (4, s_n(4), 24, vec![1, 1, 2, 3, 3]), (4, d4.clone(), 8, vec![1, 1, 1, 1, 2]),(5, a5.clone(), 60, vec![1, 3, 3, 4, 5]), ];
2541
2542 for (deg, gens, order, want_degrees) in cases {
2543 let table = character_table(deg, &gens, 2000)
2544 .unwrap_or_else(|| panic!("character_table failed for |G|={order}"));
2545 let classes = conjugacy_classes(deg, &gens, 2000).unwrap();
2546 let k = classes.len();
2547
2548 assert_eq!(table.degrees.len(), k, "#irreducibles = #conjugacy classes (|G|={order})");
2550 assert_eq!(table.degrees, want_degrees, "degree sequence (|G|={order})");
2551 assert_eq!(table.degrees.iter().map(|d| d * d).sum::<u128>(), order, "Σ dᵢ² = |G|");
2552
2553 assert!(
2555 table.values.iter().any(|row| row.iter().all(|&x| x == 1)),
2556 "trivial character present (|G|={order})"
2557 );
2558
2559 let id: Perm = (0..deg).collect();
2561 let id_class = classes.iter().position(|c| c.contains(&id)).unwrap();
2562 for s in 0..k {
2563 assert_eq!(
2564 table.values[s][id_class] as u128, table.degrees[s],
2565 "χ_{s}(1) must equal its degree (|G|={order})"
2566 );
2567 }
2568 if is_abelian(deg, &gens) {
2569 assert!(table.degrees.iter().all(|&d| d == 1), "abelian ⇒ all degrees 1");
2570 assert_eq!(table.degrees.len() as u128, order, "abelian ⇒ |G| linear characters");
2571 }
2572
2573 let p = table.prime as u128;
2574 for s in 0..k {
2576 for t in 0..k {
2577 let mut acc = 0u128;
2578 for r in 0..k {
2579 let prod = table.values[s][r] as u128
2580 * table.values[t][table.inverse_class[r]] as u128
2581 % p;
2582 acc = (acc + table.class_sizes[r] % p * prod) % p;
2583 }
2584 let want = if s == t { order % p } else { 0 };
2585 assert_eq!(acc, want, "row orthogonality s={s} t={t} (|G|={order})");
2586 }
2587 }
2588 for r in 0..k {
2590 for t in 0..k {
2591 let mut acc = 0u128;
2592 for s in 0..k {
2593 acc = (acc
2594 + table.values[s][r] as u128 * table.values[s][table.inverse_class[t]] as u128)
2595 % p;
2596 }
2597 let want = if r == t { order / table.class_sizes[r] % p } else { 0 };
2598 assert_eq!(acc, want, "column orthogonality r={r} t={t} (|G|={order})");
2599 }
2600 }
2601 }
2602
2603 let triv = character_table(1, &[], 10).unwrap();
2605 assert_eq!(triv.degrees, vec![1]);
2606 assert_eq!(triv.values, vec![vec![1]]);
2607
2608 assert_eq!(irreducible_degrees(5, &a5, 2000), Some(vec![1, 3, 3, 4, 5]));
2610 }
2611
2612 #[test]
2613 fn frobenius_schur_indicators_distinguish_d4_from_q8() {
2614 let s_n = |n: usize| -> Vec<Perm> {
2615 (0..n - 1)
2616 .map(|i| {
2617 let mut p: Perm = (0..n).collect();
2618 p.swap(i, i + 1);
2619 p
2620 })
2621 .collect()
2622 };
2623 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2624 let d4 = vec![vec![1, 2, 3, 0], vec![0, 3, 2, 1]]; let q8 = vec![vec![2, 3, 1, 0, 6, 7, 5, 4], vec![4, 5, 7, 6, 1, 0, 2, 3]];
2628 assert_eq!(schreier_sims(8, &q8).order(), 8, "Q₈ has order 8");
2629
2630 let sorted = |mut v: Vec<i8>| {
2631 v.sort();
2632 v
2633 };
2634
2635 assert_eq!(frobenius_schur_indicators(3, &s_n(3), 1000), Some(vec![1, 1, 1]), "S₃ is totally real");
2637 assert_eq!(
2638 frobenius_schur_indicators(4, &s_n(4), 1000),
2639 Some(vec![1, 1, 1, 1, 1]),
2640 "S₄ is totally real"
2641 );
2642
2643 let c4 = frobenius_schur_indicators(4, &c_n(4), 1000).unwrap();
2645 assert_eq!(sorted(c4.clone()), vec![0, 0, 1, 1], "C₄: two real, one complex-conjugate pair");
2646 assert_eq!(
2647 c4.iter().filter(|&&v| v != 0).count(),
2648 real_class_count(4, &c_n(4), 1000).unwrap(),
2649 "#real-valued characters = #real classes"
2650 );
2651
2652 assert_eq!(
2655 irreducible_degrees(4, &d4, 1000),
2656 irreducible_degrees(8, &q8, 1000),
2657 "D₄ and Q₈ share a character table"
2658 );
2659 assert_eq!(real_class_count(4, &d4, 1000), real_class_count(8, &q8, 1000), "…and # real classes");
2660
2661 let fs_d4 = frobenius_schur_indicators(4, &d4, 1000).unwrap();
2662 let fs_q8 = frobenius_schur_indicators(8, &q8, 1000).unwrap();
2663 assert_eq!(sorted(fs_d4.clone()), vec![1, 1, 1, 1, 1], "D₄: the 2-dim rep is REAL (+1)");
2664 assert_eq!(sorted(fs_q8.clone()), vec![-1, 1, 1, 1, 1], "Q₈: the 2-dim rep is QUATERNIONIC (−1)");
2665 assert_ne!(sorted(fs_d4.clone()), sorted(fs_q8.clone()), "Frobenius–Schur SEPARATES D₄ from Q₈");
2666
2667 let degs_d4 = irreducible_degrees(4, &d4, 1000).unwrap();
2670 let degs_q8 = irreducible_degrees(8, &q8, 1000).unwrap();
2671 let dot = |nu: &[i8], d: &[u128]| -> i128 { nu.iter().zip(d).map(|(&v, &x)| v as i128 * x as i128).sum() };
2672 assert_eq!(dot(&fs_d4, °s_d4), 6, "D₄: 6 square roots of identity");
2673 assert_eq!(dot(&fs_q8, °s_q8), 2, "Q₈: only id and −1 square to identity");
2674 }
2675
2676 #[test]
2677 fn isotypic_decomposition_of_the_permutation_character() {
2678 let s_n = |n: usize| -> Vec<Perm> {
2679 (0..n - 1)
2680 .map(|i| {
2681 let mut p: Perm = (0..n).collect();
2682 p.swap(i, i + 1);
2683 p
2684 })
2685 .collect()
2686 };
2687 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2688 let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]];
2689
2690 for (deg, gens) in [(3, s_n(3)), (4, s_n(4)), (5, a5.clone()), (4, c_n(4)), (6, c_n(6))] {
2691 let table = character_table(deg, &gens, 2000).unwrap();
2692 let mult = isotypic_multiplicities(deg, &gens, 2000)
2693 .unwrap_or_else(|| panic!("isotypic decomposition failed for degree {deg}"));
2694 assert_eq!(
2696 mult.iter().zip(&table.degrees).map(|(m, d)| m * d).sum::<u128>(),
2697 deg as u128,
2698 "Σ m_s·d_s = dim of the permutation representation (degree {deg})"
2699 );
2700 assert_eq!(
2701 mult.iter().map(|m| m * m).sum::<u128>(),
2702 rank(deg, &gens) as u128,
2703 "⟨π,π⟩ = #orbitals = rank (degree {deg})"
2704 );
2705 let trivial = table.values.iter().position(|row| row.iter().all(|&x| x == 1)).unwrap();
2706 assert_eq!(
2707 mult[trivial],
2708 orbits(deg, &gens).len() as u128,
2709 "⟨π,1⟩ = #orbits (Burnside) (degree {deg})"
2710 );
2711 let pi = permutation_character(deg, &gens, 2000).unwrap();
2714 assert_eq!(pi[table.identity_class], deg as u128, "the identity fixes all {deg} points");
2715 let order: u128 = table.degrees.iter().map(|d| d * d).sum();
2716 let avg_fixed: u128 = table.class_sizes.iter().zip(&pi).map(|(h, f)| h * f).sum();
2717 assert_eq!(avg_fixed, order * orbits(deg, &gens).len() as u128, "Σ|C_r|·π(C_r) = |G|·#orbits");
2718 }
2719
2720 let m_s4 = isotypic_multiplicities(4, &s_n(4), 2000).unwrap();
2723 assert_eq!(m_s4.iter().filter(|&&m| m > 0).count(), 2, "S₄ on 4 points: trivial ⊕ standard");
2724 assert!(m_s4.iter().all(|&m| m <= 1), "each at most once (2-transitive ⇒ multiplicity-free)");
2725
2726 assert_eq!(isotypic_multiplicities(4, &c_n(4), 2000).unwrap(), vec![1, 1, 1, 1], "C₄ regular rep");
2729 }
2730
2731 #[test]
2732 fn table_of_marks_classifies_the_g_sets() {
2733 let s_n = |n: usize| -> Vec<Perm> {
2734 (0..n - 1)
2735 .map(|i| {
2736 let mut p: Perm = (0..n).collect();
2737 p.swap(i, i + 1);
2738 p
2739 })
2740 .collect()
2741 };
2742 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2743 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]];
2744 let d4 = vec![vec![1, 2, 3, 0], vec![0, 3, 2, 1]];
2745
2746 let (orders, m) = table_of_marks(4, &c_n(4), 200).unwrap();
2748 assert_eq!(orders, vec![1, 2, 4], "subgroup orders 1, 2, 4");
2749 assert_eq!(m, vec![vec![4, 2, 1], vec![0, 2, 1], vec![0, 0, 1]], "C₄ table of marks");
2750
2751 let (so, sm) = table_of_marks(3, &s_n(3), 200).unwrap();
2753 assert_eq!(so, vec![1, 2, 3, 6]);
2754 assert_eq!(
2755 sm,
2756 vec![vec![6, 3, 2, 1], vec![0, 1, 0, 1], vec![0, 0, 2, 1], vec![0, 0, 0, 1]],
2757 "S₃ table of marks"
2758 );
2759
2760 for (deg, gens, order) in [(3, s_n(3), 6u128), (4, c_n(4), 4), (4, v4.clone(), 4), (4, d4.clone(), 8), (4, s_n(4), 24)] {
2764 let (ord, mk) = table_of_marks(deg, &gens, 300).unwrap();
2765 let k = ord.len();
2766 assert_eq!(*ord.last().unwrap(), order, "the largest subgroup is G itself");
2767 for j in 0..k {
2768 assert_eq!(mk[0][j], order / ord[j], "m(1, H_j) = [G : H_j]");
2769 assert_eq!(mk[j][k - 1], 1, "every subgroup fixes the single coset of G");
2770 assert_eq!(mk[k - 1][j], u128::from(j == k - 1), "G fixes a coset of H only when H = G");
2771 assert!(mk[j][j] >= 1, "diagonal [N(H_j):H_j] is nonzero ⇒ invertible");
2772 for i in 0..k {
2773 if ord[i] > ord[j] {
2774 assert_eq!(mk[i][j], 0, "triangular: no mark when |H_i| > |H_j|");
2775 }
2776 }
2777 }
2778 }
2779 }
2780
2781 #[test]
2782 fn burnside_ring_multiplies_g_sets() {
2783 let s_n = |n: usize| -> Vec<Perm> {
2784 (0..n - 1)
2785 .map(|i| {
2786 let mut p: Perm = (0..n).collect();
2787 p.swap(i, i + 1);
2788 p
2789 })
2790 .collect()
2791 };
2792 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2793 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]];
2794
2795 let n = burnside_ring_product(3, &s_n(3), 200).unwrap();
2799 assert_eq!(n[1][1], vec![1, 1, 0, 0], "G/C₂ × G/C₂ = G/1 ⊔ G/C₂ in S₃");
2800
2801 for (deg, gens, order) in [(3, s_n(3), 6u128), (4, c_n(4), 4), (4, v4.clone(), 4), (4, s_n(4), 24)] {
2803 let (_o, marks) = table_of_marks(deg, &gens, 300).unwrap();
2804 let nn = burnside_ring_product(deg, &gens, 300).unwrap();
2805 let k = marks.len();
2806 let idx = marks[0].clone(); for a in 0..k {
2808 for b in 0..k {
2809 assert!(nn[a][b].iter().all(|&c| c >= 0), "G-set multiplicities are non-negative");
2811 assert_eq!(nn[a][b], nn[b][a], "Burnside product is commutative");
2813 let lhs: i128 = (0..k).map(|l| nn[a][b][l] * idx[l] as i128).sum();
2815 assert_eq!(lhs, (idx[a] * idx[b]) as i128, "point counts multiply");
2816 }
2817 let mut id = vec![0i128; k];
2819 id[a] = 1;
2820 assert_eq!(nn[k - 1][a], id, "G/G is the identity of the Burnside ring");
2821 }
2822 let mut want0 = vec![0i128; k];
2824 want0[0] = order as i128;
2825 assert_eq!(nn[0][0], want0, "(G/1)² = |G|·(G/1)");
2826 }
2827 }
2828
2829 #[test]
2830 fn permutation_character_decomposition_bridges_marks_and_characters() {
2831 let s_n = |n: usize| -> Vec<Perm> {
2832 (0..n - 1)
2833 .map(|i| {
2834 let mut p: Perm = (0..n).collect();
2835 p.swap(i, i + 1);
2836 p
2837 })
2838 .collect()
2839 };
2840 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2841 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]];
2842 let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]];
2843
2844 let (so, sd, sm) = permutation_character_decomposition(3, &s_n(3), 200).unwrap();
2848 assert_eq!(so, vec![1, 2, 3, 6]);
2849 assert_eq!(sd, vec![1, 1, 2], "S₃ irreducible degrees");
2850 assert_eq!(sm[0], vec![1, 1, 2], "G/1 is the regular representation");
2852 assert_eq!(sm[3], {
2853 let mut e = vec![0, 0, 0];
2854 e[sd.iter().position(|&d| d == 1).unwrap()] = 1; e
2857 }, "G/G is the trivial representation");
2858 assert_eq!(sm[1].iter().map(|&x| x * x).sum::<u128>(), 2, "G/C₂ has rank 2 (2-transitive)");
2860
2861 for (deg, gens, order) in [(3, s_n(3), 6u128), (4, c_n(4), 4), (4, v4.clone(), 4), (4, s_n(4), 24), (5, a5.clone(), 60)] {
2864 let (orders, degrees, m) = permutation_character_decomposition(deg, &gens, 2000).unwrap();
2865 let triv = degrees.iter().position(|&d| {
2866 d == 1
2868 });
2869 assert!(triv.is_some());
2870 assert_eq!(m[0], degrees, "G/1 = regular representation = Σ d_s·χ_s");
2871 for (i, row) in m.iter().enumerate() {
2872 let dim: u128 = (0..degrees.len()).map(|s| row[s] * degrees[s]).sum();
2873 assert_eq!(dim, order / orders[i], "Σ_s M[i][s]·d_s = [G : H_i]");
2874 }
2875 if order == 60 {
2878 assert!(m.iter().any(|row| row.iter().map(|&x| x * x).sum::<u128>() == 2 && row.iter().any(|&x| x == 1)),
2879 "A₅ has a 2-transitive action (the natural 5-point one)");
2880 }
2881 }
2882 }
2883
2884 #[test]
2885 fn subgroup_lattice_mobius_and_generating_tuples() {
2886 let s_n = |n: usize| -> Vec<Perm> {
2887 (0..n - 1)
2888 .map(|i| {
2889 let mut p: Perm = (0..n).collect();
2890 p.swap(i, i + 1);
2891 p
2892 })
2893 .collect()
2894 };
2895 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2896 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]];
2897
2898 let nt_mobius = |mut m: usize| -> i128 {
2901 let mut sign = 1i128;
2902 let mut d = 2;
2903 while d * d <= m {
2904 if m % d == 0 {
2905 m /= d;
2906 if m % d == 0 {
2907 return 0; }
2909 sign = -sign;
2910 }
2911 d += 1;
2912 }
2913 if m > 1 {
2914 sign = -sign; }
2916 sign
2917 };
2918 for n in [2usize, 3, 4, 5, 6, 7, 8, 9, 12] {
2919 assert_eq!(
2920 mobius_number(n, &c_n(n), 400),
2921 Some(nt_mobius(n)),
2922 "μ(1, C_{n}) = number-theoretic μ({n})"
2923 );
2924 }
2925 assert_eq!(mobius_number(3, &s_n(3), 400), Some(3), "μ(1, S₃) = 3");
2927 assert_eq!(mobius_number(4, &v4, 400), Some(2), "μ(1, V₄) = 2");
2928
2929 let brute_generating = |deg: usize, gens: &[Perm], k: u32| -> i128 {
2932 let elements: Vec<Perm> =
2933 subgroup_closure(deg, &gens.iter().cloned().collect()).into_iter().collect();
2934 let total = elements.len();
2935 let mut count = 0i128;
2936 let mut tuple = vec![0usize; k as usize];
2938 'outer: loop {
2939 let seed: Vec<Perm> = tuple.iter().map(|&t| elements[t].clone()).collect();
2940 if subgroup_closure(deg, &seed.into_iter().collect()).len() == total {
2941 count += 1;
2942 }
2943 let mut pos = 0;
2945 loop {
2946 if pos == k as usize {
2947 break 'outer;
2948 }
2949 tuple[pos] += 1;
2950 if tuple[pos] < total {
2951 break;
2952 }
2953 tuple[pos] = 0;
2954 pos += 1;
2955 }
2956 }
2957 count
2958 };
2959 for (deg, gens, k) in [(3, s_n(3), 2u32), (3, s_n(3), 3), (4, c_n(4), 2), (4, v4.clone(), 2), (4, v4.clone(), 3)] {
2960 assert_eq!(
2961 generating_tuple_count(deg, &gens, 400, k),
2962 Some(brute_generating(deg, &gens, k)),
2963 "Hall's e_{k}(G) must equal the brute-force generating-tuple count"
2964 );
2965 }
2966 assert_eq!(generating_tuple_count(6, &c_n(6), 400, 1), Some(2), "e₁(C₆) = φ(6) = 2");
2968 assert_eq!(generating_tuple_count(5, &c_n(5), 400, 1), Some(4), "e₁(C₅) = φ(5) = 4");
2969 }
2970
2971 #[test]
2972 fn automorphism_group_order_matches_classical_values() {
2973 let s_n = |n: usize| -> Vec<Perm> {
2974 (0..n - 1)
2975 .map(|i| {
2976 let mut p: Perm = (0..n).collect();
2977 p.swap(i, i + 1);
2978 p
2979 })
2980 .collect()
2981 };
2982 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
2983 let v4 = vec![vec![1, 0, 3, 2], vec![2, 3, 0, 1]];
2984 let d4 = vec![vec![1, 2, 3, 0], vec![0, 3, 2, 1]];
2985 let q8 = vec![vec![2, 3, 1, 0, 6, 7, 5, 4], vec![4, 5, 7, 6, 1, 0, 2, 3]];
2986
2987 assert_eq!(automorphism_group_order(4, &c_n(4), 500), Some(2), "|Aut(C₄)| = φ(4) = 2");
2989 assert_eq!(automorphism_group_order(5, &c_n(5), 500), Some(4), "|Aut(C₅)| = φ(5) = 4");
2990 assert_eq!(automorphism_group_order(6, &c_n(6), 500), Some(2), "|Aut(C₆)| = φ(6) = 2");
2991 assert_eq!(outer_automorphism_order(5, &c_n(5), 500), Some(4), "C₅ abelian ⇒ Out = Aut");
2992
2993 assert_eq!(automorphism_group_order(3, &s_n(3), 500), Some(6), "|Aut(S₃)| = 6");
2995 assert_eq!(outer_automorphism_order(3, &s_n(3), 500), Some(1), "S₃ complete ⇒ Out = 1");
2996 assert_eq!(automorphism_group_order(4, &s_n(4), 500), Some(24), "|Aut(S₄)| = 24");
2997 assert_eq!(outer_automorphism_order(4, &s_n(4), 500), Some(1), "S₄ complete ⇒ Out = 1");
2998
2999 assert_eq!(automorphism_group_order(4, &v4, 500), Some(6), "|Aut(V₄)| = |GL(2,2)| = 6");
3001 assert_eq!(outer_automorphism_order(4, &v4, 500), Some(6), "V₄ abelian ⇒ Out = Aut = S₃");
3002
3003 assert_eq!(automorphism_group_order(4, &d4, 500), Some(8), "|Aut(D₄)| = 8");
3006 assert_eq!(automorphism_group_order(8, &q8, 500), Some(24), "|Aut(Q₈)| = 24 = |S₄|");
3007 assert_eq!(outer_automorphism_order(4, &d4, 500), Some(2), "Out(D₄) = C₂");
3008 assert_eq!(outer_automorphism_order(8, &q8, 500), Some(6), "Out(Q₈) = S₃");
3009 assert_ne!(
3010 automorphism_group_order(4, &d4, 500),
3011 automorphism_group_order(8, &q8, 500),
3012 "Aut SEPARATES D₄ from Q₈"
3013 );
3014 }
3015
3016 #[test]
3017 fn galois_action_distinguishes_real_from_rational_classes() {
3018 let s_n = |n: usize| -> Vec<Perm> {
3019 (0..n - 1)
3020 .map(|i| {
3021 let mut p: Perm = (0..n).collect();
3022 p.swap(i, i + 1);
3023 p
3024 })
3025 .collect()
3026 };
3027 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
3028 let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]];
3029
3030 assert_eq!(rational_class_count(3, &s_n(3), 2000), Some(3), "S₃ is rational");
3032 assert_eq!(rational_class_count(4, &s_n(4), 2000), Some(5), "S₄ is rational");
3033 assert_eq!(rational_class_count(4, &c_n(4), 2000), Some(2), "C₄: only e, g² rational");
3035 assert_eq!(rational_class_count(6, &c_n(6), 2000), Some(2), "C₆: only e, g³ rational");
3036 assert_eq!(rational_class_count(5, &c_n(5), 2000), Some(1), "C₅: the Galois group fuses g..g⁴");
3037
3038 assert_eq!(real_class_count(5, &a5, 2000), Some(5), "A₅: all 5 classes are real");
3041 assert_eq!(rational_class_count(5, &a5, 2000), Some(3), "A₅: only 3 classes are rational");
3042
3043 for (deg, gens) in [(3, s_n(3)), (4, s_n(4)), (4, c_n(4)), (6, c_n(6)), (5, c_n(5)), (5, a5.clone())] {
3046 let orbits = galois_class_orbits(deg, &gens, 2000).unwrap();
3047 let t = character_table(deg, &gens, 2000).unwrap();
3048 assert_eq!(orbits.iter().map(|o| o.len()).sum::<usize>(), t.degrees.len(), "orbits partition");
3050 let rational_chars = (0..t.degrees.len())
3051 .filter(|&s| orbits.iter().all(|o| o.iter().all(|&r| t.values[s][r] == t.values[s][o[0]])))
3052 .count();
3053 assert_eq!(
3054 rational_chars,
3055 rational_class_count(deg, &gens, 2000).unwrap(),
3056 "Burnside: #rational characters = #rational classes (degree {deg})"
3057 );
3058 assert!(
3060 rational_class_count(deg, &gens, 2000).unwrap() <= real_class_count(deg, &gens, 2000).unwrap(),
3061 "rational classes ⊆ real classes (degree {deg})"
3062 );
3063 }
3064 }
3065
3066 #[test]
3067 fn tensor_decomposition_is_the_representation_ring() {
3068 let s_n = |n: usize| -> Vec<Perm> {
3069 (0..n - 1)
3070 .map(|i| {
3071 let mut p: Perm = (0..n).collect();
3072 p.swap(i, i + 1);
3073 p
3074 })
3075 .collect()
3076 };
3077 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
3078 let a5 = vec![vec![1, 2, 0, 3, 4], vec![0, 1, 3, 4, 2]];
3079
3080 for (deg, gens) in [(3, s_n(3)), (4, s_n(4)), (5, a5.clone()), (4, c_n(4)), (6, c_n(6))] {
3081 let t = character_table(deg, &gens, 2000).unwrap();
3082 let n = tensor_decomposition(deg, &gens, 2000)
3083 .unwrap_or_else(|| panic!("tensor decomposition failed for degree {deg}"));
3084 let k = t.degrees.len();
3085 let fs = frobenius_schur_indicators(deg, &gens, 2000).unwrap();
3087 let trivial = t.values.iter().position(|row| row.iter().all(|&x| x == 1)).unwrap();
3088 for i in 0..k {
3089 let self_dual = n[i][i][trivial] == 1;
3090 assert_eq!(self_dual, fs[i] != 0, "χ_i⊗χ_i ⊇ 1 iff χ_i is real (degree {deg}, irrep {i})");
3091 assert_eq!(
3093 (0..k).filter(|&j| n[i][j][trivial] == 1).count(),
3094 1,
3095 "χ_i has a unique dual (degree {deg}, irrep {i})"
3096 );
3097 }
3098 }
3099
3100 let t = character_table(4, &c_n(4), 2000).unwrap();
3103 let n = tensor_decomposition(4, &c_n(4), 2000).unwrap();
3104 let gen_class = (0..4).find(|&r| t.class_reps[r] == vec![1, 2, 3, 0]).unwrap();
3106 let freq: Vec<u64> = (0..4).map(|s| t.values[s][gen_class]).collect();
3107 for a in 0..4 {
3108 for b in 0..4 {
3109 let prod_freq = (freq[a] as u128 * freq[b] as u128 % t.prime as u128) as u64;
3111 let want = (0..4).find(|&c| freq[c] == prod_freq).unwrap();
3112 for c in 0..4 {
3113 assert_eq!(
3114 n[a][b][c],
3115 u128::from(c == want),
3116 "C₄ fusion = character-group multiplication"
3117 );
3118 }
3119 }
3120 }
3121 }
3122
3123 #[test]
3124 fn upper_central_series_agrees_with_the_lower_one() {
3125 let s_n = |n: usize| -> Vec<Perm> {
3126 (0..n - 1)
3127 .map(|i| {
3128 let mut p: Perm = (0..n).collect();
3129 p.swap(i, i + 1);
3130 p
3131 })
3132 .collect()
3133 };
3134 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
3135 let d4 = vec![vec![1, 2, 3, 0], vec![0, 3, 2, 1]];
3136
3137 assert_eq!(upper_central_series(4, &c_n(4), 1000), Some(vec![1, 4]));
3139 assert_eq!(upper_central_series(4, &d4, 1000), Some(vec![1, 2, 8]));
3141 assert_eq!(upper_central_series(3, &s_n(3), 1000), Some(vec![1]), "S₃ has a trivial hypercentre");
3143
3144 for (deg, gens) in [(4, c_n(4)), (4, d4.clone()), (3, s_n(3)), (4, s_n(4)), (5, s_n(5))] {
3147 assert_eq!(
3148 upper_central_length(deg, &gens, 1000),
3149 nilpotency_class(deg, &gens),
3150 "upper- and lower-central series agree on the nilpotency class"
3151 );
3152 }
3153 }
3154
3155 #[test]
3156 fn lower_central_series_decides_nilpotency() {
3157 let s_n = |n: usize| -> Vec<Perm> {
3158 (0..n - 1)
3159 .map(|i| {
3160 let mut p: Perm = (0..n).collect();
3161 p.swap(i, i + 1);
3162 p
3163 })
3164 .collect()
3165 };
3166 let c_n = |n: usize| -> Vec<Perm> { vec![(1..n).chain(std::iter::once(0)).collect()] };
3167
3168 assert!(is_nilpotent(4, &c_n(4)), "C₄ is abelian ⇒ nilpotent");
3170
3171 let d4 = vec![vec![1, 2, 3, 0], vec![0, 3, 2, 1]];
3173 assert_eq!(schreier_sims(4, &d4).order(), 8, "D₄ has order 8");
3174 assert!(!is_abelian(4, &d4), "D₄ is non-abelian");
3175 assert!(is_nilpotent(4, &d4), "D₄ is a 2-group ⇒ nilpotent");
3176
3177 assert!(is_solvable(3, &s_n(3)) && !is_nilpotent(3, &s_n(3)), "S₃: solvable but not nilpotent");
3179 assert!(is_solvable(4, &s_n(4)) && !is_nilpotent(4, &s_n(4)), "S₄: solvable but not nilpotent");
3181
3182 assert_eq!(derived_length(4, &c_n(4)), Some(1), "C₄ abelian ⇒ derived length 1");
3184 assert_eq!(nilpotency_class(4, &c_n(4)), Some(1), "C₄ abelian ⇒ nilpotency class 1");
3185 assert_eq!(nilpotency_class(4, &d4), Some(2), "D₄ has nilpotency class 2");
3186 assert_eq!(derived_length(3, &s_n(3)), Some(2), "S₃ has derived length 2");
3187 assert_eq!(nilpotency_class(3, &s_n(3)), None, "S₃ is not nilpotent");
3188 assert_eq!(derived_length(4, &s_n(4)), Some(3), "S₄ has derived length 3 (S₄ ⊵ A₄ ⊵ V₄ ⊵ 1)");
3189 assert_eq!(derived_length(5, &s_n(5)), None, "S₅ is not solvable");
3190 }
3191}