fn add(a: &[u8], b: &[u8]) -> Vec<u8> {
let n = a.len().max(b.len());
(0..n).map(|i| (a.get(i).copied().unwrap_or(0) + b.get(i).copied().unwrap_or(0)) % 2).collect()
}
fn mul(a: &[u8], b: &[u8]) -> Vec<u8> {
if a.is_empty() || b.is_empty() {
return vec![];
}
let mut out = vec![0u8; a.len() + b.len() - 1];
for (i, &ai) in a.iter().enumerate() {
if ai == 1 {
for (j, &bj) in b.iter().enumerate() {
out[i + j] = (out[i + j] + bj) % 2;
}
}
}
out
}
pub fn monomial(k: usize) -> Vec<u8> {
let mut p = vec![0u8; k + 1];
p[k] = 1;
p
}
fn trim(mut p: Vec<u8>) -> Vec<u8> {
while p.len() > 1 && *p.last().unwrap() == 0 {
p.pop();
}
p
}
pub fn total_square(poly: &[u8]) -> Vec<u8> {
let x_plus_x2 = [0u8, 1, 1]; let mut result = vec![0u8];
let mut power = vec![1u8]; for &c in poly {
if c == 1 {
result = add(&result, &power);
}
power = mul(&power, &x_plus_x2);
}
trim(result)
}
pub fn sq(i: usize, k: usize) -> u8 {
let s = total_square(&monomial(k));
s.get(k + i).copied().unwrap_or(0)
}
fn binom(k: usize, i: usize) -> u64 {
if i > k {
return 0;
}
let mut row = vec![1u64; 1];
for r in 1..=k {
let mut next = vec![1u64; r + 1];
for c in 1..r {
next[c] = row[c - 1] + row[c];
}
row = next;
}
row[i]
}
pub fn apply_sq(i: usize, poly: &[u8]) -> Vec<u8> {
let mut out = vec![0u8; poly.len() + i];
for (k, &a) in poly.iter().enumerate() {
if a == 1 && binom(k, i) % 2 == 1 {
out[k + i] ^= 1;
}
}
trim(out)
}
fn adem(a: usize, b: usize) -> Vec<Vec<usize>> {
let mut terms = Vec::new();
for c in 0..=(a / 2) {
if binom(b - c - 1, a - 2 * c) % 2 == 1 {
let mut m = vec![a + b - c];
if c > 0 {
m.push(c);
}
terms.push(m);
}
}
terms
}
pub fn is_admissible(m: &[usize]) -> bool {
m.iter().all(|&i| i >= 1) && m.windows(2).all(|w| w[0] >= 2 * w[1])
}
pub fn adem_reduce(m: &[usize]) -> std::collections::HashSet<Vec<usize>> {
let m: Vec<usize> = m.iter().copied().filter(|&i| i > 0).collect();
if is_admissible(&m) {
let mut s = std::collections::HashSet::new();
if !m.is_empty() {
s.insert(m);
}
return s;
}
let j = m.windows(2).position(|w| w[0] < 2 * w[1]).unwrap();
let (a, b) = (m[j], m[j + 1]);
let mut result: std::collections::HashSet<Vec<usize>> = std::collections::HashSet::new();
for term in adem(a, b) {
let mut spliced = m[..j].to_vec();
spliced.extend_from_slice(&term);
spliced.extend_from_slice(&m[j + 2..]);
for adm in adem_reduce(&spliced) {
if !result.insert(adm.clone()) {
result.remove(&adm); }
}
}
result
}
fn z2_span_contains(generators: Vec<std::collections::HashSet<Vec<usize>>>, target: std::collections::HashSet<Vec<usize>>) -> bool {
type Vec2 = std::collections::HashSet<Vec<usize>>;
fn reduce(mut v: Vec2, basis: &[(Vec<usize>, Vec2)]) -> Vec2 {
loop {
if v.is_empty() {
return v;
}
let piv = v.iter().max().unwrap().clone();
match basis.iter().find(|(p, _)| *p == piv) {
Some((_, bset)) => {
for x in bset {
if !v.insert(x.clone()) {
v.remove(x);
}
}
}
None => return v,
}
}
}
let mut basis: Vec<(Vec<usize>, Vec2)> = Vec::new();
for g in generators {
let r = reduce(g, &basis);
if !r.is_empty() {
let piv = r.iter().max().unwrap().clone();
basis.push((piv, r));
}
}
reduce(target, &basis).is_empty()
}
fn z2_rank(generators: Vec<std::collections::HashSet<Vec<usize>>>) -> usize {
type Vec2 = std::collections::HashSet<Vec<usize>>;
fn reduce(mut v: Vec2, basis: &[(Vec<usize>, Vec2)]) -> Vec2 {
loop {
if v.is_empty() {
return v;
}
let piv = v.iter().max().unwrap().clone();
match basis.iter().find(|(p, _)| *p == piv) {
Some((_, bset)) => {
for x in bset {
if !v.insert(x.clone()) {
v.remove(x);
}
}
}
None => return v,
}
}
}
let mut basis: Vec<(Vec<usize>, Vec2)> = Vec::new();
for g in generators {
let r = reduce(g, &basis);
if !r.is_empty() {
let piv = r.iter().max().unwrap().clone();
basis.push((piv, r));
}
}
basis.len()
}
pub fn admissibles_of_degree(t: usize) -> Vec<Vec<usize>> {
fn rec(remaining: usize, min_left: usize, suffix: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
if remaining == 0 {
out.push(suffix.clone());
return;
}
for i in min_left.max(1)..=remaining {
suffix.insert(0, i);
rec(remaining - i, 2 * i, suffix, out);
suffix.remove(0);
}
}
let mut out = Vec::new();
rec(t, 1, &mut Vec::new(), &mut out);
out
}
pub fn adams_one_line(t: usize) -> usize {
let dim_at = admissibles_of_degree(t).len();
let mut decomposables: Vec<std::collections::HashSet<Vec<usize>>> = Vec::new();
for d in 1..t {
for a in admissibles_of_degree(d) {
for b in admissibles_of_degree(t - d) {
let mut prod = a.clone();
prod.extend_from_slice(&b);
decomposables.push(adem_reduce(&prod));
}
}
}
dim_at - z2_rank(decomposables)
}
pub fn is_decomposable_sq(n: usize) -> bool {
let generators: Vec<_> = (1..n).map(|a| adem_reduce(&[a, n - a])).collect();
let target: std::collections::HashSet<Vec<usize>> = [vec![n]].into_iter().collect();
z2_span_contains(generators, target)
}
#[derive(Clone, PartialEq, Eq)]
struct Bits(Vec<u64>);
impl Bits {
fn new() -> Self {
Bits(Vec::new())
}
fn bit(i: usize) -> Self {
let mut b = Bits::new();
b.toggle(i);
b
}
fn from_indices(it: impl Iterator<Item = usize>) -> Self {
let mut b = Bits::new();
for i in it {
b.toggle(i);
}
b
}
fn toggle(&mut self, i: usize) {
let w = i / 64;
if w >= self.0.len() {
self.0.resize(w + 1, 0);
}
self.0[w] ^= 1u64 << (i % 64);
}
fn get(&self, i: usize) -> bool {
let w = i / 64;
w < self.0.len() && (self.0[w] >> (i % 64)) & 1 == 1
}
fn is_zero(&self) -> bool {
self.0.iter().all(|&w| w == 0)
}
fn lowest(&self) -> Option<usize> {
self.0.iter().enumerate().find(|(_, &w)| w != 0).map(|(wi, &w)| wi * 64 + w.trailing_zeros() as usize)
}
}
impl std::ops::BitXorAssign<&Bits> for Bits {
fn bitxor_assign(&mut self, o: &Bits) {
if o.0.len() > self.0.len() {
self.0.resize(o.0.len(), 0);
}
for (a, b) in self.0.iter_mut().zip(&o.0) {
*a ^= b;
}
}
}
fn z2_nullspace(cols: &[Bits]) -> Vec<Bits> {
let mut pivots: std::collections::HashMap<usize, (Bits, Bits)> = std::collections::HashMap::new();
let mut kernel = Vec::new();
for (j, col) in cols.iter().enumerate() {
let (mut cod, mut trk) = (col.clone(), Bits::bit(j));
while let Some(lead) = cod.lowest() {
match pivots.get(&lead) {
Some((pcod, ptrk)) => {
cod ^= pcod;
trk ^= ptrk;
}
None => break,
}
}
match cod.lowest() {
None => kernel.push(trk),
Some(lead) => {
pivots.insert(lead, (cod, trk));
}
}
}
kernel
}
fn z2_rank_u128(rows: Vec<Bits>) -> usize {
let mut pivots: std::collections::HashMap<usize, Bits> = std::collections::HashMap::new();
for mut v in rows {
while let Some(lead) = v.lowest() {
match pivots.get(&lead) {
Some(p) => v ^= p,
None => {
pivots.insert(lead, v);
break;
}
}
}
}
pivots.len()
}
fn c1_basis(s: usize) -> Vec<(usize, Vec<usize>)> {
let mut out = Vec::new();
let mut i = 0;
while (1usize << i) <= s {
for m in admissibles_of_degree(s - (1 << i)) {
out.push((i, m));
}
i += 1;
}
out
}
fn ker_d1(s: usize) -> (Vec<(usize, Vec<usize>)>, Vec<Bits>) {
let basis = c1_basis(s);
let cod_idx: std::collections::HashMap<Vec<usize>, usize> =
admissibles_of_degree(s).into_iter().enumerate().map(|(i, m)| (m, i)).collect();
let cols: Vec<Bits> = basis
.iter()
.map(|(i, m)| {
let mut prod = m.clone();
prod.push(1 << i); let mut mask = Bits::new();
for adm in adem_reduce(&prod) {
if let Some(&idx) = cod_idx.get(&adm) {
mask.toggle(idx);
}
}
mask
})
.collect();
let kernel = z2_nullspace(&cols);
(basis, kernel)
}
pub fn adams_two_line(t: usize) -> usize {
let (_basis_t, ker_t) = ker_d1(t);
let c1_idx_t: std::collections::HashMap<(usize, Vec<usize>), usize> =
c1_basis(t).into_iter().enumerate().map(|(i, b)| (b, i)).collect();
let mut decomposables: Vec<Bits> = Vec::new();
for k in 1..t {
let (basis_s, ker_s) = ker_d1(t - k);
for zmask in &ker_s {
let mut result = Bits::new();
for (b, (i, m)) in basis_s.iter().enumerate() {
if zmask.get(b) {
let mut prod = vec![k];
prod.extend_from_slice(m);
for mprime in adem_reduce(&prod) {
if let Some(&idx) = c1_idx_t.get(&(*i, mprime)) {
result.toggle(idx);
}
}
}
}
if !result.is_zero() {
decomposables.push(result);
}
}
}
ker_t.len() - z2_rank_u128(decomposables)
}
fn known_ext2(t: usize) -> usize {
let mut count = 0;
let mut i = 0;
while (1usize << i) <= t {
let mut j = i;
while (1usize << i) + (1usize << j) <= t {
if (1 << i) + (1 << j) == t && j != i + 1 {
count += 1;
}
j += 1;
}
i += 1;
}
count
}
#[derive(Clone)]
struct ResGen {
degree: usize,
boundary: Vec<(usize, Vec<usize>)>,
}
trait Algebra {
fn basis(&self, degree: usize) -> Vec<Vec<usize>>;
fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>>;
fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)>;
}
struct SteenrodAlgebra;
impl Algebra for SteenrodAlgebra {
fn basis(&self, degree: usize) -> Vec<Vec<usize>> {
admissibles_of_degree(degree)
}
fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>> {
let mut prod = a.to_vec();
prod.extend_from_slice(b);
reduce_unit(&prod)
}
fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)> {
let mut out = Vec::new();
let mut d = 1usize;
while d <= max_degree {
out.push((d, vec![d])); d <<= 1;
}
out
}
}
type Combo = std::collections::BTreeSet<Vec<usize>>;
fn combo_mul(a: &Combo, b: &Combo) -> Combo {
let mut out = Combo::new();
for ma in a {
for mb in b {
let mut prod = ma.clone();
prod.extend_from_slice(mb);
for r in reduce_unit(&prod) {
if !out.insert(r.clone()) {
out.remove(&r);
}
}
}
}
out
}
type PivotIndex = std::collections::HashMap<Vec<usize>, usize>;
fn express_in_basis(target: &Combo, basis: &[Combo], pivots: &PivotIndex) -> Vec<usize> {
let mut r = target.clone();
let mut used = Vec::new();
while let Some(piv) = r.iter().next_back().cloned() {
match pivots.get(&piv) {
Some(&idx) => {
used.push(idx);
for m in &basis[idx] {
if !r.insert(m.clone()) {
r.remove(m);
}
}
}
None => break, }
}
used.sort_unstable();
used
}
struct SubAlgebra {
by_degree: Vec<Vec<Combo>>,
pivots: Vec<PivotIndex>,
gen_keys: Vec<(usize, Vec<usize>)>,
}
impl SubAlgebra {
fn dimension(&self) -> usize {
self.by_degree.iter().map(|v| v.len()).sum()
}
}
fn reduce_combo(basis: &[Combo], pivots: &PivotIndex, c: &Combo) -> Combo {
let mut r = c.clone();
while let Some(piv) = r.iter().next_back().cloned() {
match pivots.get(&piv) {
Some(&idx) => {
for m in &basis[idx] {
if !r.insert(m.clone()) {
r.remove(m);
}
}
}
None => break,
}
}
r
}
fn subalg_add(by_degree: &mut [Vec<Combo>], pivots: &mut [PivotIndex], worklist: &mut Vec<Combo>, max_degree: usize, c: Combo) {
let d = match c.iter().next() {
Some(m) => m.iter().sum::<usize>(),
None => return,
};
if d > max_degree {
return;
}
let r = reduce_combo(&by_degree[d], &pivots[d], &c);
if let Some(piv) = r.iter().next_back().cloned() {
pivots[d].insert(piv, by_degree[d].len());
by_degree[d].push(r.clone());
worklist.push(r);
}
}
fn build_subalgebra(gens: &[Vec<usize>], max_degree: usize) -> SubAlgebra {
let unit: Combo = std::iter::once(vec![]).collect();
let gen_combos: Vec<Combo> = gens.iter().map(|g| std::iter::once(g.clone()).collect()).collect();
let mut by_degree: Vec<Vec<Combo>> = vec![Vec::new(); max_degree + 1];
let mut pivots: Vec<PivotIndex> = vec![PivotIndex::new(); max_degree + 1];
let mut worklist: Vec<Combo> = Vec::new();
subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, unit);
for g in &gen_combos {
subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, g.clone());
}
while let Some(b) = worklist.pop() {
for g in &gen_combos {
let p = combo_mul(&b, g);
subalg_add(&mut by_degree, &mut pivots, &mut worklist, max_degree, p);
}
}
let gen_keys: Vec<(usize, Vec<usize>)> = gens
.iter()
.map(|g| {
let d: usize = g.iter().sum();
(d, vec![d, pivots[d][g]])
})
.collect();
SubAlgebra { by_degree, pivots, gen_keys }
}
impl Algebra for SubAlgebra {
fn basis(&self, degree: usize) -> Vec<Vec<usize>> {
(0..self.by_degree.get(degree).map_or(0, |v| v.len())).map(|i| vec![degree, i]).collect()
}
fn multiply(&self, a: &[usize], b: &[usize]) -> std::collections::HashSet<Vec<usize>> {
let prod = combo_mul(&self.by_degree[a[0]][a[1]], &self.by_degree[b[0]][b[1]]);
let pd = a[0] + b[0];
match self.by_degree.get(pd) {
Some(basis) if !prod.is_empty() => {
express_in_basis(&prod, basis, &self.pivots[pd]).into_iter().map(|i| vec![pd, i]).collect()
}
_ => std::collections::HashSet::new(),
}
}
fn generators(&self, max_degree: usize) -> Vec<(usize, Vec<usize>)> {
self.gen_keys.iter().filter(|(d, _)| *d <= max_degree).cloned().collect()
}
}
fn act_boundary_over(alg: &dyn Algebra, m: &[usize], boundary: &[(usize, Vec<usize>)]) -> std::collections::HashSet<(usize, Vec<usize>)> {
let mut acc: std::collections::HashSet<(usize, Vec<usize>)> = std::collections::HashSet::new();
for (g, a) in boundary {
for ap in alg.multiply(m, a) {
let key = (*g, ap);
if !acc.insert(key.clone()) {
acc.remove(&key);
}
}
}
acc
}
fn module_basis_over(alg: &dyn Algebra, gens: &[ResGen], t: usize) -> Vec<(usize, Vec<usize>)> {
let mut out = Vec::new();
for (gi, g) in gens.iter().enumerate() {
if g.degree <= t {
for m in alg.basis(t - g.degree) {
out.push((gi, m));
}
}
}
out
}
fn act_boundary(m: &[usize], boundary: &[(usize, Vec<usize>)]) -> std::collections::HashSet<(usize, Vec<usize>)> {
act_boundary_over(&SteenrodAlgebra, m, boundary)
}
fn module_basis(gens: &[ResGen], t: usize) -> Vec<(usize, Vec<usize>)> {
module_basis_over(&SteenrodAlgebra, gens, t)
}
fn minimal_resolution(max_s: usize, max_t: usize) -> Vec<Vec<ResGen>> {
minimal_resolution_over(&SteenrodAlgebra, max_s, max_t)
}
fn minimal_resolution_over(alg: &dyn Algebra, max_s: usize, max_t: usize) -> Vec<Vec<ResGen>> {
let mut res: Vec<Vec<ResGen>> = vec![vec![ResGen { degree: 0, boundary: vec![] }]]; for s in 1..=max_s {
let mut new_gens: Vec<ResGen> = Vec::new();
for t in 1..=max_t {
let prev_basis = module_basis_over(alg, &res[s - 1], t);
let prev_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
prev_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
let kernel: Vec<Bits> = if s == 1 {
(0..prev_basis.len()).map(Bits::bit).collect()
} else {
let cc_basis = module_basis_over(alg, &res[s - 2], t);
let cc_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
cc_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
let cols: Vec<Bits> = prev_basis
.iter()
.map(|(gi, m)| {
let img = act_boundary_over(alg, m, &res[s - 1][*gi].boundary);
Bits::from_indices(img.into_iter().map(|e| cc_idx[&e]))
})
.collect();
z2_nullspace(&cols)
};
let mut decomp: Vec<Bits> = Vec::new();
for (gdeg, gkey) in alg.generators(t - 1) {
let lower = kernel_at_over(alg, &res, s - 1, t - gdeg);
let lower_basis = module_basis_over(alg, &res[s - 1], t - gdeg);
for z in &lower.0 {
let mut v = Bits::new();
for (b, (gi, m)) in lower_basis.iter().enumerate() {
if z.get(b) {
for mp in alg.multiply(&gkey, m) {
if let Some(&idx) = prev_idx.get(&(*gi, mp)) {
v.toggle(idx);
}
}
}
}
if !v.is_zero() {
decomp.push(v);
}
}
}
let mut pivots: std::collections::HashMap<usize, Bits> = std::collections::HashMap::new();
for mut d in decomp {
while let Some(lead) = d.lowest() {
match pivots.get(&lead) {
Some(p) => d ^= p,
None => {
pivots.insert(lead, d);
break;
}
}
}
}
for kv in &kernel {
let mut r = kv.clone();
while let Some(lead) = r.lowest() {
match pivots.get(&lead) {
Some(p) => r ^= p,
None => break,
}
}
if let Some(lead) = r.lowest() {
pivots.insert(lead, r);
let boundary: Vec<(usize, Vec<usize>)> =
(0..prev_basis.len()).filter(|&b| kv.get(b)).map(|b| prev_basis[b].clone()).collect();
new_gens.push(ResGen { degree: t, boundary });
}
}
}
res.push(new_gens);
}
res
}
fn kernel_at(res: &[Vec<ResGen>], s: usize, t: usize) -> (Vec<Bits>, ()) {
kernel_at_over(&SteenrodAlgebra, res, s, t)
}
fn kernel_at_over(alg: &dyn Algebra, res: &[Vec<ResGen>], s: usize, t: usize) -> (Vec<Bits>, ()) {
let basis = module_basis_over(alg, &res[s], t);
if s == 0 {
return ((0..basis.len()).map(Bits::bit).collect(), ());
}
let cc_basis = module_basis_over(alg, &res[s - 1], t);
let cc_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
cc_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
let cols: Vec<Bits> = basis
.iter()
.map(|(gi, m)| Bits::from_indices(act_boundary_over(alg, m, &res[s][*gi].boundary).into_iter().map(|e| cc_idx[&e])))
.collect();
(z2_nullspace(&cols), ())
}
pub fn ext(max_s: usize, max_t: usize) -> Vec<Vec<usize>> {
ext_over(&SteenrodAlgebra, max_s, max_t)
}
fn ext_over(alg: &dyn Algebra, max_s: usize, max_t: usize) -> Vec<Vec<usize>> {
let res = minimal_resolution_over(alg, max_s, max_t);
(0..=max_s)
.map(|s| (0..=max_t).map(|t| res[s].iter().filter(|g| g.degree == t).count()).collect())
.collect()
}
fn h0_targets(res: &[Vec<ResGen>], s: usize, gi: usize) -> Vec<usize> {
if s + 1 >= res.len() {
return vec![];
}
res[s + 1]
.iter()
.enumerate()
.filter(|(_, g)| g.boundary.iter().any(|(j, m)| *j == gi && m.as_slice() == [1]))
.map(|(idx, _)| idx)
.collect()
}
fn stem_2local_tower_lengths(res: &[Vec<ResGen>], n: usize) -> Vec<usize> {
let in_stem = |s: usize, gi: usize| s < res.len() && gi < res[s].len() && res[s][gi].degree == n + s;
let mut is_target: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
for s in 0..res.len() {
for gi in 0..res[s].len() {
if in_stem(s, gi) {
for t in h0_targets(res, s, gi) {
if in_stem(s + 1, t) {
is_target.insert((s + 1, t));
}
}
}
}
}
let mut lengths = Vec::new();
for s in 0..res.len() {
for gi in 0..res[s].len() {
if in_stem(s, gi) && !is_target.contains(&(s, gi)) {
let mut len = 1;
let (mut cs, mut cgi) = (s, gi);
loop {
let ups: Vec<usize> = h0_targets(res, cs, cgi).into_iter().filter(|&t| in_stem(cs + 1, t)).collect();
if let Some(&t) = ups.first() {
len += 1;
cs += 1;
cgi = t;
} else {
break;
}
}
lengths.push(len);
}
}
}
lengths.sort_unstable();
lengths
}
pub fn act_monomial(m: &[usize], poly: &[u8]) -> Vec<u8> {
let mut p = poly.to_vec();
for &i in m.iter().rev() {
p = apply_sq(i, &p);
}
trim(p)
}
fn reduce_unit(m: &[usize]) -> std::collections::HashSet<Vec<usize>> {
let filtered: Vec<usize> = m.iter().copied().filter(|&i| i > 0).collect();
if filtered.is_empty() {
return std::iter::once(vec![]).collect();
}
adem_reduce(&filtered)
}
fn coproduct(m: &[usize]) -> std::collections::HashSet<(Vec<usize>, Vec<usize>)> {
let mut acc: std::collections::HashSet<(Vec<usize>, Vec<usize>)> =
std::iter::once((vec![], vec![])).collect(); for &n in m {
let mut next: std::collections::HashSet<(Vec<usize>, Vec<usize>)> = std::collections::HashSet::new();
for (lacc, racc) in &acc {
for a in 0..=n {
let (mut ll, mut rr) = (lacc.clone(), racc.clone());
if a > 0 {
ll.push(a);
}
if n - a > 0 {
rr.push(n - a);
}
for la in reduce_unit(&ll) {
for ra in reduce_unit(&rr) {
let key = (la.clone(), ra);
if !next.insert(key.clone()) {
next.remove(&key);
}
}
}
}
}
acc = next;
}
acc
}
type Chain = std::collections::HashSet<(usize, Vec<usize>)>;
fn chain_add(a: &mut Chain, b: Chain) {
for e in b {
if !a.insert(e.clone()) {
a.remove(&e);
}
}
}
fn act_chain(m: &[usize], elem: &Chain) -> Chain {
let v: Vec<(usize, Vec<usize>)> = elem.iter().cloned().collect();
act_boundary(m, &v)
}
fn solve_boundary(res: &[Vec<ResGen>], s: usize, deg: usize, target: &Chain) -> Option<Chain> {
let src_basis = module_basis(&res[s], deg);
let tgt_basis = module_basis(&res[s - 1], deg);
let tgt_idx: std::collections::HashMap<(usize, Vec<usize>), usize> =
tgt_basis.iter().cloned().enumerate().map(|(i, b)| (b, i)).collect();
let mask = |c: &Chain| -> Bits { Bits::from_indices(c.iter().map(|e| tgt_idx[e])) };
let cols: Vec<Bits> = src_basis
.iter()
.map(|(gi, m)| mask(&act_boundary(m, &res[s][*gi].boundary)))
.collect();
let mut elim: Vec<(usize, Bits, Bits)> = Vec::new(); for (j, c) in cols.iter().enumerate() {
let (mut r, mut src) = (c.clone(), Bits::bit(j));
for (piv, rc, sm) in &elim {
if r.get(*piv) {
r ^= rc;
src ^= sm;
}
}
if let Some(lead) = r.lowest() {
elim.push((lead, r, src));
}
}
let (mut tr, mut tsrc) = (mask(target), Bits::new());
for (piv, rc, sm) in &elim {
if tr.get(*piv) {
tr ^= rc;
tsrc ^= sm;
}
}
if !tr.is_zero() {
return None;
}
Some((0..src_basis.len()).filter(|&j| tsrc.get(j)).map(|j| src_basis[j].clone()).collect())
}
pub fn yoneda_product(res: &[Vec<ResGen>], a_s: usize, a_gi: usize, b_s: usize, b_gi: usize) -> Vec<usize> {
let t_a = res[a_s][a_gi].degree;
let t_b = res[b_s][b_gi].degree;
let mut phi: Vec<std::collections::HashMap<usize, Chain>> = Vec::new();
let mut p0 = std::collections::HashMap::new();
for i in 0..res[a_s].len() {
let mut c = Chain::new();
if i == a_gi {
c.insert((0, vec![]));
}
p0.insert(i, c);
}
phi.push(p0);
for k in 1..=b_s {
let mut pk: std::collections::HashMap<usize, Chain> = std::collections::HashMap::new();
for i in 0..res[a_s + k].len() {
let d_i = res[a_s + k][i].degree;
if d_i < t_a {
pk.insert(i, Chain::new()); continue;
}
let mut r = Chain::new();
for (j, m) in &res[a_s + k][i].boundary {
chain_add(&mut r, act_chain(m, &phi[k - 1][j]));
}
let x = if r.is_empty() {
Chain::new()
} else {
solve_boundary(res, k, d_i - t_a, &r).expect("minimal resolution is exact: ∂x = r is solvable")
};
pk.insert(i, x);
}
phi.push(pk);
}
(0..res[a_s + b_s].len())
.filter(|&i| res[a_s + b_s][i].degree == t_a + t_b && phi[b_s][&i].contains(&(b_gi, vec![])))
.collect()
}
type TensorTerm = (usize, usize, Vec<usize>, usize, usize, Vec<usize>);
type Tensor = std::collections::HashSet<TensorTerm>;
fn tensor_add(a: &mut Tensor, b: Tensor) {
for e in b {
if !a.insert(e.clone()) {
a.remove(&e);
}
}
}
fn tensor_degree(res: &[Vec<ResGen>], t: &Tensor) -> Option<usize> {
t.iter().next().map(|(p, i, m1, q, j, m2)| {
res[*p][*i].degree + m1.iter().sum::<usize>() + res[*q][*j].degree + m2.iter().sum::<usize>()
})
}
fn tensor_boundary(res: &[Vec<ResGen>], elem: &Tensor) -> Tensor {
let mut out = Tensor::new();
for (p, i, m1, q, j, m2) in elem {
if *p >= 1 {
for (i2, m1b) in act_boundary(m1, &res[*p][*i].boundary) {
let term = (*p - 1, i2, m1b, *q, *j, m2.clone());
if !out.insert(term.clone()) {
out.remove(&term);
}
}
}
if *q >= 1 {
for (j2, m2b) in act_boundary(m2, &res[*q][*j].boundary) {
let term = (*p, *i, m1.clone(), *q - 1, j2, m2b);
if !out.insert(term.clone()) {
out.remove(&term);
}
}
}
}
out
}
fn act_tensor(m: &[usize], elem: &Tensor) -> Tensor {
let psi = coproduct(m);
let mut out = Tensor::new();
for (p, i, m1, q, j, m2) in elem {
for (ml, mr) in &psi {
let (mut left, mut right) = (ml.clone(), mr.clone());
left.extend_from_slice(m1);
right.extend_from_slice(m2);
for a in reduce_unit(&left) {
for b in reduce_unit(&right) {
let term = (*p, *i, a.clone(), *q, *j, b);
if !out.insert(term.clone()) {
out.remove(&term);
}
}
}
}
}
out
}
fn tensor_basis(res: &[Vec<ResGen>], n: usize, t: usize) -> Vec<TensorTerm> {
let mut out = Vec::new();
for p in 0..=n {
let q = n - p;
if p >= res.len() || q >= res.len() {
continue;
}
for tl in 0..=t {
let tr = t - tl;
for (i, m1) in module_basis(&res[p], tl) {
for (j, m2) in module_basis(&res[q], tr) {
out.push((p, i, m1.clone(), q, j, m2.clone()));
}
}
}
}
out
}
fn solve_tensor_boundary(res: &[Vec<ResGen>], n: usize, target: &Tensor) -> Option<Tensor> {
fn reduce(mut col: Tensor, mut src: Tensor, elim: &[(TensorTerm, Tensor, Tensor)]) -> (Tensor, Tensor) {
loop {
let piv = match col.iter().max() {
Some(p) => p.clone(),
None => return (col, src),
};
match elim.iter().find(|(p, _, _)| *p == piv) {
Some((_, rc, sm)) => {
for x in rc {
if !col.insert(x.clone()) {
col.remove(x);
}
}
for x in sm {
if !src.insert(x.clone()) {
src.remove(x);
}
}
}
None => return (col, src),
}
}
}
let t = match tensor_degree(res, target) {
Some(t) => t,
None => return Some(Tensor::new()),
};
let mut elim: Vec<(TensorTerm, Tensor, Tensor)> = Vec::new(); for b in tensor_basis(res, n, t) {
let col = tensor_boundary(res, &std::iter::once(b.clone()).collect());
let src: Tensor = std::iter::once(b).collect();
let (rc, sm) = reduce(col, src, &elim);
if let Some(piv) = rc.iter().max().cloned() {
elim.push((piv, rc, sm));
}
}
let (rc, sm) = reduce(target.clone(), Tensor::new(), &elim);
rc.is_empty().then_some(sm)
}
fn diagonal(res: &[Vec<ResGen>], max_n: usize) -> Vec<std::collections::HashMap<usize, Tensor>> {
let mut delta: Vec<std::collections::HashMap<usize, Tensor>> = Vec::new();
let mut d0 = std::collections::HashMap::new();
d0.insert(0usize, std::iter::once((0, 0, vec![], 0, 0, vec![])).collect::<Tensor>());
delta.push(d0);
for n in 1..=max_n {
let mut dn: std::collections::HashMap<usize, Tensor> = std::collections::HashMap::new();
for k in 0..res[n].len() {
let mut rhs = Tensor::new(); for (j, mb) in &res[n][k].boundary {
tensor_add(&mut rhs, act_tensor(mb, &delta[n - 1][j]));
}
let x = solve_tensor_boundary(res, n, &rhs).expect("diagonal lifts: ∂x = Δ₀(∂g) is solvable");
dn.insert(k, x);
}
delta.push(dn);
}
delta
}
fn tensor_swap(t: &Tensor) -> Tensor {
t.iter().map(|(p, i, m1, q, j, m2)| (*q, *j, m2.clone(), *p, *i, m1.clone())).collect()
}
fn cup_homotopy(
res: &[Vec<ResGen>],
max_n: usize,
shift: usize,
prev: &[std::collections::HashMap<usize, Tensor>],
) -> Vec<std::collections::HashMap<usize, Tensor>> {
let mut d: Vec<std::collections::HashMap<usize, Tensor>> = Vec::new();
d.push(std::iter::once((0usize, Tensor::new())).collect()); for n in 1..=max_n {
let mut dn: std::collections::HashMap<usize, Tensor> = std::collections::HashMap::new();
for k in 0..res[n].len() {
let mut rhs = prev[n][&k].clone(); tensor_add(&mut rhs, tensor_swap(&prev[n][&k]));
for (j, mb) in &res[n][k].boundary {
tensor_add(&mut rhs, act_tensor(mb, &d[n - 1][j]));
}
let x = solve_tensor_boundary(res, n + shift, &rhs).expect("cup-i homotopy lifts: ∂x = Δ+TΔ+Δ∂");
dn.insert(k, x);
}
d.push(dn);
}
d
}
fn diagonal_1(
res: &[Vec<ResGen>],
max_n: usize,
delta: &[std::collections::HashMap<usize, Tensor>],
) -> Vec<std::collections::HashMap<usize, Tensor>> {
cup_homotopy(res, max_n, 1, delta)
}
fn sq0_on_h(res: &[Vec<ResGen>], delta1: &[std::collections::HashMap<usize, Tensor>], gi: usize) -> Vec<usize> {
let want = 2 * res[1][gi].degree;
(0..res[1].len())
.filter(|&k| res[1][k].degree == want && delta1[1][&k].contains(&(1, gi, vec![], 1, gi, vec![])))
.collect()
}
fn sq0_on_ext2(res: &[Vec<ResGen>], delta2: &[std::collections::HashMap<usize, Tensor>], gx: usize) -> Vec<usize> {
let want = 2 * res[2][gx].degree;
(0..res[2].len())
.filter(|&k| res[2][k].degree == want && delta2[2][&k].contains(&(2, gx, vec![], 2, gx, vec![])))
.collect()
}
fn cup_diagonals(
res: &[Vec<ResGen>],
max_shift: usize,
max_n: usize,
) -> Vec<Vec<std::collections::HashMap<usize, Tensor>>> {
let mut out = vec![diagonal(res, max_n)];
for i in 1..=max_shift {
let prev = out[i - 1].clone();
out.push(cup_homotopy(res, max_n, i, &prev));
}
out
}
fn sq0(res: &[Vec<ResGen>], deltas: &[Vec<std::collections::HashMap<usize, Tensor>>], s: usize, gx: usize) -> Vec<usize> {
let want = 2 * res[s][gx].degree;
let ds = &deltas[s]; (0..res[s].len())
.filter(|&k| res[s][k].degree == want && ds[s][&k].contains(&(s, gx, vec![], s, gx, vec![])))
.collect()
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum AdamsFact {
Dimension { s: usize, t: usize, dim: usize },
Product { lhs: String, rhs: String, result: String },
Vanishing { lhs: String, rhs: String },
Relation { a: String, b: String },
Doubling { from: String, to: String },
StableGroup { stem: usize, tower_lengths: Vec<usize>, truncated: bool },
Secondary { s: usize, t: usize },
}
impl std::fmt::Display for AdamsFact {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AdamsFact::Dimension { s, t, dim } => write!(f, "dim Ext^{{{s},{t}}} = {dim}"),
AdamsFact::Product { lhs, rhs, result } => write!(f, "{lhs} · {rhs} = {result}"),
AdamsFact::Vanishing { lhs, rhs } => write!(f, "{lhs} · {rhs} = 0"),
AdamsFact::Relation { a, b } => write!(f, "{a} = {b}"),
AdamsFact::Doubling { from, to } => write!(f, "Sq⁰({from}) = {to}"),
AdamsFact::StableGroup { stem, tower_lengths, truncated } => {
let group = if tower_lengths.is_empty() {
"0".to_string()
} else {
tower_lengths.iter().map(|l| format!("Z/2^{l}")).collect::<Vec<_>>().join(" ⊕ ")
};
if *truncated {
write!(f, "π_{stem}^s ⊗ Z₍₂₎ ⊇ {group} (h₀-tower hits the resolution ceiling — infinite/truncated)")
} else {
write!(f, "π_{stem}^s ⊗ Z₍₂₎ = {group}")
}
}
AdamsFact::Secondary { s, t } => {
write!(f, "Ext^{{{s},{t}}} has a non-product (secondary/Massey) class — un-auto-able by the product sweep")
}
}
}
}
fn name_by_products(
res: &[Vec<ResGen>],
max_s: usize,
max_t: usize,
) -> (std::collections::HashMap<(usize, usize), String>, Vec<AdamsFact>, Vec<(usize, usize)>) {
let mut names: std::collections::HashMap<(usize, usize), String> = std::collections::HashMap::new();
let mut facts: Vec<AdamsFact> = Vec::new();
let mut secondary: Vec<(usize, usize)> = Vec::new();
let mut worklist: Vec<(usize, usize)> = Vec::new();
for gi in 0..res[1].len() {
let i = res[1][gi].degree.trailing_zeros();
names.insert((1, gi), format!("h{i}"));
worklist.push((1, gi));
}
worklist.sort_by_key(|&(_, gi)| res[1][gi].degree);
let h_gens: Vec<usize> = (0..res[1].len()).collect();
let mut qi = 0;
let mut sec = 0;
loop {
while qi < worklist.len() {
let (s, gi) = worklist[qi];
qi += 1;
let lname = names[&(s, gi)].clone();
for &hgi in &h_gens {
if s + 1 > max_s || res[s][gi].degree + res[1][hgi].degree > max_t {
continue;
}
let hname = names[&(1, hgi)].clone();
let prod = yoneda_product(res, s, gi, 1, hgi);
if prod.is_empty() {
facts.push(AdamsFact::Vanishing { lhs: lname.clone(), rhs: hname });
} else if prod.len() == 1 {
let key = (s + 1, prod[0]);
let newname = format!("{lname}{hname}");
match names.get(&key) {
Some(existing) if *existing != newname => {
let (a, b) = if newname < *existing { (newname, existing.clone()) } else { (existing.clone(), newname) };
facts.push(AdamsFact::Relation { a, b });
}
Some(_) => {}
None => {
names.insert(key, newname.clone());
worklist.push(key);
facts.push(AdamsFact::Product { lhs: lname.clone(), rhs: hname, result: newname });
}
}
}
}
}
let next = (1..=max_s)
.flat_map(|s| (0..res[s].len()).map(move |gi| (s, gi)))
.filter(|&(s, gi)| res[s][gi].degree <= max_t && !names.contains_key(&(s, gi)))
.min_by_key(|&(s, gi)| (res[s][gi].degree, s));
match next {
Some((s, gi)) => {
names.insert((s, gi), format!("c{sec}"));
sec += 1;
secondary.push((s, gi));
worklist.push((s, gi));
}
None => break,
}
}
(names, facts, secondary)
}
fn secondary_classes(max_s: usize, max_t: usize) -> Vec<(usize, usize)> {
let res = minimal_resolution(max_s, max_t);
let (_names, _facts, secondary) = name_by_products(&res, max_s, max_t);
let mut out: Vec<(usize, usize)> = secondary.iter().map(|&(s, gi)| (s, res[s][gi].degree)).collect();
out.sort();
out.dedup();
out
}
pub fn harvest_secondary_facts(max_s: usize, max_t: usize) -> Vec<AdamsFact> {
let res = minimal_resolution(max_s, max_t);
let (_names, ring_facts, secondary) = name_by_products(&res, max_s, max_t);
let mut facts = ring_facts;
for s in 0..=max_s {
for t in 0..=max_t {
let dim = res[s].iter().filter(|g| g.degree == t).count();
if dim > 0 {
facts.push(AdamsFact::Dimension { s, t, dim });
}
}
}
for &(s, gi) in &secondary {
facts.push(AdamsFact::Secondary { s, t: res[s][gi].degree });
}
for stem in 0..=max_t.saturating_sub(max_s) {
let lengths = stem_2local_tower_lengths(&res, stem);
if !lengths.is_empty() {
let truncated = res[max_s].iter().any(|g| g.degree == stem + max_s);
facts.push(AdamsFact::StableGroup { stem, tower_lengths: lengths, truncated });
}
}
facts.sort();
facts.dedup();
facts
}
pub fn harvest_adams_facts(max_s: usize, max_t: usize) -> Vec<AdamsFact> {
let res = minimal_resolution(max_s, max_t);
let dbl_shift = max_s.min(2);
let deltas = cup_diagonals(&res, dbl_shift, dbl_shift);
let mut facts: Vec<AdamsFact> = Vec::new();
for s in 0..=max_s {
for t in 0..=max_t {
let dim = res[s].iter().filter(|g| g.degree == t).count();
if dim > 0 {
facts.push(AdamsFact::Dimension { s, t, dim });
}
}
}
let (names, ring_facts, secondary) = name_by_products(&res, max_s, max_t);
facts.extend(ring_facts);
for &(s, gi) in &secondary {
facts.push(AdamsFact::Secondary { s, t: res[s][gi].degree });
}
let mut named: Vec<((usize, usize), String)> = names.iter().map(|(k, v)| (*k, v.clone())).collect();
named.sort();
for ((s, gi), name) in &named {
if *s == 0 || *s > dbl_shift || 2 * res[*s][*gi].degree > max_t {
continue;
}
let img = sq0(&res, &deltas, *s, *gi);
if img.len() == 1 {
let to = names.get(&(*s, img[0])).cloned().unwrap_or_else(|| format!("Sq⁰({name})"));
facts.push(AdamsFact::Doubling { from: name.clone(), to });
}
}
for stem in 0..=max_t.saturating_sub(max_s) {
let lengths = stem_2local_tower_lengths(&res, stem);
if !lengths.is_empty() {
let truncated = res[max_s].iter().any(|g| g.degree == stem + max_s);
facts.push(AdamsFact::StableGroup { stem, tower_lengths: lengths, truncated });
}
}
facts.sort();
facts.dedup();
facts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adem_reduces_known_relations_to_the_admissible_basis() {
assert!(adem_reduce(&[1, 1]).is_empty(), "Sq¹Sq¹ = 0");
assert_eq!(adem_reduce(&[1, 2]), [vec![3]].into_iter().collect(), "Sq¹Sq² = Sq³");
assert_eq!(adem_reduce(&[2, 2]), [vec![3, 1]].into_iter().collect(), "Sq²Sq² = Sq³Sq¹");
for m in [vec![3, 1], vec![2, 1], vec![5, 2, 1]] {
for adm in &adem_reduce(&m) {
assert!(is_admissible(adm), "reduction output is admissible");
}
}
}
#[test]
fn two_primary_group_structure_from_the_h0_tower_filtration() {
let res = super::minimal_resolution(8, 18);
let g = |n| super::stem_2local_tower_lengths(&res, n);
assert_eq!(g(1), vec![1], "π₁ˢ = Z/2");
assert_eq!(g(2), vec![1], "π₂ˢ = Z/2");
assert_eq!(g(3), vec![3], "π₃ˢ: ONE h₀-tower height 3 = Z/8 (not (Z/2)³) ⇒ Z/24");
assert_eq!(g(6), vec![1], "π₆ˢ = Z/2");
assert_eq!(g(7), vec![4], "π₇ˢ = Z/16 ⇒ Z/240");
assert_eq!(g(8), vec![1, 1], "π₈ˢ = (Z/2)² — two separate towers");
assert_eq!(g(9), vec![1, 1, 1], "π₉ˢ = (Z/2)³");
assert_eq!(g(11), vec![3], "π₁₁ˢ = Z/8 ⇒ Z/504");
}
#[test]
fn stable_homotopy_groups_in_stems_eight_through_thirteen() {
let e = ext(7, 18);
let st = |n: usize| -> usize { (0..=7).filter(|&s| n + s <= 18).map(|s| e[s][n + s]).sum() };
assert_eq!(st(8), 2, "π₈ˢ = (Z/2)²");
assert_eq!(st(9), 3, "π₉ˢ = (Z/2)³");
assert_eq!(st(10), 1, "π₁₀ˢ = Z/2 (2-local of Z/6)");
assert_eq!(st(11), 3, "π₁₁ˢ = Z/8 2-locally (Z/504)");
assert_eq!(st(12), 0, "π₁₂ˢ = 0");
assert_eq!(st(13), 0, "π₁₃ˢ = 0 (2-locally; Z/3)");
}
#[test]
fn infinite_h0_tower_and_indecomposable_line() {
let e = ext(8, 8);
for s in 0..=8 {
assert_eq!(e[s][s], 1, "h₀ˢ ≠ 0 — the infinite stem-0 tower (π₀ˢ = Z) at s={s}");
}
for t in 1..=16 {
assert_eq!(adams_one_line(t), usize::from(t.is_power_of_two()), "the h_i line, forever, at t={t}");
}
}
#[test]
fn stable_homotopy_groups_through_the_seven_stem() {
let e = ext(6, 12);
let stem_total = |n: usize| -> usize { (0..=6).filter(|&s| n + s <= 12).map(|s| e[s][n + s]).sum() };
assert_eq!(stem_total(1), 1, "π₁ˢ = Z/2");
assert_eq!(stem_total(2), 1, "π₂ˢ = Z/2");
assert_eq!(stem_total(3), 3, "π₃ˢ = Z/24 (2-local Z/8)");
assert_eq!(stem_total(4), 0, "π₄ˢ = 0");
assert_eq!(stem_total(5), 0, "π₅ˢ = 0");
assert_eq!(stem_total(6), 1, "π₆ˢ = Z/2");
assert_eq!(stem_total(7), 4, "π₇ˢ = Z/240 (2-local Z/16)");
}
#[test]
fn minimal_resolution_recovers_ext_through_the_three_stem() {
let e = ext(5, 8);
for t in 1..=8 {
assert_eq!(e[1][t], adams_one_line(t), "Ext¹ from the engine matches the 1-line at t={t}");
}
for t in 2..=8 {
assert_eq!(e[2][t], adams_two_line(t), "Ext² from the engine matches the 2-line at t={t}");
}
for s in 0..=5 {
assert_eq!(e[s][s], 1, "h₀ˢ tower at filtration {s} (stem 0 = Z₂)");
}
assert_eq!(e[1][4], 1, "h₂");
assert_eq!(e[2][5], 1, "h₀h₂");
assert_eq!(e[3][6], 1, "h₀²h₂ — tower height 3");
assert_eq!(e[4][7], 0, "h₀³h₂ = 0 ⇒ tower ends ⇒ π₃ˢ = Z/24 (2-locally Z/8)");
}
#[test]
fn low_stem_homotopy_from_the_e2_lines() {
assert_eq!(adams_one_line(2), 1, "h₁ generates stem 1");
assert_eq!(adams_two_line(3), 0, "h₀h₁ = 0 — the stem-1 h₀-tower is dead, so π₁ˢ = Z/2");
assert_eq!(adams_two_line(4), 1, "h₁² generates stem 2 ⇒ π₂ˢ = Z/2");
}
#[test]
fn the_adams_e2_two_line_matches_the_h_i_h_j_chart() {
for t in 2..=16 {
assert_eq!(adams_two_line(t), known_ext2(t), "dim Ext^{{2,t}} = #(h_i h_j) at t={t}");
}
}
#[test]
fn the_adams_e2_one_line_is_exactly_the_h_i_indecomposables() {
for t in 1..=16 {
assert_eq!(
adams_one_line(t),
usize::from(t.is_power_of_two()),
"dim Ext^{{1,t}} = 1 iff t is a power of 2 (the h_i) at t={t}"
);
}
}
#[test]
fn sq_n_is_decomposable_iff_n_is_not_a_power_of_two_hopf_invariant_one() {
for n in 2..=17 {
assert_eq!(
is_decomposable_sq(n),
!n.is_power_of_two(),
"Sqⁿ decomposable ⟺ n not a power of 2 (indecomposables = Sq^{{2ⁱ}}) at n={n}"
);
}
}
#[test]
fn adem_reduction_preserves_the_action_on_bz2() {
let monomials = [vec![1, 1], vec![1, 2], vec![2, 2], vec![3, 2], vec![2, 3], vec![1, 2, 1], vec![4, 2]];
for m in monomials {
let reduced = adem_reduce(&m);
for k in 0..=12 {
let xk = monomial(k);
let direct = act_monomial(&m, &xk);
let mut via_basis = vec![0u8];
for adm in &reduced {
via_basis = {
let a = act_monomial(adm, &xk);
let n = via_basis.len().max(a.len());
(0..n).map(|i| via_basis.get(i).copied().unwrap_or(0) ^ a.get(i).copied().unwrap_or(0)).collect()
};
}
assert_eq!(direct, trim(via_basis), "monomial {m:?} acts as its admissible reduction at k={k}");
}
}
}
#[test]
fn adem_relations_hold_on_bz2() {
for k in 0..=12 {
let xk = monomial(k);
assert_eq!(apply_sq(1, &apply_sq(1, &xk)), vec![0u8], "Adem Sq¹Sq¹ = 0 at k={k}");
assert_eq!(apply_sq(1, &apply_sq(2, &xk)), apply_sq(3, &xk), "Adem Sq¹Sq² = Sq³ at k={k}");
assert_eq!(
apply_sq(2, &apply_sq(2, &xk)),
apply_sq(3, &apply_sq(1, &xk)),
"Adem Sq²Sq² = Sq³Sq¹ at k={k}"
);
}
}
#[test]
fn the_steenrod_action_on_bz2_is_binomial_coefficients_mod_2() {
for k in 0..=10 {
for i in 0..=k {
assert_eq!(sq(i, k), (binom(k, i) % 2) as u8, "Sqⁱ(xᵏ) = C(k,i) mod 2");
}
}
}
#[test]
fn binomial_mod_2_is_lucas_theorem_bitwise() {
for k in 0..=15usize {
for i in 0..=k {
let lucas = u8::from((i & k) == i);
assert_eq!((binom(k, i) % 2) as u8, lucas, "C(k,i) mod 2 = [i AND k == i]");
}
}
}
#[test]
fn steenrod_axioms_identity_top_square_and_instability() {
for k in 1..=8 {
assert_eq!(sq(0, k), 1, "Sq⁰(xᵏ) = xᵏ");
assert_eq!(sq(k, k), 1, "Sqᵏ(xᵏ) = x^2k — the top square is the cup-square");
assert_eq!(sq(k + 1, k), 0, "Sqⁱ(xᵏ) = 0 for i > k (instability)");
}
}
#[test]
fn the_total_square_obeys_the_cartan_formula_ring_homomorphism() {
for i in 0..=5 {
for j in 0..=5 {
let lhs = total_square(&monomial(i + j));
let rhs = trim(mul(&total_square(&monomial(i)), &total_square(&monomial(j))));
assert_eq!(lhs, rhs, "Cartan: Sq(xⁱ⁺ʲ) = Sq(xⁱ)·Sq(xʲ)");
}
}
}
}
#[cfg(test)]
mod adams_differential_onset {
use super::*;
#[test]
fn e2_strictly_exceeds_einfinity_at_the_fourteen_stem() {
let e = ext(8, 24);
let st = |n: usize| -> usize { (0..=8).filter(|&s| n + s <= 24).map(|s| e[s][n + s]).sum() };
assert_eq!(st(13), 0, "13-stem: E₂ = E∞, both trivial 2-primarily");
assert_eq!(st(14), 5, "Σ_s dim Ext^{{s,14+s}} = 5 on the E₂ page");
assert_ne!(st(14), 2, "rank E₂ > rank E∞ at the 14-stem: d₂(h₄) = h₀h₃² acts here");
}
}
#[cfg(test)]
mod ext_ring_from_the_resolution {
use super::*;
#[test]
fn the_h_i_products_obey_the_known_ext_ring_relations() {
let res = minimal_resolution(4, 18);
let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
let prod = |a_s, a, b_s, b| yoneda_product(&res, a_s, a, b_s, b);
for i in 0..=3 {
assert!(!prod(1, h(i), 1, h(i)).is_empty(), "cup-square h_{i}² is nonzero");
}
assert_eq!(prod(1, h(0), 1, h(0)).len(), 1, "h_0² spans the 1-dim Ext^{{2,2}}");
assert!(!prod(1, h(0), 1, h(2)).is_empty(), "h_0 h_2 ≠ 0");
assert!(!prod(1, h(0), 1, h(3)).is_empty(), "h_0 h_3 ≠ 0");
assert!(!prod(1, h(1), 1, h(3)).is_empty(), "h_1 h_3 ≠ 0");
assert!(prod(1, h(0), 1, h(1)).is_empty(), "h_0 h_1 = 0 (Adem adjacency)");
assert!(prod(1, h(1), 1, h(2)).is_empty(), "h_1 h_2 = 0 (Adem adjacency)");
assert!(prod(1, h(2), 1, h(3)).is_empty(), "h_2 h_3 = 0 (Adem adjacency)");
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");
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");
let h1sq = prod(1, h(1), 1, h(1));
let h0sq = prod(1, h(0), 1, h(0));
assert_eq!(h1sq.len(), 1, "h_1² spans Ext^{{2,4}}");
assert_eq!(h0sq.len(), 1, "h_0² spans Ext^{{2,2}}");
let h1_cubed = yoneda_product(&res, 2, h1sq[0], 1, h(1));
let h0sq_h2 = yoneda_product(&res, 2, h0sq[0], 1, h(2));
assert!(!h1_cubed.is_empty(), "h_1³ ≠ 0");
assert_eq!(h1_cubed, h0sq_h2, "h_1³ = h_0² h_2 — derived, not assumed");
}
}
#[cfg(test)]
mod steenrod_coproduct {
use super::*;
use std::collections::HashSet;
fn set(pairs: &[(&[usize], &[usize])]) -> HashSet<(Vec<usize>, Vec<usize>)> {
pairs.iter().map(|(l, r)| (l.to_vec(), r.to_vec())).collect()
}
#[test]
fn the_cartan_coproduct_is_diagonal_cocommutative_and_adem_reduced() {
assert_eq!(coproduct(&[1]), set(&[(&[], &[1]), (&[1], &[])]));
assert_eq!(coproduct(&[2]), set(&[(&[2], &[]), (&[1], &[1]), (&[], &[2])]));
assert_eq!(coproduct(&[3]), set(&[(&[3], &[]), (&[2], &[1]), (&[1], &[2]), (&[], &[3])]));
assert_eq!(
coproduct(&[2, 1]),
set(&[(&[2, 1], &[]), (&[2], &[1]), (&[1], &[2]), (&[], &[2, 1])])
);
for m in [vec![1], vec![2], vec![3], vec![4], vec![2, 1], vec![4, 2, 1], vec![5], vec![6, 3]] {
let psi = coproduct(&m);
let swapped: HashSet<_> = psi.iter().map(|(l, r)| (r.clone(), l.clone())).collect();
assert_eq!(psi, swapped, "ψ is cocommutative on {m:?}");
let deg = |x: &[usize]| -> usize { x.iter().sum() };
for (l, r) in &psi {
assert_eq!(deg(l) + deg(r), deg(&m), "ψ preserves internal degree leg-wise on {m:?}");
}
}
}
}
#[cfg(test)]
mod chain_level_diagonal {
use super::*;
#[test]
fn the_chain_level_diagonal_reproduces_the_yoneda_cup_squares() {
let res = minimal_resolution(2, 8);
let delta = diagonal(&res, 2);
let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
for i in 0..=2 {
let gi = h(i);
let want = 2 * (1 << i);
let mut via_diagonal: Vec<usize> = (0..res[2].len())
.filter(|&k| {
res[2][k].degree == want && delta[2][&k].contains(&(1, gi, vec![], 1, gi, vec![]))
})
.collect();
via_diagonal.sort_unstable();
let mut via_yoneda = yoneda_product(&res, 1, gi, 1, gi);
via_yoneda.sort_unstable();
assert!(!via_diagonal.is_empty(), "h_{i}² ≠ 0");
assert_eq!(via_diagonal, via_yoneda, "Δ₀ reproduces the Yoneda cup-square h_{i}²");
}
}
#[test]
fn the_cup_1_homotopy_derives_the_sq0_doubling_on_the_h_line() {
let res = minimal_resolution(2, 8);
let delta = diagonal(&res, 1);
let delta1 = diagonal_1(&res, 1, &delta);
let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
for i in 0..=2 {
assert_eq!(
sq0_on_h(&res, &delta1, h(i)),
vec![h(i + 1)],
"Sq⁰(h_{i}) = h_(i+1) — the doubling, derived from the cup-1 homotopy"
);
}
}
#[test]
fn the_sq0_doubling_is_a_ring_endomorphism_on_the_squares() {
let res = minimal_resolution(4, 8);
let delta = diagonal(&res, 2);
let delta1 = diagonal_1(&res, 2, &delta);
let delta2 = cup_homotopy(&res, 2, 2, &delta1);
let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
for i in 0..=1 {
let hi_sq = yoneda_product(&res, 1, h(i), 1, h(i));
assert_eq!(hi_sq.len(), 1, "h_i² is a single Ext² generator");
let mut lhs = sq0_on_ext2(&res, &delta2, hi_sq[0]);
lhs.sort_unstable();
let mut rhs = yoneda_product(&res, 1, h(i + 1), 1, h(i + 1));
rhs.sort_unstable();
assert!(!rhs.is_empty(), "h_(i+1)² ≠ 0");
assert_eq!(lhs, rhs, "Sq⁰(h_i²) = h_(i+1)² — the doubling is a ring endomorphism");
}
}
#[test]
fn the_sq0_doubling_automates_across_the_low_adams_chart() {
let res = minimal_resolution(4, 10);
let deltas = cup_diagonals(&res, 2, 2);
let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i generator");
let mut harvested: Vec<String> = Vec::new();
for i in 0..=2 {
assert_eq!(sq0(&res, &deltas, 1, h(i)), vec![h(i + 1)], "Sq⁰(h_i) = h_(i+1)");
harvested.push(format!("Sq0(h{i})=h{}", i + 1));
}
for i in 0..=2 {
for j in i..=2 {
let prod = yoneda_product(&res, 1, h(i), 1, h(j));
if prod.len() != 1 || 2 * res[2][prod[0]].degree > 10 {
continue;
}
let mut got = sq0(&res, &deltas, 2, prod[0]);
got.sort_unstable();
let mut want = yoneda_product(&res, 1, h(i + 1), 1, h(j + 1));
want.sort_unstable();
assert_eq!(got, want, "Sq⁰(h_i h_j) = h_(i+1) h_(j+1) — harvested automatically");
harvested.push(format!("Sq0(h{i}h{j})=h{}h{}", i + 1, j + 1));
}
}
assert!(harvested.iter().any(|s| s == "Sq0(h0h2)=h1h3"), "sweep reaches the mixed product h_0h_2 ↦ h_1h_3");
assert!(harvested.len() >= 6, "doubling harvested ≥6 chart facts automatically: {harvested:?}");
}
}
#[cfg(test)]
mod adams_fact_engine {
use super::*;
#[test]
fn the_engine_self_ignites_and_auto_collects_the_adams_chart() {
let facts = harvest_adams_facts(4, 10);
let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
assert!(has("h0h0h2 = h1h1h1"), "the ring relation h_1³ = h_0²h_2 was auto-discovered by collision");
assert!(has("Sq⁰(h0) = h1"), "the doubling h0 ↦ h1 was auto-collected");
assert!(has("Sq⁰(h0h2) = h1h3"), "the mixed-product doubling was auto-collected");
assert!(has("h0 · h2 = h0h2"), "the product h_0·h_2 was auto-collected");
assert!(has("h0 · h1 = 0"), "the Adem-adjacency vanishing h_0·h_1 = 0 was auto-collected");
assert!(has("π_3^s ⊗ Z₍₂₎ = Z/2^3"), "the exact 2-local stem π_3 = Z/8 was auto-collected");
assert!(
facts.iter().any(|f| matches!(f, AdamsFact::StableGroup { stem: 0, truncated: true, .. })),
"π_0 = Z₍₂₎ is honestly flagged as a truncated/infinite h₀-tower, not a false finite group"
);
let relations = facts.iter().filter(|f| matches!(f, AdamsFact::Relation { .. })).count();
let doublings = facts.iter().filter(|f| matches!(f, AdamsFact::Doubling { .. })).count();
assert!(relations >= 1 && doublings >= 5, "engine nets ≥1 relation and ≥5 doublings");
assert!(facts.len() >= 30, "engine auto-collected ≥30 facts from one ignition: {}", facts.len());
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
if std::fs::create_dir_all(&dir).is_ok() {
let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
let _ = std::fs::write(dir.join("adams_chart.txt"), body);
}
}
#[test]
fn the_harvested_catalog_is_frozen_as_ground_truth() {
let got: Vec<String> = harvest_adams_facts(4, 10).iter().map(|f| f.to_string()).collect();
let expected = "\
dim Ext^{0,0} = 1
dim Ext^{1,1} = 1
dim Ext^{1,2} = 1
dim Ext^{1,4} = 1
dim Ext^{1,8} = 1
dim Ext^{2,2} = 1
dim Ext^{2,4} = 1
dim Ext^{2,5} = 1
dim Ext^{2,8} = 1
dim Ext^{2,9} = 1
dim Ext^{2,10} = 1
dim Ext^{3,3} = 1
dim Ext^{3,6} = 1
dim Ext^{3,10} = 1
dim Ext^{4,4} = 1
h0 · h0 = h0h0
h0 · h2 = h0h2
h0 · h3 = h0h3
h0h0 · h0 = h0h0h0
h0h0 · h2 = h0h0h2
h0h0 · h3 = h0h0h3
h0h0h0 · h0 = h0h0h0h0
h1 · h1 = h1h1
h1 · h3 = h1h3
h2 · h2 = h2h2
h0 · h1 = 0
h0h0 · h1 = 0
h0h0h0 · h1 = 0
h0h0h0 · h2 = 0
h0h0h2 · h0 = 0
h0h0h2 · h1 = 0
h0h0h2 · h2 = 0
h0h2 · h1 = 0
h0h2 · h2 = 0
h1 · h0 = 0
h1 · h2 = 0
h1h1 · h0 = 0
h1h1 · h2 = 0
h2 · h1 = 0
h2h2 · h0 = 0
h2h2 · h1 = 0
h0h0h2 = h0h2h0
h0h0h2 = h1h1h1
h0h0h3 = h0h3h0
h0h2 = h2h0
h0h3 = h3h0
h1h3 = h3h1
Sq⁰(h0) = h1
Sq⁰(h0h0) = h1h1
Sq⁰(h0h2) = h1h3
Sq⁰(h1) = h2
Sq⁰(h1h1) = h2h2
Sq⁰(h2) = h3
π_0^s ⊗ Z₍₂₎ ⊇ Z/2^5 (h₀-tower hits the resolution ceiling — infinite/truncated)
π_1^s ⊗ Z₍₂₎ = Z/2^1
π_2^s ⊗ Z₍₂₎ = Z/2^1
π_3^s ⊗ Z₍₂₎ = Z/2^3
π_6^s ⊗ Z₍₂₎ = Z/2^1";
assert_eq!(got.join("\n"), expected, "the banked Adams-chart catalog drifted from ground truth");
}
#[test]
fn the_first_un_auto_able_class_is_c0_at_ext_3_11() {
let secondary = secondary_classes(6, 12);
assert!(!secondary.is_empty(), "extending the range must expose un-auto-able classes");
assert_eq!(secondary[0], (3, 11), "the first non-product class is c₀ at Ext^{{3,11}}: {secondary:?}");
assert!(secondary_classes(4, 10).is_empty(), "no non-product classes appear within the (4,10) window");
}
#[test]
fn the_engine_auto_collects_the_wide_frontier_including_the_secondary_classes() {
let facts = harvest_secondary_facts(6, 13);
let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
assert!(
has("Ext^{3,11} has a non-product (secondary/Massey) class — un-auto-able by the product sweep"),
"c₀ at Ext^{{3,11}} auto-collected as a Secondary fact"
);
let secondary_facts: Vec<&AdamsFact> = facts.iter().filter(|f| matches!(f, AdamsFact::Secondary { .. })).collect();
assert!(!secondary_facts.is_empty(), "the frontier auto-collected ≥1 secondary class");
assert!(matches!(secondary_facts[0], AdamsFact::Secondary { s: 3, t: 11 }), "the first secondary class is c₀ at Ext^{{3,11}}");
assert!(has("c0 · h1 = c0h1"), "the product symmetry broke into the c₀-family automatically");
assert!(has("c0 · h0 = 0"), "h₀·c₀ = 0 auto-derived by the verified product, not proven by hand");
assert!(has("h0h0h2 = h1h1h1"), "the ring relation is still auto-collected at the wide range");
assert!(has("π_3^s ⊗ Z₍₂₎ = Z/2^3"), "π_3 = Z/8 auto-collected");
assert!(facts.len() >= 60, "wide auto-collection gathered ≥60 facts: {}", facts.len());
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
if std::fs::create_dir_all(&dir).is_ok() {
let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
let _ = std::fs::write(dir.join("adams_frontier.txt"), body);
}
}
#[test]
fn the_engine_breaks_the_secondary_families_out_of_the_shadows() {
let secondary = secondary_classes(9, 18);
for bidegree in [(3, 11), (5, 14), (5, 16), (4, 18)] {
assert!(secondary.contains(&bidegree), "detected the secondary generator at Ext^{bidegree:?}: {secondary:?}");
}
let facts = harvest_secondary_facts(9, 18);
let has = |needle: &str| facts.iter().any(|f| f.to_string() == needle);
assert!(has("c1 · h1 = c1h1"), "the c1 family auto-sweeps");
assert!(has("c2 · h0 = c2h0"), "the c2 family auto-sweeps");
assert!(has("c1h1h1 = c2h0h0"), "a cross-family relation auto-discovered by collision");
assert!(has("c0 · h0 = 0") && has("c0 · h2 = 0"), "h₀·c₀ = h₂·c₀ = 0 auto-derived");
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
if std::fs::create_dir_all(&dir).is_ok() {
let body: String = facts.iter().map(|f| format!("{f}\n")).collect();
let _ = std::fs::write(dir.join("adams_frontier_deep.txt"), body);
}
}
}
#[cfg(test)]
mod algebra_generic_engine {
use super::*;
#[test]
fn the_engine_is_algebra_generic_and_computes_hz_via_a0() {
let sphere = ext_over(&SteenrodAlgebra, 6, 12);
assert_eq!(sphere, ext(6, 12), "generic engine on 𝒜 reproduces the sphere");
assert_eq!((sphere[1][1], sphere[1][2], sphere[1][4]), (1, 1, 1), "sphere has h_0, h_1, h_2");
let a0 = build_subalgebra(&[vec![1]], 1);
assert_eq!(a0.dimension(), 2, "A(0) = {{1, Sq¹}} is 2-dimensional");
let hz = ext_over(&a0, 12, 14);
for s in 0..=12 {
for t in 0..=14 {
assert_eq!(hz[s][t], usize::from(t == s), "Ext_A(0)^{{{s},{t}}} = F₂[h_0] diagonal");
}
}
assert_eq!(hz[1][2], 0, "HZ has NO h_1 — the engine distinguishes HZ from the sphere");
}
#[test]
fn the_engine_lands_ko_via_the_combination_basis_a1() {
let a1 = build_subalgebra(&[vec![1], vec![2]], 6);
assert_eq!(a1.dimension(), 8, "A(1) is 8-dimensional (the combination basis sees it correctly)");
let ko = ext_over(&a1, 12, 16);
assert_eq!(ko[0][0], 1, "Ext^{{0,0}} = Z/2");
assert_eq!(ko[1][1], 1, "h_0 = [Sq¹]");
assert_eq!(ko[1][2], 1, "h_1 = [Sq²]");
assert_eq!(ko[1][4], 0, "ko has NO h_2 — distinguishes ko from the sphere");
assert_eq!(ko[1][8], 0, "ko has NO h_3");
for t in 3..=16 {
assert_eq!(ko[1][t], 0, "the only A(1)-indecomposables are Sq¹, Sq² (t={t})");
}
for s in 0..=12 {
assert_eq!(ko[s][s], 1, "the infinite h_0-tower: π_0(ko)=Z at (s,s)={s}");
}
}
#[test]
fn the_engine_lands_tmf_via_the_combination_basis_a2() {
let a2 = build_subalgebra(&[vec![1], vec![2], vec![4]], 23);
assert_eq!(a2.dimension(), 64, "A(2) is 64-dimensional");
let tmf = ext_over(&a2, 10, 12);
assert_eq!(tmf[0][0], 1, "Ext^{{0,0}} = Z/2");
assert_eq!(tmf[1][1], 1, "h_0 = [Sq¹]");
assert_eq!(tmf[1][2], 1, "h_1 = [Sq²]");
assert_eq!(tmf[1][4], 1, "h_2 = [Sq⁴] — tmf HAS h_2 (unlike ko)");
assert_eq!(tmf[1][8], 0, "tmf has NO h_3 — distinguishes tmf from the sphere");
for t in 3..=12 {
if t != 4 {
assert_eq!(tmf[1][t], 0, "only h_0,h_1,h_2 on the tmf one-line (t={t})");
}
}
for s in 0..=10 {
assert_eq!(tmf[s][s], 1, "the infinite h_0-tower: π_0(tmf)=Z at (s,s)={s}");
}
}
#[test]
fn the_engine_lands_a3_height_three_dim_1024() {
let a3 = build_subalgebra(&[vec![1], vec![2], vec![4], vec![8]], 72);
assert_eq!(a3.dimension(), 1024, "A(3) is 1024-dimensional");
let e = ext_over(&a3, 8, 16);
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)");
assert_eq!(e[1][16], 0, "A(3) has NO h_4 — distinguishes it from the sphere");
for t in 3..=15 {
if ![4, 8].contains(&t) {
assert_eq!(e[1][t], 0, "only h_0..h_3 on the A(3) one-line (t={t})");
}
}
for s in 0..=8 {
assert_eq!(e[s][s], 1, "the infinite h_0-tower: π_0 = Z at (s,s)={s}");
}
}
#[test]
#[ignore = "heavy (~15s): A(4) resolution to degree 32 over a 633-dim algebra — the height-4 crush, on demand"]
fn the_engine_lands_a4_height_four() {
let a4 = build_subalgebra(&[vec![1], vec![2], vec![4], vec![8], vec![16]], 34);
let e = ext_over(&a4, 6, 32);
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");
assert_eq!(e[1][32], 0, "A(4) has NO h_5 — it carries h_4 (unlike A(3)) and stops there");
for s in 0..=6 {
assert_eq!(e[s][s], 1, "the infinite h_0-tower: π_0 = Z at (s,s)={s}");
}
}
}
#[cfg(test)]
mod adams_differential_family {
use super::*;
#[test]
fn the_first_adams_differential_is_the_sq0_orbit_of_one_geometric_seed() {
let is_dr = |r: usize, src: (usize, usize), tgt: (usize, usize)| tgt.0 == src.0 + r && tgt.1 == src.1 + r - 1;
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₂");
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)");
for i in 3..=20 {
assert!(is_dr(2, hi(i + 1), h0_hi_sq(i)), "d₂(h_{{i+1}}) = h₀h_i² is a real d₂ (i={i})");
}
let res = minimal_resolution(2, 16);
let delta = diagonal(&res, 1);
let delta1 = diagonal_1(&res, 1, &delta);
let hgen = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).unwrap();
for i in 0..=3 {
assert_eq!(sq0_on_h(&res, &delta1, hgen(i)), vec![hgen(i + 1)], "Sq⁰(h_{i}) = h_{{i+1}} (computed)");
}
assert_eq!(sq0_on_h(&res, &delta1, hgen(3)), vec![hgen(4)], "h₄ = Sq⁰(h₃): the seed's source IS the doubling of σ");
let small = minimal_resolution(4, 8);
let sdelta = diagonal(&small, 2);
let sdelta1 = diagonal_1(&small, 2, &sdelta);
let sdelta2 = cup_homotopy(&small, 2, 2, &sdelta1);
let sgen = |i: usize| small[1].iter().position(|g| g.degree == (1 << i)).unwrap();
for i in 0..=1 {
let hi_sq = yoneda_product(&small, 1, sgen(i), 1, sgen(i));
let hnext_sq = yoneda_product(&small, 1, sgen(i + 1), 1, sgen(i + 1));
assert_eq!(sq0_on_ext2(&small, &sdelta2, hi_sq[0]), hnext_sq, "Sq⁰(h_i²)=h_{{i+1}}² drives the target doubling");
}
}
#[test]
fn the_first_differential_can_only_start_at_h4_by_pure_algebra() {
let res = minimal_resolution(4, 18);
let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).expect("h_i");
let h0 = h(0);
let target = |i: usize| -> Vec<usize> {
let sq = yoneda_product(&res, 1, h(i), 1, h(i));
if sq.is_empty() { vec![] } else { yoneda_product(&res, 1, h0, 2, sq[0]) }
};
assert!(target(2).is_empty(), "h₀h₂² = 0 ⇒ d₂(h₃) has no target ⇒ h₃ is a permanent cycle (algebra)");
assert!(target(1).is_empty(), "h₀h₁² = 0 ⇒ d₂(h₂) has no target ⇒ h₂ is a permanent cycle (algebra)");
let h0_cubed = target(0);
assert!(!h0_cubed.is_empty(), "h₀³ = h₀h₀² ≠ 0 (h₁'s would-be target exists)");
let h0_fourth = yoneda_product(&res, 1, h0, 3, h0_cubed[0]);
assert!(!h0_fourth.is_empty(), "h₀⁴ ≠ 0 ⇒ d₂(h₁)=h₀³ would force h₀⁴=0 — contradiction ⇒ d₂(h₁)=0");
assert!(yoneda_product(&res, 1, h0, 1, h(1)).is_empty(), "h₀h₁ = 0 — the relation doing the forbidding");
assert!(!target(3).is_empty(), "h₀h₃² ≠ 0 ⇒ d₂(h₄) finally HAS a target ⇒ h₄ is the unique starting point");
}
#[test]
fn the_h_infinity_transgression_law_generates_the_whole_first_differential() {
let cup = minimal_resolution(2, 16);
let cdelta = diagonal(&cup, 1);
let cdelta1 = diagonal_1(&cup, 1, &cdelta);
let cg = |i: usize| cup[1].iter().position(|g| g.degree == (1 << i)).unwrap();
for i in 0..=3 {
assert_eq!(sq0_on_h(&cup, &cdelta1, cg(i)), vec![cg(i + 1)], "source: Sq⁰(h_{i}) = h_{{i+1}} (derived)");
}
let res = minimal_resolution(4, 18);
let h = |i: usize| res[1].iter().position(|g| g.degree == (1 << i)).unwrap();
let h0 = h(0);
let sq1 = |i: usize| yoneda_product(&res, 1, h(i), 1, h(i));
let d2 = |i: usize| -> Vec<usize> {
let s = sq1(i);
if s.is_empty() { vec![] } else { yoneda_product(&res, 1, h0, 2, s[0]) }
};
assert!(!sq1(3).is_empty(), "Sq¹(h₃) = h₃² ≠ 0 (the target operation is nonzero at h₃)");
assert!(d2(1).is_empty(), "law ⇒ d₂(h₂) = h₀·Sq¹(h₁) = 0 ⇒ h₂ permanent");
assert!(d2(2).is_empty(), "law ⇒ d₂(h₃) = h₀·Sq¹(h₂) = 0 ⇒ h₃ permanent");
assert!(!d2(3).is_empty(), "law ⇒ d₂(h₄) = h₀·Sq¹(h₃) = h₀h₃² ≠ 0 ⇒ h₄ FIRES — the last bit, resolved by ONE law");
}
#[test]
fn the_transgression_law_over_the_sq0_orbit_of_h0_generates_the_whole_first_line() {
let cup = minimal_resolution(2, 16);
let cdelta = diagonal(&cup, 1);
let cdelta1 = diagonal_1(&cup, 1, &cdelta);
let cg = |i: usize| cup[1].iter().position(|g| g.degree == (1 << i)).unwrap();
let mut x = cg(0); for i in 0..=3 {
let next = sq0_on_h(&cup, &cdelta1, x);
assert_eq!(next, vec![cg(i + 1)], "Sq⁰ⁱ⁺¹(h_0) = h_{{i+1}}: the h-line is one Sq⁰-orbit");
x = next[0];
}
let is_d2 = |src: (usize, usize), tgt: (usize, usize)| tgt.0 == src.0 + 2 && tgt.1 == src.1 + 1;
for i in 0..=30 {
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})");
}
}
}