1fn add(a: &[u8], b: &[u8]) -> Vec<u8> {
19 let n = a.len().max(b.len());
20 (0..n).map(|i| (a.get(i).copied().unwrap_or(0) + b.get(i).copied().unwrap_or(0)) % 2).collect()
21}
22
23fn mul(a: &[u8], b: &[u8]) -> Vec<u8> {
24 if a.is_empty() || b.is_empty() {
25 return vec![];
26 }
27 let mut out = vec![0u8; a.len() + b.len() - 1];
28 for (i, &ai) in a.iter().enumerate() {
29 if ai == 1 {
30 for (j, &bj) in b.iter().enumerate() {
31 out[i + j] = (out[i + j] + bj) % 2;
32 }
33 }
34 }
35 out
36}
37
38pub fn monomial(k: usize) -> Vec<u8> {
40 let mut p = vec![0u8; k + 1];
41 p[k] = 1;
42 p
43}
44
45fn trim(mut p: Vec<u8>) -> Vec<u8> {
46 while p.len() > 1 && *p.last().unwrap() == 0 {
47 p.pop();
48 }
49 p
50}
51
52pub fn total_square(poly: &[u8]) -> Vec<u8> {
55 let x_plus_x2 = [0u8, 1, 1]; let mut result = vec![0u8];
57 let mut power = vec![1u8]; for &c in poly {
59 if c == 1 {
60 result = add(&result, &power);
61 }
62 power = mul(&power, &x_plus_x2);
63 }
64 trim(result)
65}
66
67pub fn sq(i: usize, k: usize) -> u8 {
69 let s = total_square(&monomial(k));
70 s.get(k + i).copied().unwrap_or(0)
71}
72
73fn binom(k: usize, i: usize) -> u64 {
76 if i > k {
77 return 0;
78 }
79 let mut row = vec![1u64; 1];
80 for r in 1..=k {
81 let mut next = vec![1u64; r + 1];
82 for c in 1..r {
83 next[c] = row[c - 1] + row[c];
84 }
85 row = next;
86 }
87 row[i]
88}
89
90pub fn apply_sq(i: usize, poly: &[u8]) -> Vec<u8> {
92 let mut out = vec![0u8; poly.len() + i];
93 for (k, &a) in poly.iter().enumerate() {
94 if a == 1 && binom(k, i) % 2 == 1 {
95 out[k + i] ^= 1;
96 }
97 }
98 trim(out)
99}
100
101fn adem(a: usize, b: usize) -> Vec<Vec<usize>> {
104 let mut terms = Vec::new();
105 for c in 0..=(a / 2) {
106 if binom(b - c - 1, a - 2 * c) % 2 == 1 {
107 let mut m = vec![a + b - c];
108 if c > 0 {
109 m.push(c);
110 }
111 terms.push(m);
112 }
113 }
114 terms
115}
116
117pub fn is_admissible(m: &[usize]) -> bool {
119 m.iter().all(|&i| i >= 1) && m.windows(2).all(|w| w[0] >= 2 * w[1])
120}
121
122pub fn adem_reduce(m: &[usize]) -> std::collections::HashSet<Vec<usize>> {
126 let m: Vec<usize> = m.iter().copied().filter(|&i| i > 0).collect();
127 if is_admissible(&m) {
128 let mut s = std::collections::HashSet::new();
129 if !m.is_empty() {
130 s.insert(m);
131 }
132 return s;
133 }
134 let j = m.windows(2).position(|w| w[0] < 2 * w[1]).unwrap();
135 let (a, b) = (m[j], m[j + 1]);
136 let mut result: std::collections::HashSet<Vec<usize>> = std::collections::HashSet::new();
137 for term in adem(a, b) {
138 let mut spliced = m[..j].to_vec();
139 spliced.extend_from_slice(&term);
140 spliced.extend_from_slice(&m[j + 2..]);
141 for adm in adem_reduce(&spliced) {
142 if !result.insert(adm.clone()) {
143 result.remove(&adm); }
145 }
146 }
147 result
148}
149
150fn z2_span_contains(generators: Vec<std::collections::HashSet<Vec<usize>>>, target: std::collections::HashSet<Vec<usize>>) -> bool {
153 type Vec2 = std::collections::HashSet<Vec<usize>>;
154 fn reduce(mut v: Vec2, basis: &[(Vec<usize>, Vec2)]) -> Vec2 {
155 loop {
156 if v.is_empty() {
157 return v;
158 }
159 let piv = v.iter().max().unwrap().clone();
160 match basis.iter().find(|(p, _)| *p == piv) {
161 Some((_, bset)) => {
162 for x in bset {
163 if !v.insert(x.clone()) {
164 v.remove(x);
165 }
166 }
167 }
168 None => return v,
169 }
170 }
171 }
172 let mut basis: Vec<(Vec<usize>, Vec2)> = Vec::new();
173 for g in generators {
174 let r = reduce(g, &basis);
175 if !r.is_empty() {
176 let piv = r.iter().max().unwrap().clone();
177 basis.push((piv, r));
178 }
179 }
180 reduce(target, &basis).is_empty()
181}
182
183fn z2_rank(generators: Vec<std::collections::HashSet<Vec<usize>>>) -> usize {
186 type Vec2 = std::collections::HashSet<Vec<usize>>;
187 fn reduce(mut v: Vec2, basis: &[(Vec<usize>, Vec2)]) -> Vec2 {
188 loop {
189 if v.is_empty() {
190 return v;
191 }
192 let piv = v.iter().max().unwrap().clone();
193 match basis.iter().find(|(p, _)| *p == piv) {
194 Some((_, bset)) => {
195 for x in bset {
196 if !v.insert(x.clone()) {
197 v.remove(x);
198 }
199 }
200 }
201 None => return v,
202 }
203 }
204 }
205 let mut basis: Vec<(Vec<usize>, Vec2)> = Vec::new();
206 for g in generators {
207 let r = reduce(g, &basis);
208 if !r.is_empty() {
209 let piv = r.iter().max().unwrap().clone();
210 basis.push((piv, r));
211 }
212 }
213 basis.len()
214}
215
216pub fn admissibles_of_degree(t: usize) -> Vec<Vec<usize>> {
218 fn rec(remaining: usize, min_left: usize, suffix: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
219 if remaining == 0 {
220 out.push(suffix.clone());
221 return;
222 }
223 for i in min_left.max(1)..=remaining {
224 suffix.insert(0, i);
225 rec(remaining - i, 2 * i, suffix, out);
226 suffix.remove(0);
227 }
228 }
229 let mut out = Vec::new();
230 rec(t, 1, &mut Vec::new(), &mut out);
231 out
232}
233
234pub fn adams_one_line(t: usize) -> usize {
238 let dim_at = admissibles_of_degree(t).len();
239 let mut decomposables: Vec<std::collections::HashSet<Vec<usize>>> = Vec::new();
240 for d in 1..t {
241 for a in admissibles_of_degree(d) {
242 for b in admissibles_of_degree(t - d) {
243 let mut prod = a.clone();
244 prod.extend_from_slice(&b);
245 decomposables.push(adem_reduce(&prod));
246 }
247 }
248 }
249 dim_at - z2_rank(decomposables)
250}
251
252pub fn is_decomposable_sq(n: usize) -> bool {
256 let generators: Vec<_> = (1..n).map(|a| adem_reduce(&[a, n - a])).collect();
257 let target: std::collections::HashSet<Vec<usize>> = [vec![n]].into_iter().collect();
258 z2_span_contains(generators, target)
259}
260
261#[derive(Clone, PartialEq, Eq)]
264struct Bits(Vec<u64>);
265
266impl Bits {
267 fn new() -> Self {
268 Bits(Vec::new())
269 }
270 fn bit(i: usize) -> Self {
271 let mut b = Bits::new();
272 b.toggle(i);
273 b
274 }
275 fn from_indices(it: impl Iterator<Item = usize>) -> Self {
276 let mut b = Bits::new();
277 for i in it {
278 b.toggle(i);
279 }
280 b
281 }
282 fn toggle(&mut self, i: usize) {
283 let w = i / 64;
284 if w >= self.0.len() {
285 self.0.resize(w + 1, 0);
286 }
287 self.0[w] ^= 1u64 << (i % 64);
288 }
289 fn get(&self, i: usize) -> bool {
290 let w = i / 64;
291 w < self.0.len() && (self.0[w] >> (i % 64)) & 1 == 1
292 }
293 fn is_zero(&self) -> bool {
294 self.0.iter().all(|&w| w == 0)
295 }
296 fn lowest(&self) -> Option<usize> {
298 self.0.iter().enumerate().find(|(_, &w)| w != 0).map(|(wi, &w)| wi * 64 + w.trailing_zeros() as usize)
299 }
300}
301
302impl std::ops::BitXorAssign<&Bits> for Bits {
303 fn bitxor_assign(&mut self, o: &Bits) {
304 if o.0.len() > self.0.len() {
305 self.0.resize(o.0.len(), 0);
306 }
307 for (a, b) in self.0.iter_mut().zip(&o.0) {
308 *a ^= b;
309 }
310 }
311}
312
313fn z2_nullspace(cols: &[Bits]) -> Vec<Bits> {
316 let mut pivots: std::collections::HashMap<usize, (Bits, Bits)> = std::collections::HashMap::new();
317 let mut kernel = Vec::new();
318 for (j, col) in cols.iter().enumerate() {
319 let (mut cod, mut trk) = (col.clone(), Bits::bit(j));
320 while let Some(lead) = cod.lowest() {
321 match pivots.get(&lead) {
322 Some((pcod, ptrk)) => {
323 cod ^= pcod;
324 trk ^= ptrk;
325 }
326 None => break,
327 }
328 }
329 match cod.lowest() {
330 None => kernel.push(trk),
331 Some(lead) => {
332 pivots.insert(lead, (cod, trk));
333 }
334 }
335 }
336 kernel
337}
338
339fn z2_rank_u128(rows: Vec<Bits>) -> usize {
341 let mut pivots: std::collections::HashMap<usize, Bits> = std::collections::HashMap::new();
342 for mut v in rows {
343 while let Some(lead) = v.lowest() {
344 match pivots.get(&lead) {
345 Some(p) => v ^= p,
346 None => {
347 pivots.insert(lead, v);
348 break;
349 }
350 }
351 }
352 }
353 pivots.len()
354}
355
356fn c1_basis(s: usize) -> Vec<(usize, Vec<usize>)> {
359 let mut out = Vec::new();
360 let mut i = 0;
361 while (1usize << i) <= s {
362 for m in admissibles_of_degree(s - (1 << i)) {
363 out.push((i, m));
364 }
365 i += 1;
366 }
367 out
368}
369
370fn ker_d1(s: usize) -> (Vec<(usize, Vec<usize>)>, Vec<Bits>) {
373 let basis = c1_basis(s);
374 let cod_idx: std::collections::HashMap<Vec<usize>, usize> =
375 admissibles_of_degree(s).into_iter().enumerate().map(|(i, m)| (m, i)).collect();
376 let cols: Vec<Bits> = basis
377 .iter()
378 .map(|(i, m)| {
379 let mut prod = m.clone();
380 prod.push(1 << i); let mut mask = Bits::new();
382 for adm in adem_reduce(&prod) {
383 if let Some(&idx) = cod_idx.get(&adm) {
384 mask.toggle(idx);
385 }
386 }
387 mask
388 })
389 .collect();
390 let kernel = z2_nullspace(&cols);
391 (basis, kernel)
392}
393
394pub fn adams_two_line(t: usize) -> usize {
397 let (_basis_t, ker_t) = ker_d1(t);
398 let c1_idx_t: std::collections::HashMap<(usize, Vec<usize>), usize> =
399 c1_basis(t).into_iter().enumerate().map(|(i, b)| (b, i)).collect();
400 let mut decomposables: Vec<Bits> = Vec::new();
401 for k in 1..t {
402 let (basis_s, ker_s) = ker_d1(t - k);
403 for zmask in &ker_s {
404 let mut result = Bits::new();
405 for (b, (i, m)) in basis_s.iter().enumerate() {
406 if zmask.get(b) {
407 let mut prod = vec![k];
408 prod.extend_from_slice(m);
409 for mprime in adem_reduce(&prod) {
410 if let Some(&idx) = c1_idx_t.get(&(*i, mprime)) {
411 result.toggle(idx);
412 }
413 }
414 }
415 }
416 if !result.is_zero() {
417 decomposables.push(result);
418 }
419 }
420 }
421 ker_t.len() - z2_rank_u128(decomposables)
422}
423
424fn known_ext2(t: usize) -> usize {
427 let mut count = 0;
428 let mut i = 0;
429 while (1usize << i) <= t {
430 let mut j = i;
431 while (1usize << i) + (1usize << j) <= t {
432 if (1 << i) + (1 << j) == t && j != i + 1 {
433 count += 1;
434 }
435 j += 1;
436 }
437 i += 1;
438 }
439 count
440}
441
442#[derive(Clone)]
445struct ResGen {
446 degree: usize,
447 boundary: Vec<(usize, Vec<usize>)>,
448}
449
450trait Algebra {
454 fn basis(&self, degree: usize) -> Vec<Vec<usize>>;
457 fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>>;
459 fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)>;
464}
465
466struct SteenrodAlgebra;
468impl Algebra for SteenrodAlgebra {
469 fn basis(&self, degree: usize) -> Vec<Vec<usize>> {
470 admissibles_of_degree(degree)
471 }
472 fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>> {
473 let mut prod = a.to_vec();
474 prod.extend_from_slice(b);
475 reduce_unit(&prod)
476 }
477 fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)> {
478 let mut out = Vec::new();
479 let mut d = 1usize;
480 while d <= max_degree {
481 out.push((d, vec![d])); d <<= 1;
483 }
484 out
485 }
486}
487
488type Combo = std::collections::BTreeSet<Vec<usize>>;
491
492fn combo_mul(a: &Combo, b: &Combo) -> Combo {
494 let mut out = Combo::new();
495 for ma in a {
496 for mb in b {
497 let mut prod = ma.clone();
498 prod.extend_from_slice(mb);
499 for r in reduce_unit(&prod) {
500 if !out.insert(r.clone()) {
501 out.remove(&r);
502 }
503 }
504 }
505 }
506 out
507}
508
509type PivotIndex = std::collections::HashMap<Vec<usize>, usize>;
512
513fn express_in_basis(target: &Combo, basis: &[Combo], pivots: &PivotIndex) -> Vec<usize> {
517 let mut r = target.clone();
518 let mut used = Vec::new();
519 while let Some(piv) = r.iter().next_back().cloned() {
520 match pivots.get(&piv) {
521 Some(&idx) => {
522 used.push(idx);
523 for m in &basis[idx] {
524 if !r.insert(m.clone()) {
525 r.remove(m);
526 }
527 }
528 }
529 None => break, }
531 }
532 used.sort_unstable();
533 used
534}
535
536struct SubAlgebra {
541 by_degree: Vec<Vec<Combo>>,
542 pivots: Vec<PivotIndex>,
543 gen_keys: Vec<(usize, Vec<usize>)>,
544}
545
546impl SubAlgebra {
547 fn dimension(&self) -> usize {
548 self.by_degree.iter().map(|v| v.len()).sum()
549 }
550}
551
552fn reduce_combo(basis: &[Combo], pivots: &PivotIndex, c: &Combo) -> Combo {
555 let mut r = c.clone();
556 while let Some(piv) = r.iter().next_back().cloned() {
557 match pivots.get(&piv) {
558 Some(&idx) => {
559 for m in &basis[idx] {
560 if !r.insert(m.clone()) {
561 r.remove(m);
562 }
563 }
564 }
565 None => break,
566 }
567 }
568 r
569}
570
571fn subalg_add(by_degree: &mut [Vec<Combo>], pivots: &mut [PivotIndex], worklist: &mut Vec<Combo>, max_degree: usize, c: Combo) {
574 let d = match c.iter().next() {
575 Some(m) => m.iter().sum::<usize>(),
576 None => return,
577 };
578 if d > max_degree {
579 return;
580 }
581 let r = reduce_combo(&by_degree[d], &pivots[d], &c);
582 if let Some(piv) = r.iter().next_back().cloned() {
583 pivots[d].insert(piv, by_degree[d].len());
584 by_degree[d].push(r.clone());
585 worklist.push(r);
586 }
587}
588
589fn build_subalgebra(gens: &[Vec<usize>], max_degree: usize) -> SubAlgebra {
592 let unit: Combo = std::iter::once(vec![]).collect();
593 let gen_combos: Vec<Combo> = gens.iter().map(|g| std::iter::once(g.clone()).collect()).collect();
594 let mut by_degree: Vec<Vec<Combo>> = vec![Vec::new(); max_degree + 1];
595 let mut pivots: Vec<PivotIndex> = vec![PivotIndex::new(); max_degree + 1];
596 let mut worklist: Vec<Combo> = Vec::new();
597 subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, unit);
601 for g in &gen_combos {
602 subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, g.clone());
603 }
604 while let Some(b) = worklist.pop() {
605 for g in &gen_combos {
606 let p = combo_mul(&b, g);
607 subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, p);
608 }
609 }
610 let gen_keys: Vec<(usize, Vec<usize>)> = gens
613 .iter()
614 .map(|g| {
615 let d: usize = g.iter().sum();
616 (d, vec![d, pivots[d][g]])
617 })
618 .collect();
619 SubAlgebra { by_degree, pivots, gen_keys }
620}
621
622impl Algebra for SubAlgebra {
623 fn basis(&self, degree: usize) -> Vec<Vec<usize>> {
624 (0..self.by_degree.get(degree).map_or(0, |v| v.len())).map(|i| vec![degree, i]).collect()
625 }
626 fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>> {
627 let prod = combo_mul(&self.by_degree[a[0]][a[1]], &self.by_degree[b[0]][b[1]]);
628 let pd = a[0] + b[0];
629 match self.by_degree.get(pd) {
630 Some(basis) if !prod.is_empty() => {
631 express_in_basis(&prod, basis, &self.pivots[pd]).into_iter().map(|i| vec![pd, i]).collect()
632 }
633 _ => std::collections::HashSet::new(),
634 }
635 }
636 fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)> {
637 self.gen_keys.iter().filter(|(d, _)| *d <= max_degree).cloned().collect()
638 }
639}
640
641fn act_boundary_over(alg: &dyn Algebra, m: &[usize], boundary: &[(usize, Vec<usize>)]) -> std::collections::HashSet<(usize, Vec<usize>)> {
643 let mut acc: std::collections::HashSet<(usize, Vec<usize>)> = std::collections::HashSet::new();
644 for (g, a) in boundary {
645 for ap in alg.multiply(m, a) {
646 let key = (*g, ap);
647 if !acc.insert(key.clone()) {
648 acc.remove(&key);
649 }
650 }
651 }
652 acc
653}
654
655fn module_basis_over(alg: &dyn Algebra, gens: &[ResGen], t: usize) -> Vec<(usize, Vec<usize>)> {
658 let mut out = Vec::new();
659 for (gi, g) in gens.iter().enumerate() {
660 if g.degree <= t {
661 for m in alg.basis(t - g.degree) {
662 out.push((gi, m));
663 }
664 }
665 }
666 out
667}
668
669fn act_boundary(m: &[usize], boundary: &[(usize, Vec<usize>)]) -> std::collections::HashSet<(usize, Vec<usize>)> {
671 act_boundary_over(&SteenrodAlgebra, m, boundary)
672}
673
674fn module_basis(gens: &[ResGen], t: usize) -> Vec<(usize, Vec<usize>)> {
676 module_basis_over(&SteenrodAlgebra, gens, t)
677}
678
679fn minimal_resolution(max_s: usize, max_t: usize) -> Vec<Vec<ResGen>> {
682 minimal_resolution_over(&SteenrodAlgebra, max_s, max_t)
683}
684
685fn minimal_resolution_over(alg: &dyn Algebra, max_s: usize, max_t: usize) -> Vec<Vec<ResGen>> {
690 let mut res: Vec<Vec<ResGen>> = vec![vec![ResGen { degree: 0, boundary: vec![] }]]; for s in 1..=max_s {
692 let mut new_gens: Vec<ResGen> = Vec::new();
693 for t in 1..=max_t {
694 let prev_basis = module_basis_over(alg, &res[s - 1], t);
696 let prev_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
697 prev_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
698
699 let kernel: Vec<Bits> = if s == 1 {
702 (0..prev_basis.len()).map(Bits::bit).collect()
703 } else {
704 let cc_basis = module_basis_over(alg, &res[s - 2], t);
705 let cc_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
706 cc_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
707 let cols: Vec<Bits> = prev_basis
708 .iter()
709 .map(|(gi, m)| {
710 let img = act_boundary_over(alg, m, &res[s - 1][*gi].boundary);
711 Bits::from_indices(img.into_iter().map(|e| cc_idx[&e]))
712 })
713 .collect();
714 z2_nullspace(&cols)
715 };
716
717 let mut decomp: Vec<Bits> = Vec::new();
721 for (gdeg, gkey) in alg.generators(t - 1) {
722 let lower = kernel_at_over(alg, &res, s - 1, t - gdeg);
723 let lower_basis = module_basis_over(alg, &res[s - 1], t - gdeg);
724 for z in &lower.0 {
725 let mut v = Bits::new();
726 for (b, (gi, m)) in lower_basis.iter().enumerate() {
727 if z.get(b) {
728 for mp in alg.multiply(&gkey, m) {
729 if let Some(&idx) = prev_idx.get(&(*gi, mp)) {
730 v.toggle(idx);
731 }
732 }
733 }
734 }
735 if !v.is_zero() {
736 decomp.push(v);
737 }
738 }
739 }
740
741 let mut pivots: std::collections::HashMap<usize, Bits> = std::collections::HashMap::new();
743 for mut d in decomp {
744 while let Some(lead) = d.lowest() {
745 match pivots.get(&lead) {
746 Some(p) => d ^= p,
747 None => {
748 pivots.insert(lead, d);
749 break;
750 }
751 }
752 }
753 }
754 for kv in &kernel {
755 let mut r = kv.clone();
756 while let Some(lead) = r.lowest() {
757 match pivots.get(&lead) {
758 Some(p) => r ^= p,
759 None => break,
760 }
761 }
762 if let Some(lead) = r.lowest() {
763 pivots.insert(lead, r);
764 let boundary: Vec<(usize, Vec<usize>)> =
765 (0..prev_basis.len()).filter(|&b| kv.get(b)).map(|b| prev_basis[b].clone()).collect();
766 new_gens.push(ResGen { degree: t, boundary });
767 }
768 }
769 }
770 res.push(new_gens);
771 }
772 res
773}
774
775fn kernel_at(res: &[Vec<ResGen>], s: usize, t: usize) -> (Vec<Bits>, ()) {
778 kernel_at_over(&SteenrodAlgebra, res, s, t)
779}
780
781fn kernel_at_over(alg: &dyn Algebra, res: &[Vec<ResGen>], s: usize, t: usize) -> (Vec<Bits>, ()) {
783 let basis = module_basis_over(alg, &res[s], t);
784 if s == 0 {
785 return ((0..basis.len()).map(Bits::bit).collect(), ());
786 }
787 let cc_basis = module_basis_over(alg, &res[s - 1], t);
788 let cc_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
789 cc_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
790 let cols: Vec<Bits> = basis
791 .iter()
792 .map(|(gi, m)| Bits::from_indices(act_boundary_over(alg, m, &res[s][*gi].boundary).into_iter().map(|e| cc_idx[&e])))
793 .collect();
794 (z2_nullspace(&cols), ())
795}
796
797pub fn ext(max_s: usize, max_t: usize) -> Vec<Vec<usize>> {
799 ext_over(&SteenrodAlgebra, max_s, max_t)
800}
801
802fn ext_over(alg: &dyn Algebra, max_s: usize, max_t: usize) -> Vec<Vec<usize>> {
805 let res = minimal_resolution_over(alg, max_s, max_t);
806 (0..=max_s)
807 .map(|s| (0..=max_t).map(|t| res[s].iter().filter(|g| g.degree == t).count()).collect())
808 .collect()
809}
810
811fn h0_targets(res: &[Vec<ResGen>], s: usize, gi: usize) -> Vec<usize> {
815 if s + 1 >= res.len() {
816 return vec![];
817 }
818 res[s + 1]
819 .iter()
820 .enumerate()
821 .filter(|(_, g)| g.boundary.iter().any(|(j, m)| *j == gi && m.as_slice() == [1]))
822 .map(|(idx, _)| idx)
823 .collect()
824}
825
826fn stem_2local_tower_lengths(res: &[Vec<ResGen>], n: usize) -> Vec<usize> {
830 let in_stem = |s: usize, gi: usize| s < res.len() && gi < res[s].len() && res[s][gi].degree == n + s;
831 let mut is_target: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
833 for s in 0..res.len() {
834 for gi in 0..res[s].len() {
835 if in_stem(s, gi) {
836 for t in h0_targets(res, s, gi) {
837 if in_stem(s + 1, t) {
838 is_target.insert((s + 1, t));
839 }
840 }
841 }
842 }
843 }
844 let mut lengths = Vec::new();
845 for s in 0..res.len() {
846 for gi in 0..res[s].len() {
847 if in_stem(s, gi) && !is_target.contains(&(s, gi)) {
848 let mut len = 1;
850 let (mut cs, mut cgi) = (s, gi);
851 loop {
852 let ups: Vec<usize> = h0_targets(res, cs, cgi).into_iter().filter(|&t| in_stem(cs + 1, t)).collect();
853 if let Some(&t) = ups.first() {
854 len += 1;
855 cs += 1;
856 cgi = t;
857 } else {
858 break;
859 }
860 }
861 lengths.push(len);
862 }
863 }
864 }
865 lengths.sort_unstable();
866 lengths
867}
868
869pub fn act_monomial(m: &[usize], poly: &[u8]) -> Vec<u8> {
871 let mut p = poly.to_vec();
872 for &i in m.iter().rev() {
873 p = apply_sq(i, &p);
874 }
875 trim(p)
876}
877
878fn reduce_unit(m: &[usize]) -> std::collections::HashSet<Vec<usize>> {
882 let filtered: Vec<usize> = m.iter().copied().filter(|&i| i > 0).collect();
883 if filtered.is_empty() {
884 return std::iter::once(vec![]).collect();
885 }
886 adem_reduce(&filtered)
887}
888
889fn coproduct(m: &[usize]) -> std::collections::HashSet<(Vec<usize>, Vec<usize>)> {
894 let mut acc: std::collections::HashSet<(Vec<usize>, Vec<usize>)> =
895 std::iter::once((vec![], vec![])).collect(); for &n in m {
897 let mut next: std::collections::HashSet<(Vec<usize>, Vec<usize>)> = std::collections::HashSet::new();
898 for (lacc, racc) in &acc {
899 for a in 0..=n {
900 let (mut ll, mut rr) = (lacc.clone(), racc.clone());
901 if a > 0 {
902 ll.push(a);
903 }
904 if n - a > 0 {
905 rr.push(n - a);
906 }
907 for la in reduce_unit(&ll) {
908 for ra in reduce_unit(&rr) {
909 let key = (la.clone(), ra);
910 if !next.insert(key.clone()) {
911 next.remove(&key);
912 }
913 }
914 }
915 }
916 }
917 acc = next;
918 }
919 acc
920}
921
922type Chain = std::collections::HashSet<(usize, Vec<usize>)>;
925
926fn chain_add(a: &mut Chain, b: Chain) {
928 for e in b {
929 if !a.insert(e.clone()) {
930 a.remove(&e);
931 }
932 }
933}
934
935fn act_chain(m: &[usize], elem: &Chain) -> Chain {
937 let v: Vec<(usize, Vec<usize>)> = elem.iter().cloned().collect();
938 act_boundary(m, &v)
939}
940
941fn solve_boundary(res: &[Vec<ResGen>], s: usize, deg: usize, target: &Chain) -> Option<Chain> {
946 let src_basis = module_basis(&res[s], deg);
947 let tgt_basis = module_basis(&res[s - 1], deg);
948 let tgt_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
949 tgt_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
950 let mask = |c: &Chain| -> Bits { Bits::from_indices(c.iter().map(|e| tgt_idx[e])) };
951 let cols: Vec<Bits> = src_basis
952 .iter()
953 .map(|(gi, m)| mask(&act_boundary(m, &res[s][*gi].boundary)))
954 .collect();
955 let mut elim: Vec<(usize, Bits, Bits)> = Vec::new(); for (j, c) in cols.iter().enumerate() {
958 let (mut r, mut src) = (c.clone(), Bits::bit(j));
959 for (piv, rc, sm) in &elim {
960 if r.get(*piv) {
961 r ^= rc;
962 src ^= sm;
963 }
964 }
965 if let Some(lead) = r.lowest() {
966 elim.push((lead, r, src));
967 }
968 }
969 let (mut tr, mut tsrc) = (mask(target), Bits::new());
970 for (piv, rc, sm) in &elim {
971 if tr.get(*piv) {
972 tr ^= rc;
973 tsrc ^= sm;
974 }
975 }
976 if !tr.is_zero() {
977 return None;
978 }
979 Some((0..src_basis.len()).filter(|&j| tsrc.get(j)).map(|j| src_basis[j].clone()).collect())
980}
981
982pub fn yoneda_product(res: &[Vec<ResGen>], a_s: usize, a_gi: usize, b_s: usize, b_gi: usize) -> Vec<usize> {
989 let t_a = res[a_s][a_gi].degree;
990 let t_b = res[b_s][b_gi].degree;
991 let mut phi: Vec<std::collections::HashMap<usize, Chain>> = Vec::new();
993 let mut p0 = std::collections::HashMap::new();
994 for i in 0..res[a_s].len() {
995 let mut c = Chain::new();
996 if i == a_gi {
997 c.insert((0, vec![]));
998 }
999 p0.insert(i, c);
1000 }
1001 phi.push(p0);
1002 for k in 1..=b_s {
1004 let mut pk: std::collections::HashMap<usize, Chain> = std::collections::HashMap::new();
1005 for i in 0..res[a_s + k].len() {
1006 let d_i = res[a_s + k][i].degree;
1007 if d_i < t_a {
1008 pk.insert(i, Chain::new()); continue;
1010 }
1011 let mut r = Chain::new();
1013 for (j, m) in &res[a_s + k][i].boundary {
1014 chain_add(&mut r, act_chain(m, &phi[k - 1][j]));
1015 }
1016 let x = if r.is_empty() {
1017 Chain::new()
1018 } else {
1019 solve_boundary(res, k, d_i - t_a, &r).expect("minimal resolution is exact: ∂x = r is solvable")
1020 };
1021 pk.insert(i, x);
1022 }
1023 phi.push(pk);
1024 }
1025 (0..res[a_s + b_s].len())
1027 .filter(|&i| res[a_s + b_s][i].degree == t_a + t_b && phi[b_s][&i].contains(&(b_gi, vec![])))
1028 .collect()
1029}
1030
1031type TensorTerm = (usize, usize, Vec<usize>, usize, usize, Vec<usize>);
1034type Tensor = std::collections::HashSet<TensorTerm>;
1035
1036fn tensor_add(a: &mut Tensor, b: Tensor) {
1038 for e in b {
1039 if !a.insert(e.clone()) {
1040 a.remove(&e);
1041 }
1042 }
1043}
1044
1045fn tensor_degree(res: &[Vec<ResGen>], t: &Tensor) -> Option<usize> {
1047 t.iter().next().map(|(p, i, m1, q, j, m2)| {
1048 res[*p][*i].degree + m1.iter().sum::<usize>() + res[*q][*j].degree + m2.iter().sum::<usize>()
1049 })
1050}
1051
1052fn tensor_boundary(res: &[Vec<ResGen>], elem: &Tensor) -> Tensor {
1054 let mut out = Tensor::new();
1055 for (p, i, m1, q, j, m2) in elem {
1056 if *p >= 1 {
1057 for (i2, m1b) in act_boundary(m1, &res[*p][*i].boundary) {
1058 let term = (*p - 1, i2, m1b, *q, *j, m2.clone());
1059 if !out.insert(term.clone()) {
1060 out.remove(&term);
1061 }
1062 }
1063 }
1064 if *q >= 1 {
1065 for (j2, m2b) in act_boundary(m2, &res[*q][*j].boundary) {
1066 let term = (*p, *i, m1.clone(), *q - 1, j2, m2b);
1067 if !out.insert(term.clone()) {
1068 out.remove(&term);
1069 }
1070 }
1071 }
1072 }
1073 out
1074}
1075
1076fn act_tensor(m: &[usize], elem: &Tensor) -> Tensor {
1079 let psi = coproduct(m);
1080 let mut out = Tensor::new();
1081 for (p, i, m1, q, j, m2) in elem {
1082 for (ml, mr) in &psi {
1083 let (mut left, mut right) = (ml.clone(), mr.clone());
1084 left.extend_from_slice(m1);
1085 right.extend_from_slice(m2);
1086 for a in reduce_unit(&left) {
1087 for b in reduce_unit(&right) {
1088 let term = (*p, *i, a.clone(), *q, *j, b);
1089 if !out.insert(term.clone()) {
1090 out.remove(&term);
1091 }
1092 }
1093 }
1094 }
1095 }
1096 out
1097}
1098
1099fn tensor_basis(res: &[Vec<ResGen>], n: usize, t: usize) -> Vec<TensorTerm> {
1101 let mut out = Vec::new();
1102 for p in 0..=n {
1103 let q = n - p;
1104 if p >= res.len() || q >= res.len() {
1105 continue;
1106 }
1107 for tl in 0..=t {
1108 let tr = t - tl;
1109 for (i, m1) in module_basis(&res[p], tl) {
1110 for (j, m2) in module_basis(&res[q], tr) {
1111 out.push((p, i, m1.clone(), q, j, m2.clone()));
1112 }
1113 }
1114 }
1115 }
1116 out
1117}
1118
1119fn solve_tensor_boundary(res: &[Vec<ResGen>], n: usize, target: &Tensor) -> Option<Tensor> {
1123 fn reduce(mut col: Tensor, mut src: Tensor, elim: &[(TensorTerm, Tensor, Tensor)]) -> (Tensor, Tensor) {
1124 loop {
1125 let piv = match col.iter().max() {
1126 Some(p) => p.clone(),
1127 None => return (col, src),
1128 };
1129 match elim.iter().find(|(p, _, _)| *p == piv) {
1130 Some((_, rc, sm)) => {
1131 for x in rc {
1132 if !col.insert(x.clone()) {
1133 col.remove(x);
1134 }
1135 }
1136 for x in sm {
1137 if !src.insert(x.clone()) {
1138 src.remove(x);
1139 }
1140 }
1141 }
1142 None => return (col, src),
1143 }
1144 }
1145 }
1146 let t = match tensor_degree(res, target) {
1147 Some(t) => t,
1148 None => return Some(Tensor::new()),
1149 };
1150 let mut elim: Vec<(TensorTerm, Tensor, Tensor)> = Vec::new(); for b in tensor_basis(res, n, t) {
1152 let col = tensor_boundary(res, &std::iter::once(b.clone()).collect());
1153 let src: Tensor = std::iter::once(b).collect();
1154 let (rc, sm) = reduce(col, src, &elim);
1155 if let Some(piv) = rc.iter().max().cloned() {
1156 elim.push((piv, rc, sm));
1157 }
1158 }
1159 let (rc, sm) = reduce(target.clone(), Tensor::new(), &elim);
1160 rc.is_empty().then_some(sm)
1161}
1162
1163fn diagonal(res: &[Vec<ResGen>], max_n: usize) -> Vec<std::collections::HashMap<usize, Tensor>> {
1168 let mut delta: Vec<std::collections::HashMap<usize, Tensor>> = Vec::new();
1169 let mut d0 = std::collections::HashMap::new();
1170 d0.insert(0usize, std::iter::once((0, 0, vec![], 0, 0, vec![])).collect::<Tensor>());
1171 delta.push(d0);
1172 for n in 1..=max_n {
1173 let mut dn: std::collections::HashMap<usize, Tensor> = std::collections::HashMap::new();
1174 for k in 0..res[n].len() {
1175 let mut rhs = Tensor::new(); for (j, mb) in &res[n][k].boundary {
1177 tensor_add(&mut rhs, act_tensor(mb, &delta[n - 1][j]));
1178 }
1179 let x = solve_tensor_boundary(res, n, &rhs).expect("diagonal lifts: ∂x = Δ₀(∂g) is solvable");
1180 dn.insert(k, x);
1181 }
1182 delta.push(dn);
1183 }
1184 delta
1185}
1186
1187fn tensor_swap(t: &Tensor) -> Tensor {
1189 t.iter().map(|(p, i, m1, q, j, m2)| (*q, *j, m2.clone(), *p, *i, m1.clone())).collect()
1190}
1191
1192fn cup_homotopy(
1198 res: &[Vec<ResGen>],
1199 max_n: usize,
1200 shift: usize,
1201 prev: &[std::collections::HashMap<usize, Tensor>],
1202) -> Vec<std::collections::HashMap<usize, Tensor>> {
1203 let mut d: Vec<std::collections::HashMap<usize, Tensor>> = Vec::new();
1204 d.push(std::iter::once((0usize, Tensor::new())).collect()); for n in 1..=max_n {
1206 let mut dn: std::collections::HashMap<usize, Tensor> = std::collections::HashMap::new();
1207 for k in 0..res[n].len() {
1208 let mut rhs = prev[n][&k].clone(); tensor_add(&mut rhs, tensor_swap(&prev[n][&k]));
1210 for (j, mb) in &res[n][k].boundary {
1211 tensor_add(&mut rhs, act_tensor(mb, &d[n - 1][j]));
1212 }
1213 let x = solve_tensor_boundary(res, n + shift, &rhs).expect("cup-i homotopy lifts: ∂x = Δ+TΔ+Δ∂");
1214 dn.insert(k, x);
1215 }
1216 d.push(dn);
1217 }
1218 d
1219}
1220
1221fn diagonal_1(
1224 res: &[Vec<ResGen>],
1225 max_n: usize,
1226 delta: &[std::collections::HashMap<usize, Tensor>],
1227) -> Vec<std::collections::HashMap<usize, Tensor>> {
1228 cup_homotopy(res, max_n, 1, delta)
1229}
1230
1231fn sq0_on_h(res: &[Vec<ResGen>], delta1: &[std::collections::HashMap<usize, Tensor>], gi: usize) -> Vec<usize> {
1235 let want = 2 * res[1][gi].degree;
1236 (0..res[1].len())
1237 .filter(|&k| res[1][k].degree == want && delta1[1][&k].contains(&(1, gi, vec![], 1, gi, vec![])))
1238 .collect()
1239}
1240
1241fn sq0_on_ext2(res: &[Vec<ResGen>], delta2: &[std::collections::HashMap<usize, Tensor>], gx: usize) -> Vec<usize> {
1245 let want = 2 * res[2][gx].degree;
1246 (0..res[2].len())
1247 .filter(|&k| res[2][k].degree == want && delta2[2][&k].contains(&(2, gx, vec![], 2, gx, vec![])))
1248 .collect()
1249}
1250
1251fn cup_diagonals(
1254 res: &[Vec<ResGen>],
1255 max_shift: usize,
1256 max_n: usize,
1257) -> Vec<Vec<std::collections::HashMap<usize, Tensor>>> {
1258 let mut out = vec![diagonal(res, max_n)];
1259 for i in 1..=max_shift {
1260 let prev = out[i - 1].clone();
1261 out.push(cup_homotopy(res, max_n, i, &prev));
1262 }
1263 out
1264}
1265
1266fn sq0(res: &[Vec<ResGen>], deltas: &[Vec<std::collections::HashMap<usize, Tensor>>], s: usize, gx: usize) -> Vec<usize> {
1272 let want = 2 * res[s][gx].degree;
1273 let ds = &deltas[s]; (0..res[s].len())
1275 .filter(|&k| res[s][k].degree == want && ds[s][&k].contains(&(s, gx, vec![], s, gx, vec![])))
1276 .collect()
1277}
1278
1279#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
1282pub enum AdamsFact {
1283 Dimension { s: usize, t: usize, dim: usize },
1285 Product { lhs: String, rhs: String, result: String },
1287 Vanishing { lhs: String, rhs: String },
1289 Relation { a: String, b: String },
1291 Doubling { from: String, to: String },
1293 StableGroup { stem: usize, tower_lengths: Vec<usize>, truncated: bool },
1297 Secondary { s: usize, t: usize },
1301}
1302
1303impl std::fmt::Display for AdamsFact {
1304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1305 match self {
1306 AdamsFact::Dimension { s, t, dim } => write!(f, "dim Ext^{{{s},{t}}} = {dim}"),
1307 AdamsFact::Product { lhs, rhs, result } => write!(f, "{lhs} · {rhs} = {result}"),
1308 AdamsFact::Vanishing { lhs, rhs } => write!(f, "{lhs} · {rhs} = 0"),
1309 AdamsFact::Relation { a, b } => write!(f, "{a} = {b}"),
1310 AdamsFact::Doubling { from, to } => write!(f, "Sq⁰({from}) = {to}"),
1311 AdamsFact::StableGroup { stem, tower_lengths, truncated } => {
1312 let group = if tower_lengths.is_empty() {
1313 "0".to_string()
1314 } else {
1315 tower_lengths.iter().map(|l| format!("Z/2^{l}")).collect::<Vec<_>>().join(" ⊕ ")
1316 };
1317 if *truncated {
1318 write!(f, "π_{stem}^s ⊗ Z₍₂₎ ⊇ {group} (h₀-tower hits the resolution ceiling — infinite/truncated)")
1319 } else {
1320 write!(f, "π_{stem}^s ⊗ Z₍₂₎ = {group}")
1321 }
1322 }
1323 AdamsFact::Secondary { s, t } => {
1324 write!(f, "Ext^{{{s},{t}}} has a non-product (secondary/Massey) class — un-auto-able by the product sweep")
1325 }
1326 }
1327 }
1328}
1329
1330fn name_by_products(
1336 res: &[Vec<ResGen>],
1337 max_s: usize,
1338 max_t: usize,
1339) -> (std::collections::HashMap<(usize, usize), String>, Vec<AdamsFact>, Vec<(usize, usize)>) {
1340 let mut names: std::collections::HashMap<(usize, usize), String> = std::collections::HashMap::new();
1341 let mut facts: Vec<AdamsFact> = Vec::new();
1342 let mut secondary: Vec<(usize, usize)> = Vec::new();
1343 let mut worklist: Vec<(usize, usize)> = Vec::new();
1344 for gi in 0..res[1].len() {
1345 let i = res[1][gi].degree.trailing_zeros();
1346 names.insert((1, gi), format!("h{i}"));
1347 worklist.push((1, gi));
1348 }
1349 worklist.sort_by_key(|&(_, gi)| res[1][gi].degree);
1350 let h_gens: Vec<usize> = (0..res[1].len()).collect();
1351
1352 let mut qi = 0;
1358 let mut sec = 0;
1359 loop {
1360 while qi < worklist.len() {
1361 let (s, gi) = worklist[qi];
1362 qi += 1;
1363 let lname = names[&(s, gi)].clone();
1364 for &hgi in &h_gens {
1365 if s + 1 > max_s || res[s][gi].degree + res[1][hgi].degree > max_t {
1366 continue;
1367 }
1368 let hname = names[&(1, hgi)].clone();
1369 let prod = yoneda_product(res, s, gi, 1, hgi);
1370 if prod.is_empty() {
1371 facts.push(AdamsFact::Vanishing { lhs: lname.clone(), rhs: hname });
1372 } else if prod.len() == 1 {
1373 let key = (s + 1, prod[0]);
1374 let newname = format!("{lname}{hname}");
1375 match names.get(&key) {
1376 Some(existing) if *existing != newname => {
1377 let (a, b) = if newname < *existing { (newname, existing.clone()) } else { (existing.clone(), newname) };
1378 facts.push(AdamsFact::Relation { a, b });
1379 }
1380 Some(_) => {}
1381 None => {
1382 names.insert(key, newname.clone());
1383 worklist.push(key);
1384 facts.push(AdamsFact::Product { lhs: lname.clone(), rhs: hname, result: newname });
1385 }
1386 }
1387 }
1388 }
1389 }
1390 let next = (1..=max_s)
1391 .flat_map(|s| (0..res[s].len()).map(move |gi| (s, gi)))
1392 .filter(|&(s, gi)| res[s][gi].degree <= max_t && !names.contains_key(&(s, gi)))
1393 .min_by_key(|&(s, gi)| (res[s][gi].degree, s));
1394 match next {
1395 Some((s, gi)) => {
1396 names.insert((s, gi), format!("c{sec}"));
1397 sec += 1;
1398 secondary.push((s, gi));
1399 worklist.push((s, gi));
1400 }
1401 None => break,
1402 }
1403 }
1404 (names, facts, secondary)
1405}
1406
1407fn secondary_classes(max_s: usize, max_t: usize) -> Vec<(usize, usize)> {
1412 let res = minimal_resolution(max_s, max_t);
1413 let (_names, _facts, secondary) = name_by_products(&res, max_s, max_t);
1414 let mut out: Vec<(usize, usize)> = secondary.iter().map(|&(s, gi)| (s, res[s][gi].degree)).collect();
1415 out.sort();
1416 out.dedup();
1417 out
1418}
1419
1420pub fn harvest_secondary_facts(max_s: usize, max_t: usize) -> Vec<AdamsFact> {
1425 let res = minimal_resolution(max_s, max_t);
1426 let (_names, ring_facts, secondary) = name_by_products(&res, max_s, max_t);
1427 let mut facts = ring_facts;
1428 for s in 0..=max_s {
1429 for t in 0..=max_t {
1430 let dim = res[s].iter().filter(|g| g.degree == t).count();
1431 if dim > 0 {
1432 facts.push(AdamsFact::Dimension { s, t, dim });
1433 }
1434 }
1435 }
1436 for &(s, gi) in &secondary {
1437 facts.push(AdamsFact::Secondary { s, t: res[s][gi].degree });
1438 }
1439 for stem in 0..=max_t.saturating_sub(max_s) {
1440 let lengths = stem_2local_tower_lengths(&res, stem);
1441 if !lengths.is_empty() {
1442 let truncated = res[max_s].iter().any(|g| g.degree == stem + max_s);
1443 facts.push(AdamsFact::StableGroup { stem, tower_lengths: lengths, truncated });
1444 }
1445 }
1446 facts.sort();
1447 facts.dedup();
1448 facts
1449}
1450
1451pub fn harvest_adams_facts(max_s: usize, max_t: usize) -> Vec<AdamsFact> {
1458 let res = minimal_resolution(max_s, max_t);
1459 let dbl_shift = max_s.min(2);
1460 let deltas = cup_diagonals(&res, dbl_shift, dbl_shift);
1461 let mut facts: Vec<AdamsFact> = Vec::new();
1462
1463 for s in 0..=max_s {
1465 for t in 0..=max_t {
1466 let dim = res[s].iter().filter(|g| g.degree == t).count();
1467 if dim > 0 {
1468 facts.push(AdamsFact::Dimension { s, t, dim });
1469 }
1470 }
1471 }
1472
1473 let (names, ring_facts, secondary) = name_by_products(&res, max_s, max_t);
1476 facts.extend(ring_facts);
1477 for &(s, gi) in &secondary {
1478 facts.push(AdamsFact::Secondary { s, t: res[s][gi].degree });
1479 }
1480
1481 let mut named: Vec<((usize, usize), String)> = names.iter().map(|(k, v)| (*k, v.clone())).collect();
1483 named.sort();
1484 for ((s, gi), name) in &named {
1485 if *s == 0 || *s > dbl_shift || 2 * res[*s][*gi].degree > max_t {
1486 continue;
1487 }
1488 let img = sq0(&res, &deltas, *s, *gi);
1489 if img.len() == 1 {
1490 let to = names.get(&(*s, img[0])).cloned().unwrap_or_else(|| format!("Sq⁰({name})"));
1491 facts.push(AdamsFact::Doubling { from: name.clone(), to });
1492 }
1493 }
1494
1495 for stem in 0..=max_t.saturating_sub(max_s) {
1499 let lengths = stem_2local_tower_lengths(&res, stem);
1500 if !lengths.is_empty() {
1501 let truncated = res[max_s].iter().any(|g| g.degree == stem + max_s);
1502 facts.push(AdamsFact::StableGroup { stem, tower_lengths: lengths, truncated });
1503 }
1504 }
1505
1506 facts.sort();
1507 facts.dedup();
1508 facts
1509}
1510
1511#[cfg(test)]
1512mod tests {
1513 use super::*;
1514
1515 #[test]
1516 fn adem_reduces_known_relations_to_the_admissible_basis() {
1517 assert!(adem_reduce(&[1, 1]).is_empty(), "Sq¹Sq¹ = 0");
1520 assert_eq!(adem_reduce(&[1, 2]), [vec![3]].into_iter().collect(), "Sq¹Sq² = Sq³");
1521 assert_eq!(adem_reduce(&[2, 2]), [vec![3, 1]].into_iter().collect(), "Sq²Sq² = Sq³Sq¹");
1522 for m in [vec![3, 1], vec![2, 1], vec![5, 2, 1]] {
1523 for adm in &adem_reduce(&m) {
1524 assert!(is_admissible(adm), "reduction output is admissible");
1525 }
1526 }
1527 }
1528
1529 #[test]
1530 fn two_primary_group_structure_from_the_h0_tower_filtration() {
1531 let res = super::minimal_resolution(8, 18);
1534 let g = |n| super::stem_2local_tower_lengths(&res, n);
1535 assert_eq!(g(1), vec![1], "π₁ˢ = Z/2");
1536 assert_eq!(g(2), vec![1], "π₂ˢ = Z/2");
1537 assert_eq!(g(3), vec![3], "π₃ˢ: ONE h₀-tower height 3 = Z/8 (not (Z/2)³) ⇒ Z/24");
1538 assert_eq!(g(6), vec![1], "π₆ˢ = Z/2");
1539 assert_eq!(g(7), vec![4], "π₇ˢ = Z/16 ⇒ Z/240");
1540 assert_eq!(g(8), vec![1, 1], "π₈ˢ = (Z/2)² — two separate towers");
1541 assert_eq!(g(9), vec![1, 1, 1], "π₉ˢ = (Z/2)³");
1542 assert_eq!(g(11), vec![3], "π₁₁ˢ = Z/8 ⇒ Z/504");
1543 }
1544
1545 #[test]
1546 fn stable_homotopy_groups_in_stems_eight_through_thirteen() {
1547 let e = ext(7, 18);
1550 let st = |n: usize| -> usize { (0..=7).filter(|&s| n + s <= 18).map(|s| e[s][n + s]).sum() };
1551 assert_eq!(st(8), 2, "π₈ˢ = (Z/2)²");
1552 assert_eq!(st(9), 3, "π₉ˢ = (Z/2)³");
1553 assert_eq!(st(10), 1, "π₁₀ˢ = Z/2 (2-local of Z/6)");
1554 assert_eq!(st(11), 3, "π₁₁ˢ = Z/8 2-locally (Z/504)");
1555 assert_eq!(st(12), 0, "π₁₂ˢ = 0");
1556 assert_eq!(st(13), 0, "π₁₃ˢ = 0 (2-locally; Z/3)");
1557 }
1558
1559 #[test]
1560 fn infinite_h0_tower_and_indecomposable_line() {
1561 let e = ext(8, 8);
1569 for s in 0..=8 {
1570 assert_eq!(e[s][s], 1, "h₀ˢ ≠ 0 — the infinite stem-0 tower (π₀ˢ = Z) at s={s}");
1571 }
1572 for t in 1..=16 {
1573 assert_eq!(adams_one_line(t), usize::from(t.is_power_of_two()), "the h_i line, forever, at t={t}");
1574 }
1575 }
1576
1577 #[test]
1578 fn stable_homotopy_groups_through_the_seven_stem() {
1579 let e = ext(6, 12);
1585 let stem_total = |n: usize| -> usize { (0..=6).filter(|&s| n + s <= 12).map(|s| e[s][n + s]).sum() };
1586 assert_eq!(stem_total(1), 1, "π₁ˢ = Z/2");
1587 assert_eq!(stem_total(2), 1, "π₂ˢ = Z/2");
1588 assert_eq!(stem_total(3), 3, "π₃ˢ = Z/24 (2-local Z/8)");
1589 assert_eq!(stem_total(4), 0, "π₄ˢ = 0");
1590 assert_eq!(stem_total(5), 0, "π₅ˢ = 0");
1591 assert_eq!(stem_total(6), 1, "π₆ˢ = Z/2");
1592 assert_eq!(stem_total(7), 4, "π₇ˢ = Z/240 (2-local Z/16)");
1593 }
1594
1595 #[test]
1596 fn minimal_resolution_recovers_ext_through_the_three_stem() {
1597 let e = ext(5, 8);
1601 for t in 1..=8 {
1602 assert_eq!(e[1][t], adams_one_line(t), "Ext¹ from the engine matches the 1-line at t={t}");
1603 }
1604 for t in 2..=8 {
1605 assert_eq!(e[2][t], adams_two_line(t), "Ext² from the engine matches the 2-line at t={t}");
1606 }
1607 for s in 0..=5 {
1609 assert_eq!(e[s][s], 1, "h₀ˢ tower at filtration {s} (stem 0 = Z₂)");
1610 }
1611 assert_eq!(e[1][4], 1, "h₂");
1613 assert_eq!(e[2][5], 1, "h₀h₂");
1614 assert_eq!(e[3][6], 1, "h₀²h₂ — tower height 3");
1615 assert_eq!(e[4][7], 0, "h₀³h₂ = 0 ⇒ tower ends ⇒ π₃ˢ = Z/24 (2-locally Z/8)");
1616 }
1617
1618 #[test]
1619 fn low_stem_homotopy_from_the_e2_lines() {
1620 assert_eq!(adams_one_line(2), 1, "h₁ generates stem 1");
1626 assert_eq!(adams_two_line(3), 0, "h₀h₁ = 0 — the stem-1 h₀-tower is dead, so π₁ˢ = Z/2");
1627 assert_eq!(adams_two_line(4), 1, "h₁² generates stem 2 ⇒ π₂ˢ = Z/2");
1628 }
1629
1630 #[test]
1631 fn the_adams_e2_two_line_matches_the_h_i_h_j_chart() {
1632 for t in 2..=16 {
1637 assert_eq!(adams_two_line(t), known_ext2(t), "dim Ext^{{2,t}} = #(h_i h_j) at t={t}");
1638 }
1639 }
1640
1641 #[test]
1642 fn the_adams_e2_one_line_is_exactly_the_h_i_indecomposables() {
1643 for t in 1..=16 {
1649 assert_eq!(
1650 adams_one_line(t),
1651 usize::from(t.is_power_of_two()),
1652 "dim Ext^{{1,t}} = 1 iff t is a power of 2 (the h_i) at t={t}"
1653 );
1654 }
1655 }
1656
1657 #[test]
1658 fn sq_n_is_decomposable_iff_n_is_not_a_power_of_two_hopf_invariant_one() {
1659 for n in 2..=17 {
1665 assert_eq!(
1666 is_decomposable_sq(n),
1667 !n.is_power_of_two(),
1668 "Sqⁿ decomposable ⟺ n not a power of 2 (indecomposables = Sq^{{2ⁱ}}) at n={n}"
1669 );
1670 }
1671 }
1672
1673 #[test]
1674 fn adem_reduction_preserves_the_action_on_bz2() {
1675 let monomials = [vec![1, 1], vec![1, 2], vec![2, 2], vec![3, 2], vec![2, 3], vec![1, 2, 1], vec![4, 2]];
1679 for m in monomials {
1680 let reduced = adem_reduce(&m);
1681 for k in 0..=12 {
1682 let xk = monomial(k);
1683 let direct = act_monomial(&m, &xk);
1684 let mut via_basis = vec![0u8];
1685 for adm in &reduced {
1686 via_basis = {
1687 let a = act_monomial(adm, &xk);
1688 let n = via_basis.len().max(a.len());
1689 (0..n).map(|i| via_basis.get(i).copied().unwrap_or(0) ^ a.get(i).copied().unwrap_or(0)).collect()
1690 };
1691 }
1692 assert_eq!(direct, trim(via_basis), "monomial {m:?} acts as its admissible reduction at k={k}");
1693 }
1694 }
1695 }
1696
1697 #[test]
1698 fn adem_relations_hold_on_bz2() {
1699 for k in 0..=12 {
1703 let xk = monomial(k);
1704 assert_eq!(apply_sq(1, &apply_sq(1, &xk)), vec![0u8], "Adem Sq¹Sq¹ = 0 at k={k}");
1705 assert_eq!(apply_sq(1, &apply_sq(2, &xk)), apply_sq(3, &xk), "Adem Sq¹Sq² = Sq³ at k={k}");
1706 assert_eq!(
1707 apply_sq(2, &apply_sq(2, &xk)),
1708 apply_sq(3, &apply_sq(1, &xk)),
1709 "Adem Sq²Sq² = Sq³Sq¹ at k={k}"
1710 );
1711 }
1712 }
1713
1714 #[test]
1715 fn the_steenrod_action_on_bz2_is_binomial_coefficients_mod_2() {
1716 for k in 0..=10 {
1720 for i in 0..=k {
1721 assert_eq!(sq(i, k), (binom(k, i) % 2) as u8, "Sqⁱ(xᵏ) = C(k,i) mod 2");
1722 }
1723 }
1724 }
1725
1726 #[test]
1727 fn binomial_mod_2_is_lucas_theorem_bitwise() {
1728 for k in 0..=15usize {
1731 for i in 0..=k {
1732 let lucas = u8::from((i & k) == i);
1733 assert_eq!((binom(k, i) % 2) as u8, lucas, "C(k,i) mod 2 = [i AND k == i]");
1734 }
1735 }
1736 }
1737
1738 #[test]
1739 fn steenrod_axioms_identity_top_square_and_instability() {
1740 for k in 1..=8 {
1744 assert_eq!(sq(0, k), 1, "Sq⁰(xᵏ) = xᵏ");
1745 assert_eq!(sq(k, k), 1, "Sqᵏ(xᵏ) = x^2k — the top square is the cup-square");
1746 assert_eq!(sq(k + 1, k), 0, "Sqⁱ(xᵏ) = 0 for i > k (instability)");
1747 }
1748 }
1749
1750 #[test]
1751 fn the_total_square_obeys_the_cartan_formula_ring_homomorphism() {
1752 for i in 0..=5 {
1755 for j in 0..=5 {
1756 let lhs = total_square(&monomial(i + j));
1757 let rhs = trim(mul(&total_square(&monomial(i)), &total_square(&monomial(j))));
1758 assert_eq!(lhs, rhs, "Cartan: Sq(xⁱ⁺ʲ) = Sq(xⁱ)·Sq(xʲ)");
1759 }
1760 }
1761 }
1762}
1763
1764#[cfg(test)]
1765mod adams_differential_onset {
1766 use super::*;
1767
1768 #[test]
1776 fn e2_strictly_exceeds_einfinity_at_the_fourteen_stem() {
1777 let e = ext(8, 24);
1778 let st = |n: usize| -> usize { (0..=8).filter(|&s| n + s <= 24).map(|s| e[s][n + s]).sum() };
1779 assert_eq!(st(13), 0, "13-stem: E₂ = E∞, both trivial 2-primarily");
1780 assert_eq!(st(14), 5, "Σ_s dim Ext^{{s,14+s}} = 5 on the E₂ page");
1781 assert_ne!(st(14), 2, "rank E₂ > rank E∞ at the 14-stem: d₂(h₄) = h₀h₃² acts here");
1782 }
1783}
1784
1785#[cfg(test)]
1786mod ext_ring_from_the_resolution {
1787 use super::*;
1788
1789 #[test]
1802 fn the_h_i_products_obey_the_known_ext_ring_relations() {
1803 let res = minimal_resolution(4, 18);
1804 let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1805 let prod = |a_s, a, b_s, b| yoneda_product(&res, a_s, a, b_s, b);
1806
1807 for i in 0..=3 {
1808 assert!(!prod(1, h(i), 1, h(i)).is_empty(), "cup-square h_{i}² is nonzero");
1809 }
1810 assert_eq!(prod(1, h(0), 1, h(0)).len(), 1, "h_0² spans the 1-dim Ext^{{2,2}}");
1811
1812 assert!(!prod(1, h(0), 1, h(2)).is_empty(), "h_0 h_2 ≠ 0");
1813 assert!(!prod(1, h(0), 1, h(3)).is_empty(), "h_0 h_3 ≠ 0");
1814 assert!(!prod(1, h(1), 1, h(3)).is_empty(), "h_1 h_3 ≠ 0");
1815
1816 assert!(prod(1, h(0), 1, h(1)).is_empty(), "h_0 h_1 = 0 (Adem adjacency)");
1817 assert!(prod(1, h(1), 1, h(2)).is_empty(), "h_1 h_2 = 0 (Adem adjacency)");
1818 assert!(prod(1, h(2), 1, h(3)).is_empty(), "h_2 h_3 = 0 (Adem adjacency)");
1819
1820 assert_eq!(prod(1, h(0), 1, h(2)), prod(1, h(2), 1, h(0)), "commutativity h_0 h_2 = h_2 h_0");
1821 assert_eq!(prod(1, h(1), 1, h(3)), prod(1, h(3), 1, h(1)), "commutativity h_1 h_3 = h_3 h_1");
1822
1823 let h1sq = prod(1, h(1), 1, h(1));
1826 let h0sq = prod(1, h(0), 1, h(0));
1827 assert_eq!(h1sq.len(), 1, "h_1² spans Ext^{{2,4}}");
1828 assert_eq!(h0sq.len(), 1, "h_0² spans Ext^{{2,2}}");
1829 let h1_cubed = yoneda_product(&res, 2, h1sq[0], 1, h(1));
1830 let h0sq_h2 = yoneda_product(&res, 2, h0sq[0], 1, h(2));
1831 assert!(!h1_cubed.is_empty(), "h_1³ ≠ 0");
1832 assert_eq!(h1_cubed, h0sq_h2, "h_1³ = h_0² h_2 — derived, not assumed");
1833 }
1834}
1835
1836#[cfg(test)]
1837mod steenrod_coproduct {
1838 use super::*;
1839 use std::collections::HashSet;
1840
1841 fn set(pairs: &[(&[usize], &[usize])]) -> HashSet<(Vec<usize>, Vec<usize>)> {
1842 pairs.iter().map(|(l, r)| (l.to_vec(), r.to_vec())).collect()
1843 }
1844
1845 #[test]
1851 fn the_cartan_coproduct_is_diagonal_cocommutative_and_adem_reduced() {
1852 assert_eq!(coproduct(&[1]), set(&[(&[], &[1]), (&[1], &[])]));
1853 assert_eq!(coproduct(&[2]), set(&[(&[2], &[]), (&[1], &[1]), (&[], &[2])]));
1854 assert_eq!(coproduct(&[3]), set(&[(&[3], &[]), (&[2], &[1]), (&[1], &[2]), (&[], &[3])]));
1855
1856 assert_eq!(
1858 coproduct(&[2, 1]),
1859 set(&[(&[2, 1], &[]), (&[2], &[1]), (&[1], &[2]), (&[], &[2, 1])])
1860 );
1861
1862 for m in [vec![1], vec![2], vec![3], vec![4], vec![2, 1], vec![4, 2, 1], vec![5], vec![6, 3]] {
1863 let psi = coproduct(&m);
1864 let swapped: HashSet<_> = psi.iter().map(|(l, r)| (r.clone(), l.clone())).collect();
1865 assert_eq!(psi, swapped, "ψ is cocommutative on {m:?}");
1866 let deg = |x: &[usize]| -> usize { x.iter().sum() };
1867 for (l, r) in &psi {
1868 assert_eq!(deg(l) + deg(r), deg(&m), "ψ preserves internal degree leg-wise on {m:?}");
1869 }
1870 }
1871 }
1872}
1873
1874#[cfg(test)]
1875mod chain_level_diagonal {
1876 use super::*;
1877
1878 #[test]
1884 fn the_chain_level_diagonal_reproduces_the_yoneda_cup_squares() {
1885 let res = minimal_resolution(2, 8);
1886 let delta = diagonal(&res, 2);
1887 let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1888 for i in 0..=2 {
1889 let gi = h(i);
1890 let want = 2 * (1 << i);
1891 let mut via_diagonal: Vec<usize> = (0..res[2].len())
1892 .filter(|&k| {
1893 res[2][k].degree == want && delta[2][&k].contains(&(1, gi, vec![], 1, gi, vec![]))
1894 })
1895 .collect();
1896 via_diagonal.sort_unstable();
1897 let mut via_yoneda = yoneda_product(&res, 1, gi, 1, gi);
1898 via_yoneda.sort_unstable();
1899 assert!(!via_diagonal.is_empty(), "h_{i}² ≠ 0");
1900 assert_eq!(via_diagonal, via_yoneda, "Δ₀ reproduces the Yoneda cup-square h_{i}²");
1901 }
1902 }
1903
1904 #[test]
1909 fn the_cup_1_homotopy_derives_the_sq0_doubling_on_the_h_line() {
1910 let res = minimal_resolution(2, 8);
1911 let delta = diagonal(&res, 1);
1912 let delta1 = diagonal_1(&res, 1, &delta);
1913 let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1914 for i in 0..=2 {
1915 assert_eq!(
1916 sq0_on_h(&res, &delta1, h(i)),
1917 vec![h(i + 1)],
1918 "Sq⁰(h_{i}) = h_(i+1) — the doubling, derived from the cup-1 homotopy"
1919 );
1920 }
1921 }
1922
1923 #[test]
1929 fn the_sq0_doubling_is_a_ring_endomorphism_on_the_squares() {
1930 let res = minimal_resolution(4, 8);
1931 let delta = diagonal(&res, 2);
1932 let delta1 = diagonal_1(&res, 2, &delta);
1933 let delta2 = cup_homotopy(&res, 2, 2, &delta1);
1934 let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1935 for i in 0..=1 {
1936 let hi_sq = yoneda_product(&res, 1, h(i), 1, h(i));
1937 assert_eq!(hi_sq.len(), 1, "h_i² is a single Ext² generator");
1938 let mut lhs = sq0_on_ext2(&res, &delta2, hi_sq[0]);
1939 lhs.sort_unstable();
1940 let mut rhs = yoneda_product(&res, 1, h(i + 1), 1, h(i + 1));
1941 rhs.sort_unstable();
1942 assert!(!rhs.is_empty(), "h_(i+1)² ≠ 0");
1943 assert_eq!(lhs, rhs, "Sq⁰(h_i²) = h_(i+1)² — the doubling is a ring endomorphism");
1944 }
1945 }
1946
1947 #[test]
1953 fn the_sq0_doubling_automates_across_the_low_adams_chart() {
1954 let res = minimal_resolution(4, 10);
1955 let deltas = cup_diagonals(&res, 2, 2);
1956 let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
1957 let mut harvested: Vec<String> = Vec::new();
1958
1959 for i in 0..=2 {
1960 assert_eq!(sq0(&res, &deltas, 1, h(i)), vec![h(i + 1)], "Sq⁰(h_i) = h_(i+1)");
1961 harvested.push(format!("Sq0(h{i})=h{}", i + 1));
1962 }
1963
1964 for i in 0..=2 {
1965 for j in i..=2 {
1966 let prod = yoneda_product(&res, 1, h(i), 1, h(j));
1967 if prod.len() != 1 || 2 * res[2][prod[0]].degree > 10 {
1968 continue;
1969 }
1970 let mut got = sq0(&res, &deltas, 2, prod[0]);
1971 got.sort_unstable();
1972 let mut want = yoneda_product(&res, 1, h(i + 1), 1, h(j + 1));
1973 want.sort_unstable();
1974 assert_eq!(got, want, "Sq⁰(h_i h_j) = h_(i+1) h_(j+1) — harvested automatically");
1975 harvested.push(format!("Sq0(h{i}h{j})=h{}h{}", i + 1, j + 1));
1976 }
1977 }
1978
1979 assert!(harvested.iter().any(|s| s == "Sq0(h0h2)=h1h3"), "sweep reaches the mixed product h_0h_2 ↦ h_1h_3");
1980 assert!(harvested.len() >= 6, "doubling harvested ≥6 chart facts automatically: {harvested:?}");
1981 }
1982}
1983
1984#[cfg(test)]
1985mod adams_fact_engine {
1986 use super::*;
1987
1988 #[test]
1997 fn the_engine_self_ignites_and_auto_collects_the_adams_chart() {
1998 let facts = harvest_adams_facts(4, 10);
1999
2000 let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
2001 assert!(has("h0h0h2 = h1h1h1"), "the ring relation h_1³ = h_0²h_2 was auto-discovered by collision");
2002 assert!(has("Sq⁰(h0) = h1"), "the doubling h0 ↦ h1 was auto-collected");
2003 assert!(has("Sq⁰(h0h2) = h1h3"), "the mixed-product doubling was auto-collected");
2004 assert!(has("h0 · h2 = h0h2"), "the product h_0·h_2 was auto-collected");
2005 assert!(has("h0 · h1 = 0"), "the Adem-adjacency vanishing h_0·h_1 = 0 was auto-collected");
2006 assert!(has("π_3^s ⊗ Z₍₂₎ = Z/2^3"), "the exact 2-local stem π_3 = Z/8 was auto-collected");
2007 assert!(
2008 facts.iter().any(|f| matches!(f, AdamsFact::StableGroup { stem: 0, truncated: true, .. })),
2009 "π_0 = Z₍₂₎ is honestly flagged as a truncated/infinite h₀-tower, not a false finite group"
2010 );
2011
2012 let relations = facts.iter().filter(|f| matches!(f, AdamsFact::Relation { .. })).count();
2013 let doublings = facts.iter().filter(|f| matches!(f, AdamsFact::Doubling { .. })).count();
2014 assert!(relations >= 1 && doublings >= 5, "engine nets ≥1 relation and ≥5 doublings");
2015 assert!(facts.len() >= 30, "engine auto-collected ≥30 facts from one ignition: {}", facts.len());
2016
2017 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
2019 if std::fs::create_dir_all(&dir).is_ok() {
2020 let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
2021 let _ = std::fs::write(dir.join("adams_chart.txt"), body);
2022 }
2023 }
2024
2025 #[test]
2029 fn the_harvested_catalog_is_frozen_as_ground_truth() {
2030 let got: Vec<String> = harvest_adams_facts(4, 10).iter().map(|f| f.to_string()).collect();
2031 let expected = "\
2032dim Ext^{0,0} = 1
2033dim Ext^{1,1} = 1
2034dim Ext^{1,2} = 1
2035dim Ext^{1,4} = 1
2036dim Ext^{1,8} = 1
2037dim Ext^{2,2} = 1
2038dim Ext^{2,4} = 1
2039dim Ext^{2,5} = 1
2040dim Ext^{2,8} = 1
2041dim Ext^{2,9} = 1
2042dim Ext^{2,10} = 1
2043dim Ext^{3,3} = 1
2044dim Ext^{3,6} = 1
2045dim Ext^{3,10} = 1
2046dim Ext^{4,4} = 1
2047h0 · h0 = h0h0
2048h0 · h2 = h0h2
2049h0 · h3 = h0h3
2050h0h0 · h0 = h0h0h0
2051h0h0 · h2 = h0h0h2
2052h0h0 · h3 = h0h0h3
2053h0h0h0 · h0 = h0h0h0h0
2054h1 · h1 = h1h1
2055h1 · h3 = h1h3
2056h2 · h2 = h2h2
2057h0 · h1 = 0
2058h0h0 · h1 = 0
2059h0h0h0 · h1 = 0
2060h0h0h0 · h2 = 0
2061h0h0h2 · h0 = 0
2062h0h0h2 · h1 = 0
2063h0h0h2 · h2 = 0
2064h0h2 · h1 = 0
2065h0h2 · h2 = 0
2066h1 · h0 = 0
2067h1 · h2 = 0
2068h1h1 · h0 = 0
2069h1h1 · h2 = 0
2070h2 · h1 = 0
2071h2h2 · h0 = 0
2072h2h2 · h1 = 0
2073h0h0h2 = h0h2h0
2074h0h0h2 = h1h1h1
2075h0h0h3 = h0h3h0
2076h0h2 = h2h0
2077h0h3 = h3h0
2078h1h3 = h3h1
2079Sq⁰(h0) = h1
2080Sq⁰(h0h0) = h1h1
2081Sq⁰(h0h2) = h1h3
2082Sq⁰(h1) = h2
2083Sq⁰(h1h1) = h2h2
2084Sq⁰(h2) = h3
2085π_0^s ⊗ Z₍₂₎ ⊇ Z/2^5 (h₀-tower hits the resolution ceiling — infinite/truncated)
2086π_1^s ⊗ Z₍₂₎ = Z/2^1
2087π_2^s ⊗ Z₍₂₎ = Z/2^1
2088π_3^s ⊗ Z₍₂₎ = Z/2^3
2089π_6^s ⊗ Z₍₂₎ = Z/2^1";
2090 assert_eq!(got.join("\n"), expected, "the banked Adams-chart catalog drifted from ground truth");
2091 }
2092
2093 #[test]
2098 fn the_first_un_auto_able_class_is_c0_at_ext_3_11() {
2099 let secondary = secondary_classes(6, 12);
2100 assert!(!secondary.is_empty(), "extending the range must expose un-auto-able classes");
2101 assert_eq!(secondary[0], (3, 11), "the first non-product class is c₀ at Ext^{{3,11}}: {secondary:?}");
2102 assert!(secondary_classes(4, 10).is_empty(), "no non-product classes appear within the (4,10) window");
2104 }
2105
2106 #[test]
2111 fn the_engine_auto_collects_the_wide_frontier_including_the_secondary_classes() {
2112 let facts = harvest_secondary_facts(6, 13);
2113 let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
2114
2115 assert!(
2117 has("Ext^{3,11} has a non-product (secondary/Massey) class — un-auto-able by the product sweep"),
2118 "c₀ at Ext^{{3,11}} auto-collected as a Secondary fact"
2119 );
2120 let secondary_facts: Vec<&AdamsFact> = facts.iter().filter(|f| matches!(f, AdamsFact::Secondary { .. })).collect();
2121 assert!(!secondary_facts.is_empty(), "the frontier auto-collected ≥1 secondary class");
2122 assert!(matches!(secondary_facts[0], AdamsFact::Secondary { s: 3, t: 11 }), "the first secondary class is c₀ at Ext^{{3,11}}");
2123
2124 assert!(has("c0 · h1 = c0h1"), "the product symmetry broke into the c₀-family automatically");
2128 assert!(has("c0 · h0 = 0"), "h₀·c₀ = 0 auto-derived by the verified product, not proven by hand");
2129
2130 assert!(has("h0h0h2 = h1h1h1"), "the ring relation is still auto-collected at the wide range");
2132 assert!(has("π_3^s ⊗ Z₍₂₎ = Z/2^3"), "π_3 = Z/8 auto-collected");
2133 assert!(facts.len() >= 60, "wide auto-collection gathered ≥60 facts: {}", facts.len());
2134
2135 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
2136 if std::fs::create_dir_all(&dir).is_ok() {
2137 let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
2138 let _ = std::fs::write(dir.join("adams_frontier.txt"), body);
2139 }
2140 }
2141
2142 #[test]
2148 fn the_engine_breaks_the_secondary_families_out_of_the_shadows() {
2149 let secondary = secondary_classes(9, 18);
2150 for bidegree in [(3, 11), (5, 14), (5, 16), (4, 18)] {
2151 assert!(secondary.contains(&bidegree), "detected the secondary generator at Ext^{bidegree:?}: {secondary:?}");
2152 }
2153
2154 let facts = harvest_secondary_facts(9, 18);
2155 let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
2156
2157 assert!(has("c1 · h1 = c1h1"), "the c1 family auto-sweeps");
2159 assert!(has("c2 · h0 = c2h0"), "the c2 family auto-sweeps");
2160 assert!(has("c1h1h1 = c2h0h0"), "a cross-family relation auto-discovered by collision");
2162 assert!(has("c0 · h0 = 0") && has("c0 · h2 = 0"), "h₀·c₀ = h₂·c₀ = 0 auto-derived");
2164
2165 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
2166 if std::fs::create_dir_all(&dir).is_ok() {
2167 let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
2168 let _ = std::fs::write(dir.join("adams_frontier_deep.txt"), body);
2169 }
2170 }
2171}
2172
2173
2174
2175
2176#[cfg(test)]
2177mod algebra_generic_engine {
2178 use super::*;
2179
2180 #[test]
2190 fn the_engine_is_algebra_generic_and_computes_hz_via_a0() {
2191 let sphere = ext_over(&SteenrodAlgebra, 6, 12);
2193 assert_eq!(sphere, ext(6, 12), "generic engine on 𝒜 reproduces the sphere");
2194 assert_eq!((sphere[1][1], sphere[1][2], sphere[1][4]), (1, 1, 1), "sphere has h_0, h_1, h_2");
2195
2196 let a0 = build_subalgebra(&[vec![1]], 1);
2198 assert_eq!(a0.dimension(), 2, "A(0) = {{1, Sq¹}} is 2-dimensional");
2199 let hz = ext_over(&a0, 12, 14);
2200 for s in 0..=12 {
2201 for t in 0..=14 {
2202 assert_eq!(hz[s][t], usize::from(t == s), "Ext_A(0)^{{{s},{t}}} = F₂[h_0] diagonal");
2203 }
2204 }
2205 assert_eq!(hz[1][2], 0, "HZ has NO h_1 — the engine distinguishes HZ from the sphere");
2206 }
2207
2208 #[test]
2216 fn the_engine_lands_ko_via_the_combination_basis_a1() {
2217 let a1 = build_subalgebra(&[vec![1], vec![2]], 6);
2218 assert_eq!(a1.dimension(), 8, "A(1) is 8-dimensional (the combination basis sees it correctly)");
2219
2220 let ko = ext_over(&a1, 12, 16);
2221 assert_eq!(ko[0][0], 1, "Ext^{{0,0}} = Z/2");
2222 assert_eq!(ko[1][1], 1, "h_0 = [Sq¹]");
2223 assert_eq!(ko[1][2], 1, "h_1 = [Sq²]");
2224 assert_eq!(ko[1][4], 0, "ko has NO h_2 — distinguishes ko from the sphere");
2225 assert_eq!(ko[1][8], 0, "ko has NO h_3");
2226 for t in 3..=16 {
2227 assert_eq!(ko[1][t], 0, "the only A(1)-indecomposables are Sq¹, Sq² (t={t})");
2228 }
2229 for s in 0..=12 {
2230 assert_eq!(ko[s][s], 1, "the infinite h_0-tower: π_0(ko)=Z at (s,s)={s}");
2231 }
2232 }
2233
2234 #[test]
2242 fn the_engine_lands_tmf_via_the_combination_basis_a2() {
2243 let a2 = build_subalgebra(&[vec![1], vec![2], vec![4]], 23);
2244 assert_eq!(a2.dimension(), 64, "A(2) is 64-dimensional");
2245
2246 let tmf = ext_over(&a2, 10, 12);
2247 assert_eq!(tmf[0][0], 1, "Ext^{{0,0}} = Z/2");
2248 assert_eq!(tmf[1][1], 1, "h_0 = [Sq¹]");
2249 assert_eq!(tmf[1][2], 1, "h_1 = [Sq²]");
2250 assert_eq!(tmf[1][4], 1, "h_2 = [Sq⁴] — tmf HAS h_2 (unlike ko)");
2251 assert_eq!(tmf[1][8], 0, "tmf has NO h_3 — distinguishes tmf from the sphere");
2252 for t in 3..=12 {
2253 if t != 4 {
2254 assert_eq!(tmf[1][t], 0, "only h_0,h_1,h_2 on the tmf one-line (t={t})");
2255 }
2256 }
2257 for s in 0..=10 {
2258 assert_eq!(tmf[s][s], 1, "the infinite h_0-tower: π_0(tmf)=Z at (s,s)={s}");
2259 }
2260 }
2261
2262 #[test]
2267 fn the_engine_lands_a3_height_three_dim_1024() {
2268 let a3 = build_subalgebra(&[vec![1], vec![2], vec![4], vec![8]], 72);
2269 assert_eq!(a3.dimension(), 1024, "A(3) is 1024-dimensional");
2270
2271 let e = ext_over(&a3, 8, 16);
2272 assert_eq!((e[1][1], e[1][2], e[1][4], e[1][8]), (1, 1, 1, 1), "one-line h_0,h_1,h_2,h_3 (degrees 1,2,4,8)");
2273 assert_eq!(e[1][16], 0, "A(3) has NO h_4 — distinguishes it from the sphere");
2274 for t in 3..=15 {
2275 if ![4, 8].contains(&t) {
2276 assert_eq!(e[1][t], 0, "only h_0..h_3 on the A(3) one-line (t={t})");
2277 }
2278 }
2279 for s in 0..=8 {
2280 assert_eq!(e[s][s], 1, "the infinite h_0-tower: π_0 = Z at (s,s)={s}");
2281 }
2282 }
2283
2284 #[test]
2290 #[ignore = "heavy (~15s): A(4) resolution to degree 32 over a 633-dim algebra — the height-4 crush, on demand"]
2291 fn the_engine_lands_a4_height_four() {
2292 let a4 = build_subalgebra(&[vec![1], vec![2], vec![4], vec![8], vec![16]], 34);
2293 let e = ext_over(&a4, 6, 32);
2294 assert_eq!((e[1][1], e[1][2], e[1][4], e[1][8], e[1][16]), (1, 1, 1, 1, 1), "one-line h_0..h_4 at degrees 1,2,4,8,16");
2295 assert_eq!(e[1][32], 0, "A(4) has NO h_5 — it carries h_4 (unlike A(3)) and stops there");
2296 for s in 0..=6 {
2297 assert_eq!(e[s][s], 1, "the infinite h_0-tower: π_0 = Z at (s,s)={s}");
2298 }
2299 }
2300}
2301
2302#[cfg(test)]
2303mod adams_differential_family {
2304 use super::*;
2305
2306 #[test]
2318 fn the_first_adams_differential_is_the_sq0_orbit_of_one_geometric_seed() {
2319 let is_dr = |r: usize, src: (usize, usize), tgt: (usize, usize)| tgt.0 == src.0 + r && tgt.1 == src.1 + r - 1;
2320 let hi = |i: usize| (1usize, 1usize << i); let h0_hi_sq = |i: usize| (3usize, 1 + 2 * (1usize << i)); assert!(is_dr(2, hi(4), h0_hi_sq(3)), "seed d₂(h₄)=h₀h₃² is a real d₂");
2325 assert_eq!((hi(4).1 - hi(4).0, h0_hi_sq(3).1 - h0_hi_sq(3).0), (15, 14), "h₄ (stem 15) ↦ h₀h₃² (stem 14)");
2326
2327 for i in 3..=20 {
2329 assert!(is_dr(2, hi(i + 1), h0_hi_sq(i)), "d₂(h_{{i+1}}) = h₀h_i² is a real d₂ (i={i})");
2330 }
2331
2332 let res = minimal_resolution(2, 16);
2335 let delta = diagonal(&res, 1);
2336 let delta1 = diagonal_1(&res, 1, &delta);
2337 let hgen = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2338 for i in 0..=3 {
2339 assert_eq!(sq0_on_h(&res, &delta1, hgen(i)), vec![hgen(i + 1)], "Sq⁰(h_{i}) = h_{{i+1}} (computed)");
2340 }
2341 assert_eq!(sq0_on_h(&res, &delta1, hgen(3)), vec![hgen(4)], "h₄ = Sq⁰(h₃): the seed's source IS the doubling of σ");
2342
2343 let small = minimal_resolution(4, 8);
2346 let sdelta = diagonal(&small, 2);
2347 let sdelta1 = diagonal_1(&small, 2, &sdelta);
2348 let sdelta2 = cup_homotopy(&small, 2, 2, &sdelta1);
2349 let sgen = |i: usize| small[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2350 for i in 0..=1 {
2351 let hi_sq = yoneda_product(&small, 1, sgen(i), 1, sgen(i));
2352 let hnext_sq = yoneda_product(&small, 1, sgen(i + 1), 1, sgen(i + 1));
2353 assert_eq!(sq0_on_ext2(&small, &sdelta2, hi_sq[0]), hnext_sq, "Sq⁰(h_i²)=h_{{i+1}}² drives the target doubling");
2354 }
2355 }
2356
2357 #[test]
2370 fn the_first_differential_can_only_start_at_h4_by_pure_algebra() {
2371 let res = minimal_resolution(4, 18);
2372 let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i");
2373 let h0 = h(0);
2374 let target = |i: usize| -> Vec<usize> {
2376 let sq = yoneda_product(&res, 1, h(i), 1, h(i));
2377 if sq.is_empty() { vec![] } else { yoneda_product(&res, 1, h0, 2, sq[0]) }
2378 };
2379
2380 assert!(target(2).is_empty(), "h₀h₂² = 0 ⇒ d₂(h₃) has no target ⇒ h₃ is a permanent cycle (algebra)");
2382 assert!(target(1).is_empty(), "h₀h₁² = 0 ⇒ d₂(h₂) has no target ⇒ h₂ is a permanent cycle (algebra)");
2383
2384 let h0_cubed = target(0);
2386 assert!(!h0_cubed.is_empty(), "h₀³ = h₀h₀² ≠ 0 (h₁'s would-be target exists)");
2387 let h0_fourth = yoneda_product(&res, 1, h0, 3, h0_cubed[0]);
2388 assert!(!h0_fourth.is_empty(), "h₀⁴ ≠ 0 ⇒ d₂(h₁)=h₀³ would force h₀⁴=0 — contradiction ⇒ d₂(h₁)=0");
2389 assert!(yoneda_product(&res, 1, h0, 1, h(1)).is_empty(), "h₀h₁ = 0 — the relation doing the forbidding");
2390
2391 assert!(!target(3).is_empty(), "h₀h₃² ≠ 0 ⇒ d₂(h₄) finally HAS a target ⇒ h₄ is the unique starting point");
2393 }
2394
2395 #[test]
2410 fn the_h_infinity_transgression_law_generates_the_whole_first_differential() {
2411 let cup = minimal_resolution(2, 16);
2413 let cdelta = diagonal(&cup, 1);
2414 let cdelta1 = diagonal_1(&cup, 1, &cdelta);
2415 let cg = |i: usize| cup[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2416 for i in 0..=3 {
2417 assert_eq!(sq0_on_h(&cup, &cdelta1, cg(i)), vec![cg(i + 1)], "source: Sq⁰(h_{i}) = h_{{i+1}} (derived)");
2418 }
2419
2420 let res = minimal_resolution(4, 18);
2422 let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2423 let h0 = h(0);
2424 let sq1 = |i: usize| yoneda_product(&res, 1, h(i), 1, h(i)); let d2 = |i: usize| -> Vec<usize> {
2428 let s = sq1(i);
2429 if s.is_empty() { vec![] } else { yoneda_product(&res, 1, h0, 2, s[0]) }
2430 };
2431 assert!(!sq1(3).is_empty(), "Sq¹(h₃) = h₃² ≠ 0 (the target operation is nonzero at h₃)");
2432 assert!(d2(1).is_empty(), "law ⇒ d₂(h₂) = h₀·Sq¹(h₁) = 0 ⇒ h₂ permanent");
2433 assert!(d2(2).is_empty(), "law ⇒ d₂(h₃) = h₀·Sq¹(h₂) = 0 ⇒ h₃ permanent");
2434 assert!(!d2(3).is_empty(), "law ⇒ d₂(h₄) = h₀·Sq¹(h₃) = h₀h₃² ≠ 0 ⇒ h₄ FIRES — the last bit, resolved by ONE law");
2435 }
2436
2437 #[test]
2443 fn the_transgression_law_over_the_sq0_orbit_of_h0_generates_the_whole_first_line() {
2444 let cup = minimal_resolution(2, 16);
2446 let cdelta = diagonal(&cup, 1);
2447 let cdelta1 = diagonal_1(&cup, 1, &cdelta);
2448 let cg = |i: usize| cup[1].iter().position(|g| g.degree == (1 << i)).unwrap();
2449 let mut x = cg(0); for i in 0..=3 {
2451 let next = sq0_on_h(&cup, &cdelta1, x);
2452 assert_eq!(next, vec![cg(i + 1)], "Sq⁰ⁱ⁺¹(h_0) = h_{{i+1}}: the h-line is one Sq⁰-orbit");
2453 x = next[0];
2454 }
2455
2456 let is_d2 = |src: (usize, usize), tgt: (usize, usize)| tgt.0 == src.0 + 2 && tgt.1 == src.1 + 1;
2458 for i in 0..=30 {
2459 let source = (1usize, 1usize << (i + 1)); let target = (3usize, 1 + 2 * (1usize << i)); assert!(is_d2(source, target), "law over orbit ⇒ d₂(h_{{i+1}}) = h₀h_i² is a real d₂ (i={i})");
2462 }
2463 }
2464}
2465
2466
2467