diff --git a/ext/crates/sseq/src/coordinates/bze.rs b/ext/crates/sseq/src/coordinates/bze.rs new file mode 100644 index 0000000000..47f5c01bf4 --- /dev/null +++ b/ext/crates/sseq/src/coordinates/bze.rs @@ -0,0 +1,50 @@ +use std::fmt::{self, Display, Formatter}; + +use fp::matrix::Subquotient; + +/// Classification of a spectral sequence generator by its role relative to a differential on a +/// given page. At page $r$, each generator in bidegree $b$ is exactly one of: +/// +/// - **B** (boundary): in the image of $d_{r-1}$ from another bidegree. +/// - **Z** (cycle): in the kernel of $d_r$, and not a boundary. +/// - **E** (supports $d_r$): $d_r(x) \neq 0$. +/// +/// Every spectral sequence admits this decomposition; the Adams spectral sequence for $S$ at $E_2$ +/// has everything as **Z** (degenerate), while $S/\lambda^2$ at $E_3$ has a nontrivial splitting. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BZE { + /// Boundary: in the image of an incoming differential. + B, + /// Cycle mod boundary: survives to the next page. + Z, + /// Supports a differential: $d_r(x) \neq 0$. + E, +} + +impl BZE { + /// Classify generator `idx` from a page's [`Subquotient`]. + /// + /// The subquotient encodes $E_r = Z_r / B_r$ at a given bidegree: + /// - `page.zeros().pivots()[idx] >= 0` means `idx` is a boundary pivot (**B**). + /// - `page.complement_pivots()` yields generators not in the cycle subspace (**E**). + /// - Everything else is a cycle that is not a boundary (**Z**). + pub fn from_page_data(page: &Subquotient, idx: usize) -> Self { + if page.zeros().pivots()[idx] >= 0 { + return Self::B; + } + if page.complement_pivots().any(|p| p == idx) { + return Self::E; + } + Self::Z + } +} + +impl Display for BZE { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::B => write!(f, "B"), + Self::Z => write!(f, "Z"), + Self::E => write!(f, "E"), + } + } +} diff --git a/ext/crates/sseq/src/coordinates/mod.rs b/ext/crates/sseq/src/coordinates/mod.rs index c030db46b5..ae6d6f8760 100644 --- a/ext/crates/sseq/src/coordinates/mod.rs +++ b/ext/crates/sseq/src/coordinates/mod.rs @@ -1,3 +1,4 @@ +pub use bze::BZE; pub use degree::MultiDegree; pub use element::MultiDegreeElement; pub use generator::MultiDegreeGenerator; @@ -5,6 +6,7 @@ use maybe_rayon::prelude::*; use ordered::OrderedMultiDegree; pub use range::BidegreeRange; +pub mod bze; pub mod degree; pub mod element; pub mod generator; diff --git a/ext/crates/sseq/src/sseq.rs b/ext/crates/sseq/src/sseq.rs index a1d70e8e14..ee3e684297 100644 --- a/ext/crates/sseq/src/sseq.rs +++ b/ext/crates/sseq/src/sseq.rs @@ -9,7 +9,9 @@ use fp::{ use once::MultiIndexed; use crate::{ - coordinates::{Bidegree, BidegreeGenerator, degree::MultiDegree, element::MultiDegreeElement}, + coordinates::{ + BZE, Bidegree, BidegreeGenerator, degree::MultiDegree, element::MultiDegreeElement, + }, differential::Differential, }; @@ -39,6 +41,28 @@ impl SseqProfile<2> for Adams { } } +/// Trigraded Adams profile for S/λ². Coordinates are `(n, s, b)` = (stem, Adams filtration, +/// Bockstein degree). The d₂ differential maps `(n, s, b) → (n − 1, s + 2, b + 1)`. +pub struct AdamsLambda2; + +impl SseqProfile<3> for AdamsLambda2 { + const MIN_R: i32 = 2; + + fn profile(r: i32, b: MultiDegree<3>) -> MultiDegree<3> { + let [n, s, bock] = b.coords(); + MultiDegree::new([n - 1, s + r, bock + 1]) + } + + fn profile_inverse(r: i32, b: MultiDegree<3>) -> MultiDegree<3> { + let [n, s, bock] = b.coords(); + MultiDegree::new([n + 1, s - r, bock - 1]) + } + + fn differential_length(offset: MultiDegree<3>) -> i32 { + offset.coords()[1] + } +} + pub struct Product { pub b: MultiDegree, /// Whether the product acts on the left or not. This affects the sign in the Leibniz rule. @@ -368,6 +392,30 @@ impl> Sseq { &self.data[b].page_data } + /// Classify generator `i` at degree `b` on page `r` as B (boundary), Z (cycle), or E + /// (supports a differential). Panics if the degree or page is not defined. + pub fn classify(&self, b: MultiDegree, r: i32, i: usize) -> BZE { + let pd = &self.data[b].page_data; + let page = &pd[std::cmp::min(r, pd.len() - 1)]; + BZE::from_page_data(page, i) + } + + /// The B, Z, E generator indices at degree `b` on page `r`. + pub fn bze_indices(&self, b: MultiDegree, r: i32) -> (Vec, Vec, Vec) { + let dim = self.data[b].dimension; + let mut bs = Vec::new(); + let mut zs = Vec::new(); + let mut es = Vec::new(); + for i in 0..dim { + match self.classify(b, r, i) { + BZE::B => bs.push(i), + BZE::Z => zs.push(i), + BZE::E => es.push(i), + } + } + (bs, zs, es) + } + /// Compute the product between `product` and the class `class`. Returns `None` if /// the product is not yet computed. pub fn multiply( diff --git a/ext/examples/benchmarks/secondary-C2 b/ext/examples/benchmarks/secondary-C2 index 397ccad84a..9b2ab4a613 100644 --- a/ext/examples/benchmarks/secondary-C2 +++ b/ext/examples/benchmarks/secondary-C2 @@ -1,12 +1,12 @@ secondary -- C2 "" 30 7 "" -d_2 x_(9, 2, 0) = [0] -d_2 x_(10, 3, 0) = [0] -d_2 x_(17, 3, 0) = [0] -d_2 x_(17, 4, 0) = [1] -d_2 x_(18, 2, 0) = [0] -d_2 x_(18, 4, 0) = [0] -d_2 x_(19, 5, 0) = [1] -d_2 x_(20, 3, 0) = [0] -d_2 x_(22, 3, 0) = [0] -d_2 x_(22, 4, 0) = [0] -d_2 x_(24, 5, 0) = [0] +Z x_(9, 2, 0) +Z x_(10, 3, 0) +Z x_(17, 3, 0) +E d_2 x_(17, 4, 0) = [1] +Z x_(18, 2, 0) +Z x_(18, 4, 0) +E d_2 x_(19, 5, 0) = [1] +Z x_(20, 3, 0) +Z x_(22, 3, 0) +Z x_(22, 4, 0) +Z x_(24, 5, 0) diff --git a/ext/examples/benchmarks/secondary-C2-nassau b/ext/examples/benchmarks/secondary-C2-nassau index 0dc2bb66f5..b96cd88bc0 100644 --- a/ext/examples/benchmarks/secondary-C2-nassau +++ b/ext/examples/benchmarks/secondary-C2-nassau @@ -1,12 +1,12 @@ secondary -- C2 /tmp/test_nassau_c2 30 7 "" -d_2 x_(9, 2, 0) = [0] -d_2 x_(10, 3, 0) = [0] -d_2 x_(17, 3, 0) = [0] -d_2 x_(17, 4, 0) = [1] -d_2 x_(18, 2, 0) = [0] -d_2 x_(18, 4, 0) = [0] -d_2 x_(19, 5, 0) = [1] -d_2 x_(20, 3, 0) = [0] -d_2 x_(22, 3, 0) = [0] -d_2 x_(22, 4, 0) = [0] -d_2 x_(24, 5, 0) = [0] +Z x_(9, 2, 0) +Z x_(10, 3, 0) +Z x_(17, 3, 0) +E d_2 x_(17, 4, 0) = [1] +Z x_(18, 2, 0) +Z x_(18, 4, 0) +E d_2 x_(19, 5, 0) = [1] +Z x_(20, 3, 0) +Z x_(22, 3, 0) +Z x_(22, 4, 0) +Z x_(24, 5, 0) diff --git a/ext/examples/benchmarks/secondary-S_2 b/ext/examples/benchmarks/secondary-S_2 index 5586a84778..4700fd3317 100644 --- a/ext/examples/benchmarks/secondary-S_2 +++ b/ext/examples/benchmarks/secondary-S_2 @@ -1,19 +1,19 @@ secondary -- S_2 "" 30 7 "" -d_2 x_(1, 1, 0) = [0] -d_2 x_(8, 2, 0) = [0] -d_2 x_(15, 1, 0) = [1] -d_2 x_(15, 2, 0) = [0] -d_2 x_(15, 3, 0) = [0] -d_2 x_(15, 4, 0) = [0] -d_2 x_(16, 2, 0) = [0] -d_2 x_(17, 4, 0) = [1] -d_2 x_(17, 5, 0) = [0] -d_2 x_(18, 2, 0) = [0] -d_2 x_(18, 3, 0) = [0] -d_2 x_(18, 4, 0) = [0] -d_2 x_(18, 4, 1) = [1] -d_2 x_(18, 5, 0) = [1] -d_2 x_(19, 3, 0) = [0] -d_2 x_(21, 3, 0) = [0] -d_2 x_(24, 5, 0) = [0] -d_2 x_(30, 5, 0) = [0] +Z x_(1, 1, 0) +Z x_(8, 2, 0) +E d_2 x_(15, 1, 0) = [1] +Z x_(15, 2, 0) +Z x_(15, 3, 0) +Z x_(15, 4, 0) +Z x_(16, 2, 0) +E d_2 x_(17, 4, 0) = [1] +Z x_(17, 5, 0) +Z x_(18, 2, 0) +Z x_(18, 3, 0) +Z x_(18, 4, 0) +E d_2 x_(18, 4, 1) = [1] +E d_2 x_(18, 5, 0) = [1] +Z x_(19, 3, 0) +Z x_(21, 3, 0) +Z x_(24, 5, 0) +Z x_(30, 5, 0) diff --git a/ext/examples/benchmarks/secondary-tmf b/ext/examples/benchmarks/secondary-tmf index 38960aa4f8..e65e8c6afb 100644 --- a/ext/examples/benchmarks/secondary-tmf +++ b/ext/examples/benchmarks/secondary-tmf @@ -1,13 +1,13 @@ secondary -- A-mod-Sq1-Sq2-Sq4 "" 30 7 "" -d_2 x_(1, 1, 0) = [0] -d_2 x_(9, 4, 0) = [0] -d_2 x_(9, 5, 0) = [0] -d_2 x_(12, 3, 0) = [1] -d_2 x_(12, 4, 0) = [1] -d_2 x_(12, 5, 0) = [1] -d_2 x_(15, 3, 0) = [1] -d_2 x_(15, 4, 0) = [1] -d_2 x_(17, 5, 0) = [0] -d_2 x_(18, 4, 0) = [1] -d_2 x_(21, 5, 0) = [0] -d_2 x_(25, 5, 0) = [0] +Z x_(1, 1, 0) +Z x_(9, 4, 0) +Z x_(9, 5, 0) +E d_2 x_(12, 3, 0) = [1] +E d_2 x_(12, 4, 0) = [1] +E d_2 x_(12, 5, 0) = [1] +E d_2 x_(15, 3, 0) = [1] +E d_2 x_(15, 4, 0) = [1] +Z x_(17, 5, 0) +E d_2 x_(18, 4, 0) = [1] +Z x_(21, 5, 0) +Z x_(25, 5, 0) diff --git a/ext/examples/benchmarks/secondary_product-h_0-S_2 b/ext/examples/benchmarks/secondary_product-h_0-S_2 index 22a4c7b1dc..25eab0763e 100644 --- a/ext/examples/benchmarks/secondary_product-h_0-S_2 +++ b/ext/examples/benchmarks/secondary_product-h_0-S_2 @@ -1,38 +1,38 @@ secondary_product -- S_2 "" 30 7 h_0 0 1 [1] -[h_0] [x_(0, 0, 0)] = [1] + λ [0] -[h_0] [x_(0, 1, 0)] = [1] + λ [1] -[h_0] [x_(0, 2, 0)] = [1] + λ [0] -[h_0] [x_(0, 3, 0)] = [1] + λ [1] -[h_0] [x_(0, 4, 0)] = [1] + λ [0] -[h_0] [x_(0, 5, 0)] = [1] + λ [1] -[h_0] [x_(3, 1, 0)] = [1] + λ [1] -[h_0] [x_(3, 2, 0)] = [1] + λ [] -[h_0] [x_(7, 1, 0)] = [1] + λ [0] -[h_0] [x_(7, 2, 0)] = [1] + λ [1] -[h_0] [x_(7, 3, 0)] = [1] + λ [] -[h_0] [x_(8, 2, 0)] = [0] + λ [] -[h_0] [x_(9, 3, 0)] = [0] + λ [0] -[h_0] [x_(9, 4, 0)] = [0] + λ [] -[h_0] [x_(11, 5, 0)] = [1] + λ [1] -[h_0] [x_(14, 2, 0)] = [1] + λ [1] -[h_0] [x_(14, 3, 0)] = [0] + λ [0] -[h_0] [x_(14, 4, 0)] = [1] + λ [1] -[h_0] [x_(14, 5, 0)] = [1] + λ [] -[h_0] λ x_(15, 1, 0) = λ [1] -[h_0] [x_(15, 2, 0)] = [1] + λ [1] -[h_0] [x_(15, 3, 0)] = [1] + λ [0, 0] -[h_0] [x_(15, 4, 0)] = [1, 0] + λ [1] -[h_0] [x_(15, 5, 0)] = [1] + λ [0] -[h_0] [x_(15, 5, 1)] = [0] + λ [0] -[h_0] [x_(17, 3, 0)] = [0] + λ [0] -[h_0] λ x_(17, 4, 0) = λ [1] -[h_0] [x_(17, 5, 0)] = [1] + λ [0] -[h_0] [x_(18, 2, 0)] = [1] + λ [1, 1] -[h_0] [x_(18, 3, 0)] = [1, 0] + λ [1] -[h_0] λ x_(18, 4, 1) = λ [1] -[h_0] [x_(18, 4, 0)] = [0] + λ [] -[h_0] [x_(20, 4, 0)] = [1] + λ [0] -[h_0] [x_(20, 5, 0)] = [1] + λ [] -[h_0] [x_(21, 3, 0)] = [] + λ [0] -[h_0] [x_(23, 4, 0)] = [0] + λ [0] -[h_0] [x_(23, 5, 0)] = [1] + λ [0] +Z [h_0] [x_(0, 0, 0)] = [x_(0, 1, 0)] +Z [h_0] [x_(0, 1, 0)] = [x_(0, 2, 0)] + λx_(0, 3, 0) +Z [h_0] [x_(0, 2, 0)] = [x_(0, 3, 0)] +Z [h_0] [x_(0, 3, 0)] = [x_(0, 4, 0)] + λx_(0, 5, 0) +Z [h_0] [x_(0, 4, 0)] = [x_(0, 5, 0)] +Z [h_0] [x_(0, 5, 0)] = [x_(0, 6, 0)] + λx_(0, 7, 0) +Z [h_0] [x_(3, 1, 0)] = [x_(3, 2, 0)] + λx_(3, 3, 0) +Z [h_0] [x_(3, 2, 0)] = [x_(3, 3, 0)] +Z [h_0] [x_(7, 1, 0)] = [x_(7, 2, 0)] +Z [h_0] [x_(7, 2, 0)] = [x_(7, 3, 0)] + λx_(7, 4, 0) +Z [h_0] [x_(7, 3, 0)] = [x_(7, 4, 0)] +Z [h_0] [x_(8, 2, 0)] = 0 +Z [h_0] [x_(9, 3, 0)] = 0 +Z [h_0] [x_(9, 4, 0)] = 0 +Z [h_0] [x_(11, 5, 0)] = [x_(11, 6, 0)] + λx_(11, 7, 0) +Z [h_0] [x_(14, 2, 0)] = [x_(14, 3, 0)] + λx_(14, 4, 0) +B [h_0] [x_(14, 3, 0)] = 0 +Z [h_0] [x_(14, 4, 0)] = [x_(14, 5, 0)] + λx_(14, 6, 0) +Z [h_0] [x_(14, 5, 0)] = [x_(14, 6, 0)] +E [h_0] λ x_(15, 1, 0) = λ [1] +Z [h_0] [x_(15, 2, 0)] = [x_(15, 3, 0)] + λx_(15, 4, 0) +Z [h_0] [x_(15, 3, 0)] = [x_(15, 4, 0)] +Z [h_0] [x_(15, 4, 0)] = [x_(15, 5, 0)] + λx_(15, 6, 0) +Z [h_0] [x_(15, 5, 0)] = [x_(15, 6, 0)] +Z [h_0] [x_(15, 5, 1)] = 0 +Z [h_0] [x_(17, 3, 0)] = 0 +E [h_0] λ x_(17, 4, 0) = λ [1] +Z [h_0] [x_(17, 5, 0)] = [x_(17, 6, 0)] +Z [h_0] [x_(18, 2, 0)] = [x_(18, 3, 0)] + λ(x_(18, 4, 0) + x_(18, 4, 1)) +Z [h_0] [x_(18, 3, 0)] = [x_(18, 4, 0)] + λx_(18, 5, 0) +E [h_0] λ x_(18, 4, 1) = λ [1] +Z [h_0] [x_(18, 4, 0)] = 0 +Z [h_0] [x_(20, 4, 0)] = [x_(20, 5, 0)] +Z [h_0] [x_(20, 5, 0)] = [x_(20, 6, 0)] +Z [h_0] [x_(21, 3, 0)] = 0 +Z [h_0] [x_(23, 4, 0)] = 0 +Z [h_0] [x_(23, 5, 0)] = [x_(23, 6, 0)] diff --git a/ext/examples/benchmarks/secondary_product-h_0-S_2-nassau b/ext/examples/benchmarks/secondary_product-h_0-S_2-nassau index 75dbb090de..399e06f5ed 100644 --- a/ext/examples/benchmarks/secondary_product-h_0-S_2-nassau +++ b/ext/examples/benchmarks/secondary_product-h_0-S_2-nassau @@ -1,38 +1,38 @@ secondary_product -- S_2 /tmp/test_nassau_s_2 30 7 h_0 0 1 [1] -[h_0] [x_(0, 0, 0)] = [1] + λ [0] -[h_0] [x_(0, 1, 0)] = [1] + λ [1] -[h_0] [x_(0, 2, 0)] = [1] + λ [0] -[h_0] [x_(0, 3, 0)] = [1] + λ [1] -[h_0] [x_(0, 4, 0)] = [1] + λ [0] -[h_0] [x_(0, 5, 0)] = [1] + λ [1] -[h_0] [x_(3, 1, 0)] = [1] + λ [1] -[h_0] [x_(3, 2, 0)] = [1] + λ [] -[h_0] [x_(7, 1, 0)] = [1] + λ [0] -[h_0] [x_(7, 2, 0)] = [1] + λ [1] -[h_0] [x_(7, 3, 0)] = [1] + λ [] -[h_0] [x_(8, 2, 0)] = [0] + λ [] -[h_0] [x_(9, 3, 0)] = [0] + λ [0] -[h_0] [x_(9, 4, 0)] = [0] + λ [] -[h_0] [x_(11, 5, 0)] = [1] + λ [1] -[h_0] [x_(14, 2, 0)] = [1] + λ [0] -[h_0] [x_(14, 3, 0)] = [0] + λ [1] -[h_0] [x_(14, 4, 0)] = [1] + λ [0] -[h_0] [x_(14, 5, 0)] = [1] + λ [] -[h_0] λ x_(15, 1, 0) = λ [1] -[h_0] [x_(15, 2, 0)] = [1] + λ [1] -[h_0] [x_(15, 3, 0)] = [1] + λ [0, 0] -[h_0] [x_(15, 4, 0)] = [1, 0] + λ [1] -[h_0] [x_(15, 5, 0)] = [1] + λ [0] -[h_0] [x_(15, 5, 1)] = [0] + λ [0] -[h_0] [x_(17, 3, 0)] = [0] + λ [0] -[h_0] λ x_(17, 4, 0) = λ [1] -[h_0] [x_(17, 5, 0)] = [1] + λ [0] -[h_0] [x_(18, 2, 0)] = [1] + λ [1, 0] -[h_0] [x_(18, 3, 0)] = [1, 0] + λ [0] -[h_0] λ x_(18, 4, 1) = λ [1] -[h_0] [x_(18, 4, 0)] = [0] + λ [] -[h_0] [x_(20, 4, 0)] = [1] + λ [1] -[h_0] [x_(20, 5, 0)] = [1] + λ [] -[h_0] [x_(21, 3, 0)] = [] + λ [0] -[h_0] [x_(23, 4, 0)] = [0] + λ [0] -[h_0] [x_(23, 5, 0)] = [1] + λ [0] +Z [h_0] [x_(0, 0, 0)] = [x_(0, 1, 0)] +Z [h_0] [x_(0, 1, 0)] = [x_(0, 2, 0)] + λx_(0, 3, 0) +Z [h_0] [x_(0, 2, 0)] = [x_(0, 3, 0)] +Z [h_0] [x_(0, 3, 0)] = [x_(0, 4, 0)] + λx_(0, 5, 0) +Z [h_0] [x_(0, 4, 0)] = [x_(0, 5, 0)] +Z [h_0] [x_(0, 5, 0)] = [x_(0, 6, 0)] + λx_(0, 7, 0) +Z [h_0] [x_(3, 1, 0)] = [x_(3, 2, 0)] + λx_(3, 3, 0) +Z [h_0] [x_(3, 2, 0)] = [x_(3, 3, 0)] +Z [h_0] [x_(7, 1, 0)] = [x_(7, 2, 0)] +Z [h_0] [x_(7, 2, 0)] = [x_(7, 3, 0)] + λx_(7, 4, 0) +Z [h_0] [x_(7, 3, 0)] = [x_(7, 4, 0)] +Z [h_0] [x_(8, 2, 0)] = 0 +Z [h_0] [x_(9, 3, 0)] = 0 +Z [h_0] [x_(9, 4, 0)] = 0 +Z [h_0] [x_(11, 5, 0)] = [x_(11, 6, 0)] + λx_(11, 7, 0) +Z [h_0] [x_(14, 2, 0)] = [x_(14, 3, 0)] +B [h_0] [x_(14, 3, 0)] = λx_(14, 5, 0) +Z [h_0] [x_(14, 4, 0)] = [x_(14, 5, 0)] +Z [h_0] [x_(14, 5, 0)] = [x_(14, 6, 0)] +E [h_0] λ x_(15, 1, 0) = λ [1] +Z [h_0] [x_(15, 2, 0)] = [x_(15, 3, 0)] + λx_(15, 4, 0) +Z [h_0] [x_(15, 3, 0)] = [x_(15, 4, 0)] +Z [h_0] [x_(15, 4, 0)] = [x_(15, 5, 0)] + λx_(15, 6, 0) +Z [h_0] [x_(15, 5, 0)] = [x_(15, 6, 0)] +Z [h_0] [x_(15, 5, 1)] = 0 +Z [h_0] [x_(17, 3, 0)] = 0 +E [h_0] λ x_(17, 4, 0) = λ [1] +Z [h_0] [x_(17, 5, 0)] = [x_(17, 6, 0)] +Z [h_0] [x_(18, 2, 0)] = [x_(18, 3, 0)] + λx_(18, 4, 0) +Z [h_0] [x_(18, 3, 0)] = [x_(18, 4, 0)] +E [h_0] λ x_(18, 4, 1) = λ [1] +Z [h_0] [x_(18, 4, 0)] = 0 +Z [h_0] [x_(20, 4, 0)] = [x_(20, 5, 0)] + λx_(20, 6, 0) +Z [h_0] [x_(20, 5, 0)] = [x_(20, 6, 0)] +Z [h_0] [x_(21, 3, 0)] = 0 +Z [h_0] [x_(23, 4, 0)] = 0 +Z [h_0] [x_(23, 5, 0)] = [x_(23, 6, 0)] diff --git a/ext/examples/benchmarks/secondary_product-v_1-C2 b/ext/examples/benchmarks/secondary_product-v_1-C2 index 77b656aa70..21922aff35 100644 --- a/ext/examples/benchmarks/secondary_product-v_1-C2 +++ b/ext/examples/benchmarks/secondary_product-v_1-C2 @@ -1,36 +1,36 @@ secondary_product -- C2 "" 30 7 "" v_1 2 1 [1] -[v_1] [x_(0, 0, 0)] = [1] + λ [0] -[v_1] [x_(0, 1, 0)] = [1] + λ [] -[v_1] [x_(1, 1, 0)] = [1] + λ [] -[v_1] [x_(2, 2, 0)] = [1] + λ [] -[v_1] [x_(6, 2, 0)] = [0] + λ [0] -[v_1] [x_(7, 1, 0)] = [1] + λ [1, 1] -[v_1] [x_(7, 2, 0)] = [1, 0] + λ [1] -[v_1] [x_(7, 3, 0)] = [0] + λ [0] -[v_1] [x_(7, 4, 0)] = [0] + λ [] -[v_1] [x_(8, 2, 0)] = [1] + λ [0] -[v_1] [x_(8, 3, 0)] = [1] + λ [1] -[v_1] [x_(9, 3, 0)] = [] + λ [0] -[v_1] [x_(9, 4, 0)] = [1] + λ [0] -[v_1] [x_(9, 5, 0)] = [1] + λ [] -[v_1] [x_(14, 3, 0)] = [] + λ [1] -[v_1] [x_(14, 4, 0)] = [1] + λ [0] -[v_1] [x_(14, 5, 0)] = [1] + λ [0] -[v_1] λ x_(15, 1, 0) = λ [1] -[v_1] [x_(15, 2, 0)] = [1] + λ [1] -[v_1] [x_(15, 3, 0)] = [0] + λ [] -[v_1] [x_(15, 4, 0)] = [] + λ [0] -[v_1] [x_(15, 5, 0)] = [0] + λ [0] -[v_1] [x_(15, 5, 1)] = [1] + λ [0] -[v_1] [x_(16, 2, 0)] = [1] + λ [0] -[v_1] [x_(17, 3, 0)] = [1] + λ [0] -[v_1] λ x_(17, 4, 0) = λ [1] -[v_1] [x_(18, 2, 0)] = [0] + λ [0] -[v_1] [x_(18, 3, 0)] = [0] + λ [] -[v_1] [x_(19, 3, 0)] = [] + λ [0] -[v_1] [x_(20, 4, 0)] = [1] + λ [] -[v_1] [x_(21, 3, 0)] = [0, 0] + λ [0] -[v_1] [x_(21, 5, 0)] = [] + λ [1] -[v_1] [x_(22, 4, 0)] = [0] + λ [0] -[v_1] [x_(23, 4, 0)] = [1] + λ [] -[v_1] [x_(24, 5, 0)] = [1] + λ [0] +Z [v_1] [x_(0, 0, 0)] = [x_(2, 1, 0)] +Z [v_1] [x_(0, 1, 0)] = [x_(2, 2, 0)] +Z [v_1] [x_(1, 1, 0)] = [x_(3, 2, 0)] +Z [v_1] [x_(2, 2, 0)] = [x_(4, 3, 0)] +Z [v_1] [x_(6, 2, 0)] = 0 +Z [v_1] [x_(7, 1, 0)] = [x_(9, 2, 0)] + λ(x_(9, 3, 0) + x_(9, 3, 1)) +Z [v_1] [x_(7, 2, 0)] = [x_(9, 3, 0)] + λx_(9, 4, 0) +Z [v_1] [x_(7, 3, 0)] = 0 +Z [v_1] [x_(7, 4, 0)] = 0 +Z [v_1] [x_(8, 2, 0)] = [x_(10, 3, 0)] +Z [v_1] [x_(8, 3, 0)] = [x_(10, 4, 0)] + λx_(10, 5, 0) +Z [v_1] [x_(9, 3, 0)] = 0 +Z [v_1] [x_(9, 4, 0)] = [x_(11, 5, 0)] +Z [v_1] [x_(9, 5, 0)] = [x_(11, 6, 0)] +B [v_1] [x_(14, 3, 0)] = λx_(16, 5, 0) +Z [v_1] [x_(14, 4, 0)] = [x_(16, 5, 0)] +Z [v_1] [x_(14, 5, 0)] = [x_(16, 6, 0)] +E [v_1] λ x_(15, 1, 0) = λ [1] +Z [v_1] [x_(15, 2, 0)] = [x_(17, 3, 0)] + λx_(17, 4, 0) +Z [v_1] [x_(15, 3, 0)] = 0 +Z [v_1] [x_(15, 4, 0)] = 0 +Z [v_1] [x_(15, 5, 0)] = 0 +Z [v_1] [x_(15, 5, 1)] = [x_(17, 6, 0)] +Z [v_1] [x_(16, 2, 0)] = [x_(18, 3, 0)] +Z [v_1] [x_(17, 3, 0)] = [x_(19, 4, 0)] +E [v_1] λ x_(17, 4, 0) = λ [1] +Z [v_1] [x_(18, 2, 0)] = 0 +Z [v_1] [x_(18, 3, 0)] = 0 +Z [v_1] [x_(19, 3, 0)] = 0 +Z [v_1] [x_(20, 4, 0)] = [x_(22, 5, 0)] +Z [v_1] [x_(21, 3, 0)] = 0 +Z [v_1] [x_(21, 5, 0)] = λx_(23, 7, 0) +Z [v_1] [x_(22, 4, 0)] = 0 +Z [v_1] [x_(23, 4, 0)] = [x_(25, 5, 0)] +Z [v_1] [x_(24, 5, 0)] = [x_(26, 6, 0)] diff --git a/ext/examples/lambda2.rs b/ext/examples/lambda2.rs new file mode 100644 index 0000000000..7d24ea396d --- /dev/null +++ b/ext/examples/lambda2.rs @@ -0,0 +1,239 @@ +//! Computes tables for $S/\lambda^2$ (the cofiber of the Adams $d_2$ differential). +//! +//! This implements the four tables from "The Cofiber of the Adams d2 Differential": +//! +//! - **Table I**: B/Z/E decomposition of Ext, adapted to $d_2$. +//! - **Table II**: Products in Ext expressed in the standard basis. +//! - **Table III**: Conical basis for $\pi(S/\lambda^2)$. +//! - **Table IV**: Products in $\pi(S/\lambda^2)$ with commutators. +//! +//! # Usage +//! ```shell +//! cargo run --example lambda2 S_2 /tmp/save 40 20 +//! ``` +//! +//! Set `TABLE=I`, `TABLE=II`, `TABLE=III`, or `TABLE=IV` to print a single table; otherwise all +//! tables are printed. Supports sharding via `SECONDARY_JOB` (see [`secondary`](../secondary)). + +use std::sync::Arc; + +use ext::{ + chain_complex::FreeChainComplex, + ext_algebra::{BZE, ExtAlgebra, secondary::SecondaryExtAlgebra}, + secondary::LAMBDA_BIDEGREE, + utils::query_module, +}; +use sseq::coordinates::{Bidegree, BidegreeGenerator}; + +fn main() -> anyhow::Result<()> { + ext::utils::init_logging()?; + + let table_var = std::env::var("TABLE").ok(); + let table = table_var.as_deref().unwrap_or("all"); + + let resolution = Arc::new(query_module(Some(algebra::AlgebraType::Milnor), true)?); + let e2 = Arc::new(ExtAlgebra::from_resolution(Arc::clone(&resolution))?); + + // The tables are defined for the sphere: Table II reads multiplicands from `e2.resolution()` + // and feeds them to `e2.unit_generator(..)`, which is only the same basis when `M == k`. Bail + // early on non-unit modules rather than print products from a mismatched basis. + anyhow::ensure!( + e2.is_unit(), + "lambda2 only supports the unit module (e.g. S_2); got a non-unit module" + ); + + let sec_e2 = Arc::new(SecondaryExtAlgebra::new(Arc::clone(&e2))); + + if let Some(s) = ext::utils::secondary_job() { + sec_e2.compute_partial(s); + return Ok(()); + } + + sec_e2.extend_all(); + + match table { + "I" => print_table_i(&e2, &sec_e2), + "II" => print_table_ii(&e2, &sec_e2), + "III" => print_table_iii(&e2, &sec_e2), + "IV" => print_table_iv(&e2, &sec_e2), + "all" => { + println!("=== TABLE I: B/Z/E decomposition ==="); + print_table_i(&e2, &sec_e2); + println!("\n=== TABLE II: Ext products ==="); + print_table_ii(&e2, &sec_e2); + println!("\n=== TABLE III: Conical basis for π(S/λ²) ==="); + print_table_iii(&e2, &sec_e2); + println!("\n=== TABLE IV: Products in π(S/λ²) ==="); + print_table_iv(&e2, &sec_e2); + } + _ => anyhow::bail!("unknown table: {table}; expected I, II, III, IV, or all"), + } + + Ok(()) +} + +/// Table I: B/Z/E decomposition of Ext adapted to d2. +/// +/// For each bidegree (n, s), classifies each Ext generator as: +/// - **Z** (d2-cycle, not a boundary) — survives to E3. +/// - **B** (boundary) — in the image of d2. +/// - **E** (supports d2) — d2(x) ≠ 0; prints the d2 value. +fn print_table_i(e2: &ExtAlgebra, sec_e2: &SecondaryExtAlgebra) +where + CC: FreeChainComplex + ext::chain_complex::AugmentedChainComplex, + CC::Algebra: algebra::pair_algebra::PairAlgebra, +{ + for b in e2.resolution().iter_nonzero_stem() { + let dim = e2.dimension(b); + for i in 0..dim { + let g = BidegreeGenerator::new(b, i); + match sec_e2.classify(g) { + BZE::Z => println!("Z x_{g}"), + BZE::B => println!("B x_{g}"), + BZE::E => { + let elem = e2.generator(g); + if let Some(d2) = sec_e2.d2(&elem) { + let target: Vec = d2.vec().iter().collect(); + println!("E x_{g} d2 = {target:?}"); + } else { + println!("E x_{g} d2 = (not computed)"); + } + } + } + } + } +} + +/// Table II: Products in Ext expressed in the standard basis. +/// +/// For each pair of generators (x, y) with x ≤ y (by bidegree then index), computes x · y. +fn print_table_ii(e2: &ExtAlgebra, _sec_e2: &SecondaryExtAlgebra) +where + CC: FreeChainComplex + ext::chain_complex::AugmentedChainComplex, + CC::Algebra: algebra::pair_algebra::PairAlgebra, +{ + let gens: Vec = e2 + .resolution() + .iter_nonzero_stem() + .flat_map(|b| (0..e2.dimension(b)).map(move |i| BidegreeGenerator::new(b, i))) + .collect(); + + for (idx_x, &x_gen) in gens.iter().enumerate() { + let x = e2.generator(x_gen); + for &y_gen in &gens[idx_x..] { + let y = e2.unit_generator(y_gen); + if let Some(prod) = e2.try_multiply(&x, &y) { + let coords: Vec = prod.vec().iter().collect(); + if coords.iter().any(|&c| c != 0) { + println!("x_{x_gen} · x_{y_gen} = {coords:?}"); + } + } + } + } +} + +/// Table III: Conical basis for π(S/λ²). +/// +/// Lists the E3 = E∞ generators of S/λ² at each tridegree (n, s, bock), annotated with their +/// B/Z/E type. The conical basis Xπ has four element types per Condition 3.2 of the paper: +/// - x⁰_π for x ∈ B: (n, s, 0) +/// - x⁰_π for x ∈ Z: (n, s, 0) +/// - x¹_π for x ∈ Z: (n, s, 1) +/// - x¹_π for x ∈ E: (n, s, 1) +fn print_table_iii(e2: &ExtAlgebra, sec_e2: &SecondaryExtAlgebra) +where + CC: FreeChainComplex + ext::chain_complex::AugmentedChainComplex, + CC::Algebra: algebra::pair_algebra::PairAlgebra, +{ + for b in e2.resolution().iter_stem() { + if e2.dimension(b) == 0 { + continue; + } + for g in sec_e2.pi_basis(b) { + let [n, s] = g.bidegree().coords(); + let bock = g.weight().as_i32(); + let x = BidegreeGenerator::new(g.bidegree(), g.idx()); + println!("{} x_{x}^{bock} ({n}, {s}, {bock})", g.bze()); + } + } +} + +/// Table IV: Products in π(S/λ²) with commutators. +/// +/// For each pair (x, y) where x ∈ Z (surviving cycle) and y runs over E3-surviving classes of +/// the unit at each bidegree, computes the secondary product x · y and projects to π(S/λ²) via +/// `to_pi`. The result is expressed in the E3 subquotient at each weight. +/// +/// The commutator [x, y] = x·y − y·x equals 2·(x·y) when both stems are odd (Proposition 6.4), +/// and vanishes when at least one stem is even. +fn print_table_iv(e2: &ExtAlgebra, sec_e2: &SecondaryExtAlgebra) +where + CC: FreeChainComplex + ext::chain_complex::AugmentedChainComplex, + CC::Algebra: algebra::pair_algebra::PairAlgebra, +{ + let z_gens: Vec = e2 + .resolution() + .iter_nonzero_stem() + .filter(|b| b.s() >= 1) + .flat_map(|b| { + let dim = e2.dimension(b); + (0..dim) + .filter(move |&i| sec_e2.classify(BidegreeGenerator::new(b, i)) == BZE::Z) + .map(move |i| BidegreeGenerator::new(b, i)) + }) + .collect(); + + for &x_gen in &z_gens { + let x = e2.generator(x_gen); + let shift = x.degree(); + + for b in e2.unit().iter_nonzero_stem() { + if !e2 + .resolution() + .has_computed_bidegree(b + shift + LAMBDA_BIDEGREE) + { + continue; + } + if !e2 + .resolution() + .has_computed_bidegree(b + shift - Bidegree::s_t(1, 0)) + { + continue; + } + + let target_dim = e2.dimension(b + shift); + let lambda_dim = e2.dimension(b + shift + LAMBDA_BIDEGREE); + if target_dim == 0 && lambda_dim == 0 { + continue; + } + + let both_odd = shift.n() % 2 != 0 && b.n() % 2 != 0; + + for prod in sec_e2.secondary_multiply_into(&x, b) { + let (pi_ext, pi_lambda) = sec_e2.to_pi(&prod.value); + if pi_ext.is_zero() && pi_lambda.is_zero() { + continue; + } + + let comm = if both_odd { + "[x,y] = 2(x·y)" + } else { + "[x,y] = 0" + }; + + let mut parts = Vec::new(); + if !pi_ext.is_zero() { + parts.push(format!("[{pi_ext}]")); + } + if !pi_lambda.is_zero() { + parts.push(format!("{pi_lambda}")); + } + let value_str = parts.join(" + "); + println!( + "x_{x_gen} · [{src}] = {value_str} {comm}", + src = prod.source.to_basis_string(), + ); + } + } + } +} diff --git a/ext/examples/secondary.rs b/ext/examples/secondary.rs index 44c5cc3b0d..a143f166a9 100644 --- a/ext/examples/secondary.rs +++ b/ext/examples/secondary.rs @@ -50,10 +50,10 @@ use std::sync::Arc; use algebra::module::Module; use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - ext_algebra::{ExtAlgebra, secondary::SecondaryExtAlgebra}, + ext_algebra::{BZE, ExtAlgebra, secondary::SecondaryExtAlgebra}, utils::query_module, }; -use sseq::coordinates::Bidegree; +use sseq::coordinates::{Bidegree, BidegreeGenerator}; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; @@ -74,22 +74,29 @@ fn main() -> anyhow::Result<()> { let e2 = sec_e2.ext_algebra(); let d2_shift = Bidegree::n_s(-1, 2); - // Iterate through the target of the d2, in the same order as before. + // Iterate through the target of the d2. We omit elements whose d2 target bidegree is zero. for b in e2.resolution().iter_nonzero_stem() { if b.s() < 3 { continue; } - // The source of the d2 must be computed (its degree may exceed what `module(b.s() - 2)` - // reaches when resolving through a stem). if b.t() - 1 > e2.resolution().module(b.s() - 2).max_computed_degree() { continue; } - for g in e2.basis(b - d2_shift) { - if let Some(dx) = sec_e2.d2(&e2.generator(g)) { - let entry: Vec = dx.vec().iter().collect(); - println!("d_2 x_{g} = {entry:?}"); + for i in 0..e2.dimension(b - d2_shift) { + let g = BidegreeGenerator::new(b - d2_shift, i); + match sec_e2.classify(g) { + BZE::Z => println!("Z x_{g}"), + BZE::B => println!("B x_{g}"), + BZE::E => { + if let Some(dx) = sec_e2.d2(&e2.generator(g)) { + let entry: Vec = dx.vec().iter().collect(); + println!("E d_2 x_{g} = {entry:?}"); + } else { + println!("E x_{g} d2 = (not computed)"); + } + } } } } diff --git a/ext/examples/secondary_massey.rs b/ext/examples/secondary_massey.rs index 9c8169a4fb..4c3cd0e401 100644 --- a/ext/examples/secondary_massey.rs +++ b/ext/examples/secondary_massey.rs @@ -30,7 +30,7 @@ use fp::{ vector::FpVector, }; use itertools::Itertools; -use sseq::coordinates::{Bidegree, BidegreeElement}; +use sseq::coordinates::Bidegree; struct HomData { name: String, @@ -386,44 +386,9 @@ fn main() -> anyhow::Result<()> { let mb = b.underlying().get_map(c.s() + b_shift.s()).hom_k(c.t()); for g in e3_kernel.iter() { - // Print name - { - print!("<{a_name}, {b_name}, "); - let has_ext = { - let ext_part = g.restrict(0, target_num_gens); - if ext_part.iter_nonzero().count() > 0 { - print!( - "[{basis_string}]", - basis_string = - BidegreeElement::new(c, ext_part.to_owned()).to_basis_string() - ); - true - } else { - false - } - }; - - let lambda_part = g.restrict(target_num_gens, target_all_gens); - let num_entries = lambda_part.iter_nonzero().count(); - if num_entries > 0 { - if has_ext { - print!(" + "); - } - print!("λ"); - - let basis_string = BidegreeElement::new( - c + LAMBDA_BIDEGREE, - g.restrict(target_num_gens, target_all_gens).to_owned(), - ) - .to_basis_string(); - if num_entries == 1 { - print!("{basis_string}",); - } else { - print!("({basis_string})",); - } - } - print!("> = ±"); - } + // Print name. `g` is the concatenated `[ext | λ]` class of the multiplicand. + let elt = SecondaryElement::from_concatenated(c, g, target_num_gens); + print!("<{a_name}, {b_name}, {elt}> = ±"); scratch0.clear(); scratch0.resize(source_num_gens, 0); diff --git a/ext/examples/secondary_product.rs b/ext/examples/secondary_product.rs index b024a8ce2e..6cce13b2b1 100644 --- a/ext/examples/secondary_product.rs +++ b/ext/examples/secondary_product.rs @@ -25,7 +25,7 @@ use std::sync::Arc; use algebra::module::Module; use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - ext_algebra::{ExtAlgebra, SecondaryExtAlgebra}, + ext_algebra::{BZE, ExtAlgebra, SecondaryExtAlgebra}, secondary::{LAMBDA_BIDEGREE, SecondaryLift}, utils::query_module, }; @@ -98,25 +98,33 @@ fn main() -> anyhow::Result<()> { let page = sec_e2.unit_page_data(b); - // First the products with non-surviving classes: these are just λ times the (primary) - // product, read off the multiplication map. + // Products with non-surviving classes (E): just λ times the primary product. if target_num_gens > 0 && let Some(rows) = e2.multiply_into(&x, b) { - for i in page.complement_pivots() { + for i in 0..e2.unit_dimension(b) { + if BZE::from_page_data(&page, i) != BZE::E { + continue; + } let g = BidegreeGenerator::new(b, i); let entry: Vec = rows.row(i).iter().collect(); - println!("{disp} λ x_{g} = λ {entry:?}"); + println!("E {disp} λ x_{g} = λ {entry:?}"); } } - // Now the genuinely secondary products with surviving classes. + // Secondary products with surviving classes (B and Z). The source is a whole class, so we + // classify it by membership in the boundary subspace rather than by its first basis index: + // boundaries live in `page.zeros()`, every other surviving class is a non-boundary cycle. for prod in sec_e2.secondary_multiply_into(&x, b) { + let label = if page.zeros().contains(prod.source.vec()) { + "B" + } else { + "Z" + }; println!( - "{disp} [{basis}] = {ext} + λ {lambda}", + "{label} {disp} [{basis}] = {value}", basis = prod.source.to_basis_string(), - ext = prod.ext_part.as_slice(), - lambda = prod.lambda_part.as_slice(), + value = prod.value, ); } } diff --git a/ext/src/ext_algebra/mod.rs b/ext/src/ext_algebra/mod.rs index f229886636..6fe016c07b 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -26,9 +26,10 @@ use std::sync::Arc; use dashmap::DashMap; use fp::{matrix::Matrix, prime::ValidPrime, vector::FpVector}; -use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator}; +use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator, MultiDegree}; -pub use self::secondary::{SecondaryExtAlgebra, SecondaryProduct}; +pub use self::secondary::{BZE, PiElement, PiGenerator, SecondaryExtAlgebra, SecondaryProduct}; +pub use crate::secondary::{SecondaryDegree, SecondaryElement, SecondaryGenerator, Weight}; use crate::{ chain_complex::{AugmentedChainComplex, FreeChainComplex}, resolution_homomorphism::ResolutionHomomorphism, @@ -106,6 +107,24 @@ impl ExtAlgebra { self.resolution.prime() } + /// The trigraded spectral sequence for this Ext algebra, built from the *current* resolution + /// state. Generators live at weight 0 only; weight 1 is unpopulated, so $d_2$ is trivially zero + /// and $E_2 = E_3 = E_\infty$. Every generator classifies as Z. + /// + /// This is computed on demand rather than cached so it always reflects how far the resolution + /// has been extended (e.g. by [`compute_through_bidegree`](Self::compute_through_bidegree)); + /// building it is cheap — one `set_dimension` per stem bidegree, with no differentials. + pub fn sseq(&self) -> sseq::Sseq<3, sseq::AdamsLambda2> { + let p = self.resolution.prime(); + let mut sseq = sseq::Sseq::new(p); + for b in self.resolution.iter_stem() { + let dim = self.resolution.number_of_gens_in_bidegree(b); + let [n, s] = b.coords(); + sseq.set_dimension(MultiDegree::new([n, s, 0]), dim); + } + sseq + } + /// Ensure both the resolution and the unit are computed through the given bidegree. pub fn compute_through_bidegree(&self, b: Bidegree) { self.unit.compute_through_bidegree(b); diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index 983a7c60ac..0d2ec84d2e 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -12,19 +12,26 @@ //! algebra is implemented here. The layer is split out from [`ExtAlgebra`] because the secondary //! machinery requires `CC::Algebra: PairAlgebra`, a bound the primary layer does not impose. -use std::sync::{Arc, Mutex}; +use std::{ + fmt, + sync::{Arc, Mutex}, +}; use algebra::pair_algebra::PairAlgebra; use dashmap::DashMap; use fp::{matrix::Subquotient, prime::Prime, vector::FpVector}; -use sseq::coordinates::{Bidegree, BidegreeElement}; +pub use sseq::coordinates::BZE; +use sseq::coordinates::{ + Bidegree, BidegreeElement, BidegreeGenerator, MultiDegree, MultiDegreeElement, +}; use super::ExtAlgebra; use crate::{ chain_complex::FreeChainComplex, resolution_homomorphism::ResolutionHomomorphism, secondary::{ - LAMBDA_BIDEGREE, SecondaryLift, SecondaryResolution, SecondaryResolutionHomomorphism, + LAMBDA_BIDEGREE, SecondaryDegree, SecondaryElement, SecondaryGenerator, SecondaryLift, + SecondaryResolution, SecondaryResolutionHomomorphism, Weight, }, }; @@ -33,11 +40,129 @@ use crate::{ pub struct SecondaryProduct { /// The multiplicand: an $E_3$-surviving generator of the unit at the queried bidegree `b`. pub source: BidegreeElement, - /// The $\Ext$ part of the product, in bidegree `b + x.degree()`. - pub ext_part: FpVector, - /// The $\lambda$ part of the product, in bidegree `b + x.degree() + LAMBDA_BIDEGREE`, already - /// reduced by the image of $d_2$. - pub lambda_part: FpVector, + /// The product `x · source`, a class in the secondary ($\Mod_{C\lambda^2}$) homotopy with base + /// bidegree `b + x.degree()`. Its $\lambda$ part is already reduced by the image of $d_2$. + pub value: SecondaryElement, +} + +/// A conical basis generator of $\pi(S/\lambda^2)$. +/// +/// Each Ext generator at bidegree $(n, s)$ contributes to the conical basis at one or both weights, +/// determined by its Adams BZE classification (see +/// [`classify`](SecondaryExtAlgebra::classify)): +/// - **B**: weight 0 only (killed at weight 1 by $d_2$ boundaries). +/// - **Z**: both weights (permanent cycle). +/// - **E**: weight 1 only (killed at weight 0 by supporting $d_2$). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PiGenerator { + bidegree: Bidegree, + weight: Weight, + bze: BZE, + idx: usize, +} + +impl PiGenerator { + pub fn new(bidegree: Bidegree, weight: Weight, bze: BZE, idx: usize) -> Self { + Self { + bidegree, + weight, + bze, + idx, + } + } + + pub fn bidegree(&self) -> Bidegree { + self.bidegree + } + + pub fn weight(&self) -> Weight { + self.weight + } + + pub fn bze(&self) -> BZE { + self.bze + } + + pub fn idx(&self) -> usize { + self.idx + } +} + +impl fmt::Display for PiGenerator { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let g = BidegreeGenerator::new(self.bidegree, self.idx); + let w = self.weight.as_i32(); + write!(f, "{} x_{g}^{w}", self.bze) + } +} + +/// An element in the $E_3 = E_\infty$ page of $\pi(S/\lambda^2)$ at a specific weight. +/// +/// The coordinates are in the [`Subquotient`] generator basis of $E_3$ at the given bidegree and +/// weight: `coords()[k]` is the coefficient of the `k`-th surviving $E_3$ basis class. Each such +/// class is named by its *leading* (pivot) ambient Ext generator, recorded in +/// [`basis_indices`](Self::basis_indices)`[k]`. The two run in lockstep — `basis_indices` is the +/// list of generator-subspace pivot columns, which is exactly the indexing +/// [`Subquotient::reduce`] returns coefficients for. A class that survives as a non-unit vector is +/// thus displayed by its leading generator, not expanded into ambient coordinates. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PiElement { + bidegree: Bidegree, + weight: Weight, + coords: Vec, + basis_indices: Vec, +} + +impl PiElement { + pub fn bidegree(&self) -> Bidegree { + self.bidegree + } + + pub fn weight(&self) -> Weight { + self.weight + } + + /// Coordinates in the $E_3$ generator basis. `coords()[k]` is the coefficient of the `k`-th + /// surviving class, whose leading ambient index is [`basis_indices()`](Self::basis_indices)`[k]`. + pub fn coords(&self) -> &[u32] { + &self.coords + } + + /// The leading (pivot) ambient Ext generator index of each surviving $E_3$ basis class, in the + /// same order as [`coords`](Self::coords). + pub fn basis_indices(&self) -> &[usize] { + &self.basis_indices + } + + pub fn is_zero(&self) -> bool { + self.coords.iter().all(|&c| c == 0) + } +} + +impl fmt::Display for PiElement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let w = self.weight.as_i32(); + let mut first = true; + for (&c, &idx) in self.coords.iter().zip(&self.basis_indices) { + if c == 0 { + continue; + } + if !first { + write!(f, " + ")?; + } + first = false; + let g = BidegreeGenerator::new(self.bidegree, idx); + if c == 1 { + write!(f, "x_{g}^{w}")?; + } else { + write!(f, "{c} x_{g}^{w}")?; + } + } + if first { + write!(f, "0")?; + } + Ok(()) + } } /// The secondary layer over an [`ExtAlgebra`]: the $d_2$ differential and the $\Mod_{C\lambda^2}$ @@ -54,6 +179,9 @@ where res_sseq: Mutex>>>, /// $E_3$ page of the unit, filled by [`extend_all`](Self::extend_all). unit_sseq: Mutex>>>, + /// Trigraded spectral sequence for $S/\lambda^2$. Coordinates are `(n, s, bock)` = (stem, + /// Adams filtration, Bockstein degree). Filled by [`extend_all`](Self::extend_all). + lambda2_sseq: Mutex>>>, /// Secondary lift of the multiplication map, cached per multiplier class `(degree, coords)`. secondary_products: DashMap>>, } @@ -77,6 +205,7 @@ where unit_lift, res_sseq: Mutex::new(None), unit_sseq: Mutex::new(None), + lambda2_sseq: Mutex::new(None), secondary_products: DashMap::new(), } } @@ -97,6 +226,8 @@ where Arc::new(self.unit_lift.e3_page()) }; *self.unit_sseq.lock().unwrap() = Some(unit); + + *self.lambda2_sseq.lock().unwrap() = Some(Arc::new(self.build_lambda2_sseq())); } /// Sharding entry point: compute only the secondary resolution data for filtration `s`, @@ -164,10 +295,324 @@ where Self::e3_page_data(g.as_ref().expect("call extend_all() first"), b).clone() } + // Indexing the Cλ²-module the way `Bidegree{,Generator,Element}` index `ExtAlgebra`. These read + // the *ambient* generator counts (like `ExtAlgebra::dimension`), so the secondary `d_2`/$E_3$ + // structure stays a separate computation. `b` and `b + LAMBDA_BIDEGREE` must be computed; + // otherwise `number_of_gens_in_bidegree` panics, matching `ExtAlgebra::dimension`. + + /// The dimension of the weight-`weight` part of the secondary homotopy of `M` at `deg`: the + /// ambient number of generators of $\Ext(M, k)$ in that part's bidegree. + pub fn weight_dimension(&self, deg: SecondaryDegree, weight: Weight) -> usize { + self.alg + .resolution() + .number_of_gens_in_bidegree(deg.bidegree(weight)) + } + + /// The total dimension of the secondary homotopy of `M` at `deg` (weight 0 plus weight 1). + pub fn dimension(&self, deg: SecondaryDegree) -> usize { + self.weight_dimension(deg, Weight::Ext) + self.weight_dimension(deg, Weight::Lambda) + } + + /// The basis generators of the secondary homotopy of `M` at `deg`: the weight-0 generators + /// followed by the weight-1 (λ) generators. + pub fn basis(&self, deg: SecondaryDegree) -> Vec { + let base = deg.base(); + [Weight::Ext, Weight::Lambda] + .into_iter() + .flat_map(|w| { + (0..self.weight_dimension(deg, w)).map(move |i| SecondaryGenerator::new(base, w, i)) + }) + .collect() + } + + /// A class in the secondary homotopy of `M` at `deg` from its coordinates in the weight-0 and + /// weight-1 generator bases. + pub fn element( + &self, + deg: SecondaryDegree, + ext_coords: &[u32], + lambda_coords: &[u32], + ) -> SecondaryElement { + assert_eq!(self.weight_dimension(deg, Weight::Ext), ext_coords.len()); + assert_eq!( + self.weight_dimension(deg, Weight::Lambda), + lambda_coords.len() + ); + let p = self.prime(); + SecondaryElement::new( + deg.base(), + FpVector::from_slice(p, ext_coords), + FpVector::from_slice(p, lambda_coords), + ) + } + + /// A single generator of the secondary homotopy of `M` as a class. + pub fn generator(&self, g: SecondaryGenerator) -> SecondaryElement { + let deg = g.degree(); + assert!(self.weight_dimension(deg, g.weight()) > g.idx()); + g.into_element( + self.prime(), + self.weight_dimension(deg, Weight::Ext), + self.weight_dimension(deg, Weight::Lambda), + ) + } + + /// The dimension of the weight-`weight` part of the secondary homotopy of the unit `k` at + /// `deg` (the multiplicand / "scalar" side, i.e. $C\lambda^2$ itself). + pub fn unit_weight_dimension(&self, deg: SecondaryDegree, weight: Weight) -> usize { + self.alg + .unit() + .number_of_gens_in_bidegree(deg.bidegree(weight)) + } + + /// The total dimension of the secondary homotopy of the unit `k` at `deg`. + pub fn unit_dimension(&self, deg: SecondaryDegree) -> usize { + self.unit_weight_dimension(deg, Weight::Ext) + + self.unit_weight_dimension(deg, Weight::Lambda) + } + + /// The basis generators of the secondary homotopy of the unit `k` at `deg`. + pub fn unit_basis(&self, deg: SecondaryDegree) -> Vec { + let base = deg.base(); + [Weight::Ext, Weight::Lambda] + .into_iter() + .flat_map(|w| { + (0..self.unit_weight_dimension(deg, w)) + .map(move |i| SecondaryGenerator::new(base, w, i)) + }) + .collect() + } + + /// A class in the secondary homotopy of the unit `k` at `deg`. + pub fn unit_element( + &self, + deg: SecondaryDegree, + ext_coords: &[u32], + lambda_coords: &[u32], + ) -> SecondaryElement { + assert_eq!( + self.unit_weight_dimension(deg, Weight::Ext), + ext_coords.len() + ); + assert_eq!( + self.unit_weight_dimension(deg, Weight::Lambda), + lambda_coords.len() + ); + let p = self.prime(); + SecondaryElement::new( + deg.base(), + FpVector::from_slice(p, ext_coords), + FpVector::from_slice(p, lambda_coords), + ) + } + + /// A single generator of the secondary homotopy of the unit `k` as a class. + pub fn unit_generator(&self, g: SecondaryGenerator) -> SecondaryElement { + let deg = g.degree(); + assert!(self.unit_weight_dimension(deg, g.weight()) > g.idx()); + g.into_element( + self.prime(), + self.unit_weight_dimension(deg, Weight::Ext), + self.unit_weight_dimension(deg, Weight::Lambda), + ) + } + fn e3_page_data(sseq: &sseq::Sseq<2, sseq::Adams>, b: Bidegree) -> &Subquotient { let d = sseq.page_data(b); &d[std::cmp::min(3, d.len() - 1)] } + + /// Classify a generator at bidegree `(n, s)` as B, Z, or E in the $d_2$ decomposition. + /// + /// - **B** (boundary): in the image of $d_2$ from another bidegree. + /// - **Z** (cycle mod boundary): a $d_2$-cycle that is not a boundary; survives to $E_3$. + /// - **E** (supports $d_2$): $d_2(x) \neq 0$. + /// + /// At each bidegree, $\Ext = B \oplus Z \oplus E$ and $d_2$ restricts to an isomorphism + /// $E_{(n,s)} \to B_{(n-1,s+2)}$. + /// + /// Both incoming and outgoing $d_2$ are inspected via the two weights of the $S/\lambda^2$ + /// spectral sequence: the weight-0 copy at $(n, s, 0)$ carries the *outgoing* $d_2$ (so it + /// detects **E**), while the weight-1 copy at $(n, s, 1)$ receives the *incoming* $d_2$ (so it + /// detects **B**). Inspecting only one weight would miss either boundaries or supports. + pub fn classify(&self, g: BidegreeGenerator) -> BZE { + let [n, s] = g.degree().coords(); + let l2 = self.lambda2_sseq(); + + let td0 = MultiDegree::new([n, s, 0]); + if l2.defined(td0) && l2.classify(td0, 3, g.idx()) == BZE::E { + return BZE::E; + } + + let td1 = MultiDegree::new([n, s, 1]); + if l2.defined(td1) && l2.classify(td1, 3, g.idx()) == BZE::B { + return BZE::B; + } + + BZE::Z + } + + /// The conical basis of $\pi(S/\lambda^2)$ at bidegree `b` (Condition 3.2 of the paper). + /// + /// Each Ext generator at `b` is classified by [`classify`](Self::classify), then placed at the + /// weights it contributes to: + /// - **B**: weight 0 only. + /// - **Z**: both weights. + /// - **E**: weight 1 only. + pub fn pi_basis(&self, b: Bidegree) -> Vec { + let dim = self.alg.dimension(b); + let mut result = Vec::new(); + + for i in 0..dim { + let g = BidegreeGenerator::new(b, i); + let bze = self.classify(g); + + if bze != BZE::E { + result.push(PiGenerator::new(b, Weight::Ext, bze, i)); + } + if bze != BZE::B { + result.push(PiGenerator::new(b, Weight::Lambda, bze, i)); + } + } + + result + } + + /// Project a [`SecondaryElement`] to the $E_3$ subquotient at each weight, giving a pair of + /// [`PiElement`]s. + /// + /// The first element is the weight-0 projection (Ext part at `base`), the second is the + /// weight-1 projection ($\lambda$ part at `base + LAMBDA_BIDEGREE`). + pub fn to_pi(&self, elt: &SecondaryElement) -> (PiElement, PiElement) { + let base = elt.base(); + let ext_pi = self.project_to_pi(base, Weight::Ext, elt.ext()); + let lambda_pi = self.project_to_pi(base + LAMBDA_BIDEGREE, Weight::Lambda, elt.lambda()); + (ext_pi, lambda_pi) + } + + fn project_to_pi( + &self, + bidegree: Bidegree, + weight: Weight, + ambient_vec: fp::vector::FpSlice, + ) -> PiElement { + let [n, s] = bidegree.coords(); + let td = MultiDegree::new([n, s, weight.as_i32()]); + + if let Some(sq) = self.lambda2_page_data(td) { + let mut v = ambient_vec.to_owned(); + // `reduce` quotients out the boundary part and returns one coefficient per generator + // pivot column of `sq`, in ascending column order. + let coords = sq.reduce(v.as_slice_mut()); + + // The matching labels: the generator pivot columns are exactly the ambient indices that + // are neither a quotient (boundary) pivot nor in the complement of the cycle subspace. + // This is the same set `reduce` iterates, in the same order, so it lines up with + // `coords` entry-for-entry. + let complement: Vec = sq.complement_pivots().collect(); + let basis_indices: Vec = (0..sq.ambient_dimension()) + .filter(|&i| sq.zeros().pivots()[i] < 0 && !complement.contains(&i)) + .collect(); + + PiElement { + bidegree, + weight, + coords, + basis_indices, + } + } else { + PiElement { + bidegree, + weight, + coords: vec![], + basis_indices: vec![], + } + } + } + + /// Build the trigraded spectral sequence for $S/\lambda^2$. + /// + /// E2 has two copies of Ext at each bidegree $(n, s)$: one at Bockstein degree 0 and one at + /// Bockstein degree 1. The $d_2$ differential maps $(n, s, 0) \to (n-1, s+2, 1)$ using the + /// same hom\_k data as the Adams $d_2$. The E3 page (which equals $E_\infty$) gives + /// $\pi(S/\lambda^2)$. + fn build_lambda2_sseq(&self) -> sseq::Sseq<3, sseq::AdamsLambda2> { + let p = self.prime(); + let res = self.alg.resolution(); + let mut sseq = sseq::Sseq::new(p); + + for b in res.iter_stem() { + let dim = res.number_of_gens_in_bidegree(b); + let [n, s] = b.coords(); + sseq.set_dimension(MultiDegree::new([n, s, 0]), dim); + sseq.set_dimension(MultiDegree::new([n, s, 1]), dim); + } + + let mut source_vec = FpVector::new(p, 0); + let mut target_vec = FpVector::new(p, 0); + + for b in res.iter_stem() { + let target_bidegree = b + Bidegree::n_s(-1, 2); + if b.t() > 0 && res.has_computed_bidegree(target_bidegree) { + let m = self.res_lift.homotopy(b.s() + 2).homotopies.hom_k(b.t()); + if m.is_empty() || m[0].is_empty() { + continue; + } + + let [n, s] = b.coords(); + source_vec.set_scratch_vector_size(m.len()); + target_vec.set_scratch_vector_size(m[0].len()); + + for (i, row) in m.into_iter().enumerate() { + source_vec.set_to_zero(); + source_vec.set_entry(i, 1); + target_vec.copy_from_slice(&row); + + let source = MultiDegreeElement::new(MultiDegree::new([n, s, 0]), source_vec); + sseq.add_differential(2, &source, target_vec.as_slice()); + + source_vec = source.into_vec(); + } + } + } + + let invalid: Vec<_> = sseq.iter_degrees().filter(|&b| sseq.invalid(b)).collect(); + for b in invalid { + sseq.update_degree(b); + } + + sseq + } + + /// The trigraded spectral sequence for $S/\lambda^2$. + pub fn lambda2_sseq(&self) -> Arc> { + Arc::clone( + self.lambda2_sseq + .lock() + .unwrap() + .as_ref() + .expect("call extend_all() first"), + ) + } + + /// The $E_3$-page subquotient at a trigraded degree `(n, s, bock)` of $S/\lambda^2$. + /// Returns `None` if the degree is not defined in the spectral sequence. + pub fn lambda2_page_data(&self, b: MultiDegree<3>) -> Option { + let g = self.lambda2_sseq.lock().unwrap(); + let sseq = g.as_ref().expect("call extend_all() first"); + if !sseq.defined(b) { + return None; + } + let d = sseq.page_data(b); + Some(d[std::cmp::min(3, d.len() - 1)].clone()) + } + + /// The dimension of $E_3 = E_\infty$ of $S/\lambda^2$ at the trigraded degree `(n, s, bock)`. + /// Returns 0 if the degree is not defined. + pub fn lambda2_e3_dimension(&self, n: i32, s: i32, bock: i32) -> usize { + self.lambda2_page_data(MultiDegree::new([n, s, bock])) + .map_or(0, |sq| sq.dimension()) + } } impl SecondaryExtAlgebra @@ -257,8 +702,7 @@ where .zip(outputs) .map(|(g, out)| SecondaryProduct { source: BidegreeElement::new(b, g.to_owned()), - ext_part: out.slice(0, ext_dim).to_owned(), - lambda_part: out.slice(ext_dim, ext_dim + lambda_dim).to_owned(), + value: SecondaryElement::from_concatenated(b + shift, out.as_slice(), ext_dim), }) .collect() } @@ -269,7 +713,7 @@ mod tests { use sseq::coordinates::BidegreeGenerator; use super::*; - use crate::utils::construct_standard; + use crate::{chain_complex::ChainComplex, utils::construct_standard}; #[test] fn test_sphere_d2() { @@ -305,4 +749,174 @@ mod tests { let h4_survives = sec_e2.survives(&h4).expect("h4 should have a computed d2"); assert!(!h4_survives, "h4 should not survive d2"); } + + #[test] + fn test_secondary_indexing() { + let res = Arc::new(construct_standard::("S_2", None).unwrap()); + res.compute_through_stem(Bidegree::n_s(8, 8)); + let e2 = Arc::new(ExtAlgebra::new(Arc::clone(&res), res)); + let sec = SecondaryExtAlgebra::new(Arc::clone(&e2)); + + // (0,0): bottom class + λh0; (0,1): h0 + λh0²; (1,1): h1 + λ-part. + for (n, s) in [(0, 0), (0, 1), (1, 1)] { + let base = Bidegree::n_s(n, s); + let deg = SecondaryDegree::new(base); + + let ext_dim = e2.resolution().number_of_gens_in_bidegree(base); + let lambda_dim = e2 + .resolution() + .number_of_gens_in_bidegree(base + LAMBDA_BIDEGREE); + + assert_eq!(sec.weight_dimension(deg, Weight::Ext), ext_dim); + assert_eq!(sec.weight_dimension(deg, Weight::Lambda), lambda_dim); + assert_eq!(sec.dimension(deg), ext_dim + lambda_dim); + + let basis = sec.basis(deg); + assert_eq!(basis.len(), sec.dimension(deg)); + + for (i, g) in basis.iter().enumerate() { + // Weight-0 generators come first, then weight-1. + let (expected_weight, expected_idx) = if i < ext_dim { + (Weight::Ext, i) + } else { + (Weight::Lambda, i - ext_dim) + }; + assert_eq!(g.weight(), expected_weight); + assert_eq!(g.idx(), expected_idx); + assert_eq!(g.base(), base); + + // generator(g) round-trips to element(...) with a single 1 in the right part. + let elt = sec.generator(*g); + let mut ext_coords = vec![0u32; ext_dim]; + let mut lambda_coords = vec![0u32; lambda_dim]; + match g.weight() { + Weight::Ext => ext_coords[g.idx()] = 1, + Weight::Lambda => lambda_coords[g.idx()] = 1, + } + assert_eq!(elt, sec.element(deg, &ext_coords, &lambda_coords)); + assert_eq!( + elt.ext().iter_nonzero().count() + elt.lambda().iter_nonzero().count(), + 1 + ); + } + } + } + + #[test] + fn test_lambda2_sseq() { + let res = Arc::new(construct_standard::("S_2", None).unwrap()); + res.compute_through_stem(Bidegree::n_s(16, 6)); + let e2 = Arc::new(ExtAlgebra::new(Arc::clone(&res), res)); + let sec_e2 = SecondaryExtAlgebra::new(Arc::clone(&e2)); + sec_e2.extend_all(); + + let _l2 = sec_e2.lambda2_sseq(); + + // Structural check: at each bidegree (n, s), + // E3(n, s, 0) = ker(d2 from (n,s)) — d2-cycles at bock=0 + // E3(n, s, 1) = coker(d2 into (n,s)) — quotient by boundaries at bock=1 + // + // d2 maps E_{(n,s)} isomorphically to B_{(n-1, s+2)}, so: + // dim(Ext(n,s)) - dim(E3(n,s,0)) = dim(Ext(n-1,s+2)) - dim(E3(n-1,s+2,1)) + for b in e2.resolution().iter_stem() { + let ext_dim = e2.dimension(b); + let [n, s] = b.coords(); + + // dim(E) at (n, s) = dim(Ext) - dim(ker d2) = ext_dim - dim(E3(n,s,0)) + let e3_bock0 = sec_e2.lambda2_e3_dimension(n, s, 0); + let e_dim = ext_dim - e3_bock0; + + // dim(B) at (n-1, s+2) = dim(im d2) = dim(Ext(n-1,s+2)) - dim(E3(n-1,s+2,1)) + let target = b + Bidegree::n_s(-1, 2); + if e2.resolution().has_computed_bidegree(target) { + let target_ext_dim = e2.dimension(target); + let [tn, ts] = target.coords(); + let e3_target_bock1 = sec_e2.lambda2_e3_dimension(tn, ts, 1); + let b_dim = target_ext_dim - e3_target_bock1; + + assert_eq!( + e_dim, b_dim, + "dim(E at ({n},{s})) should equal dim(B at ({tn},{ts})): d2: E → B is an \ + isomorphism" + ); + } + + // dim(ker d2) + dim(E) = dim(Ext) always holds. + assert_eq!( + e3_bock0 + e_dim, + ext_dim, + "dim(ker d2) + dim(E) = dim(Ext) at ({n}, {s})" + ); + } + + // h4 at (15, 1) supports d2, so E3(15, 1, 0) should be 0. + assert_eq!( + sec_e2.lambda2_e3_dimension(15, 1, 0), + 0, + "h4 should not survive in E3 at bock=0" + ); + + // d2(h4) = h0*h3^2 lands at (14, 3, 1), quotienting out one generator. + let ext_dim_14_3 = e2.dimension(Bidegree::n_s(14, 3)); + assert_eq!( + sec_e2.lambda2_e3_dimension(14, 3, 1), + ext_dim_14_3 - 1, + "d2 image should quotient out one generator at (14, 3, 1)" + ); + + // h_0 at (0, 1) is a permanent cycle. + assert_eq!(sec_e2.lambda2_e3_dimension(0, 1, 0), 1); + assert_eq!(sec_e2.lambda2_e3_dimension(0, 1, 1), 1); + } + + #[test] + fn test_pi_types() { + let res = Arc::new(construct_standard::("S_2", None).unwrap()); + res.compute_through_stem(Bidegree::n_s(16, 6)); + let e2 = Arc::new(ExtAlgebra::new(Arc::clone(&res), res)); + let sec_e2 = SecondaryExtAlgebra::new(Arc::clone(&e2)); + sec_e2.extend_all(); + + // h0 at (0, 1) is a permanent Z-cycle: appears at both weights. + let h0_bze = sec_e2.classify(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); + assert_eq!(h0_bze, BZE::Z); + let pi_01 = sec_e2.pi_basis(Bidegree::n_s(0, 1)); + assert_eq!(pi_01.len(), 2); // weight 0 + weight 1 + assert_eq!(pi_01[0].weight(), Weight::Ext); + assert_eq!(pi_01[0].bze(), BZE::Z); + assert_eq!(pi_01[1].weight(), Weight::Lambda); + assert_eq!(pi_01[1].bze(), BZE::Z); + + // h4 at (15, 1) supports d2: classified as E, appears at weight 1 only. + let h4_bze = sec_e2.classify(BidegreeGenerator::new(Bidegree::n_s(15, 1), 0)); + assert_eq!(h4_bze, BZE::E); + let pi_15_1 = sec_e2.pi_basis(Bidegree::n_s(15, 1)); + assert_eq!(pi_15_1.len(), 1); + assert_eq!(pi_15_1[0].weight(), Weight::Lambda); + assert_eq!(pi_15_1[0].bze(), BZE::E); + + // h0*h3^2 at (14, 3) is a d2-boundary: classified as B, appears at weight 0 only. + let b14_3 = sec_e2.classify(BidegreeGenerator::new(Bidegree::n_s(14, 3), 0)); + assert_eq!(b14_3, BZE::B); + let pi_14_3 = sec_e2.pi_basis(Bidegree::n_s(14, 3)); + assert_eq!(pi_14_3.len(), 1); + assert_eq!(pi_14_3[0].weight(), Weight::Ext); + assert_eq!(pi_14_3[0].bze(), BZE::B); + + // to_pi: a zero secondary element projects to zero PiElements. + let zero_elt = sec_e2.element(SecondaryDegree::new(Bidegree::n_s(0, 1)), &[0], &[0]); + let (pi_ext, pi_lambda) = sec_e2.to_pi(&zero_elt); + assert!(pi_ext.is_zero()); + assert!(pi_lambda.is_zero()); + + // to_pi: h0 as a unit vector at weight 0 should project to a nonzero PiElement. + let h0_elt = sec_e2.element(SecondaryDegree::new(Bidegree::n_s(0, 1)), &[1], &[0]); + let (pi_ext, _pi_lambda) = sec_e2.to_pi(&h0_elt); + assert!(!pi_ext.is_zero()); + assert_eq!(pi_ext.weight(), Weight::Ext); + + // Display: PiGenerator and PiElement produce meaningful output. + assert!(!format!("{}", pi_01[0]).is_empty()); + assert!(!format!("{pi_ext}").is_empty()); + } } diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index 34aaa6dd7f..5c4776cb9c 100644 --- a/ext/src/secondary.rs +++ b/ext/src/secondary.rs @@ -1,4 +1,8 @@ -use std::{io, sync::Arc}; +use std::{ + fmt::{self, Display, Formatter}, + io, + sync::Arc, +}; use algebra::{ Algebra, @@ -30,6 +34,254 @@ use crate::{ pub static LAMBDA_BIDEGREE: Bidegree = Bidegree::n_s(0, 1); +/// The λ-adic weight of a homogeneous secondary class: [`Ext`](Weight::Ext) is the weight-0 +/// (bottom) part, [`Lambda`](Weight::Lambda) the weight-1 (λ) part. In $\Mod_{C\lambda^2}$ only +/// these two weights occur, since $\lambda^2 = 0$. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Weight { + Ext, + Lambda, +} + +impl Weight { + /// The weight as a synthetic coordinate: `0` for [`Ext`](Self::Ext), `1` for + /// [`Lambda`](Self::Lambda). (Lets these coordinates feed a future 3-graded synthetic + /// spectral sequence with the weight in the third coordinate.) + pub fn as_i32(self) -> i32 { + match self { + Self::Ext => 0, + Self::Lambda => 1, + } + } +} + +impl Display for Weight { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.as_i32()) + } +} + +/// A degree for the secondary ($\Mod_{C\lambda^2}$) layer: the base bidegree `b` of a homotopy +/// group of a module over $C\lambda^2$. The weight-0 (Ext) part lives at `b` and the weight-1 (λ) +/// part at `b + LAMBDA_BIDEGREE`. +/// +/// The secondary analog of [`Bidegree`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SecondaryDegree { + base: Bidegree, +} + +impl SecondaryDegree { + pub fn new(base: Bidegree) -> Self { + Self { base } + } + + /// The base bidegree; the Ext (weight 0) part lives here. + pub fn base(&self) -> Bidegree { + self.base + } + + /// The bidegree of the λ (weight 1) part, `base + LAMBDA_BIDEGREE`. + pub fn lambda_bidegree(&self) -> Bidegree { + self.base + LAMBDA_BIDEGREE + } + + /// The bidegree the part of the given `weight` lives in. + pub fn bidegree(&self, weight: Weight) -> Bidegree { + match weight { + Weight::Ext => self.base, + Weight::Lambda => self.lambda_bidegree(), + } + } +} + +impl From for SecondaryDegree { + fn from(base: Bidegree) -> Self { + Self::new(base) + } +} + +impl Display for SecondaryDegree { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(&self.base, f) + } +} + +/// A basis generator of a secondary ($\Mod_{C\lambda^2}$) homotopy group: a base bidegree, a +/// [`Weight`], and an index into the generator basis of the bidegree the generator lives in. +/// +/// The secondary analog of [`BidegreeGenerator`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SecondaryGenerator { + base: Bidegree, + weight: Weight, + idx: usize, +} + +impl SecondaryGenerator { + pub fn new(base: Bidegree, weight: Weight, idx: usize) -> Self { + Self { base, weight, idx } + } + + /// The base bidegree (the degree of the weight-0 part). + pub fn base(&self) -> Bidegree { + self.base + } + + /// The secondary degree this generator belongs to. + pub fn degree(&self) -> SecondaryDegree { + SecondaryDegree::new(self.base) + } + + pub fn weight(&self) -> Weight { + self.weight + } + + pub fn idx(&self) -> usize { + self.idx + } + + /// The bidegree this generator actually lives in: `base` for [`Weight::Ext`], `base + + /// LAMBDA_BIDEGREE` for [`Weight::Lambda`]. + pub fn bidegree(&self) -> Bidegree { + self.degree().bidegree(self.weight) + } + + /// Realize the generator as a [`SecondaryElement`]. `ext_ambient`/`lambda_ambient` are the + /// dimensions of the weight-0/weight-1 parts; the single nonzero entry is placed in the part + /// selected by [`weight`](Self::weight). Mirrors `MultiDegreeGenerator::into_element`. + pub fn into_element( + self, + p: ValidPrime, + ext_ambient: usize, + lambda_ambient: usize, + ) -> SecondaryElement { + let mut ext = FpVector::new(p, ext_ambient); + let mut lambda = FpVector::new(p, lambda_ambient); + match self.weight { + Weight::Ext => ext.set_entry(self.idx, 1), + Weight::Lambda => lambda.set_entry(self.idx, 1), + } + SecondaryElement::new(self.base, ext, lambda) + } +} + +impl Display for SecondaryGenerator { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let g = BidegreeGenerator::new(self.bidegree(), self.idx); + match self.weight { + Weight::Ext => write!(f, "x_{g}"), + Weight::Lambda => write!(f, "λx_{g}"), + } + } +} + +/// An element of a secondary ($\Mod_{C\lambda^2}$) homotopy group: the homotopy of a module over +/// $C\lambda^2$ at a base bidegree `b`. It bundles an Ext (weight 0) part in bidegree `b` and a +/// $\lambda$ (weight 1) part in bidegree `b + LAMBDA_BIDEGREE` (the latter understood modulo the +/// image of $d_2$). A generic class is *not* weight-homogeneous, so both parts are stored. +/// +/// The secondary analog of [`BidegreeElement`]: a pure data container that performs no reduction, +/// signs or $\mathbb{Z}/p^2$ arithmetic — those stay in the primitives that produce its components. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SecondaryElement { + base: Bidegree, + /// The Ext (weight 0) part, in bidegree `base`. + ext: FpVector, + /// The $\lambda$ (weight 1) part, in bidegree `base + LAMBDA_BIDEGREE`. + lambda: FpVector, +} + +impl SecondaryElement { + /// Build a secondary element from its Ext part (in bidegree `base`) and $\lambda$ part (in + /// bidegree `base + LAMBDA_BIDEGREE`). + pub fn new(base: Bidegree, ext: FpVector, lambda: FpVector) -> Self { + Self { base, ext, lambda } + } + + /// Split a concatenated `[ext | lambda]` vector, where `ext_dim` is the length of the Ext + /// chunk. This mirrors the output layout of + /// [`SecondaryResolutionHomomorphism::hom_k`](crate::secondary::SecondaryResolutionHomomorphism::hom_k): + /// the first `ext_dim` entries are the Ext part and the remainder the $\lambda$ part. + pub fn from_concatenated(base: Bidegree, concatenated: FpSlice, ext_dim: usize) -> Self { + let total = concatenated.len(); + Self { + base, + ext: concatenated.restrict(0, ext_dim).to_owned(), + lambda: concatenated.restrict(ext_dim, total).to_owned(), + } + } + + /// The base bidegree `b`; the Ext part lives here and the $\lambda$ part in `b + LAMBDA_BIDEGREE`. + pub fn base(&self) -> Bidegree { + self.base + } + + /// The secondary degree of this element. + pub fn degree(&self) -> SecondaryDegree { + SecondaryDegree::new(self.base) + } + + /// The Ext (weight 0) part, in bidegree [`base`](Self::base). + pub fn ext(&self) -> FpSlice<'_> { + self.ext.as_slice() + } + + /// The $\lambda$ (weight 1) part, in bidegree `base + LAMBDA_BIDEGREE`. + pub fn lambda(&self) -> FpSlice<'_> { + self.lambda.as_slice() + } + + /// The Ext part as a [`BidegreeElement`] at [`base`](Self::base). + pub fn ext_element(&self) -> BidegreeElement { + BidegreeElement::new(self.base, self.ext.clone()) + } + + /// The $\lambda$ part as a [`BidegreeElement`] at `base + LAMBDA_BIDEGREE`. + pub fn lambda_element(&self) -> BidegreeElement { + BidegreeElement::new(self.base + LAMBDA_BIDEGREE, self.lambda.clone()) + } + + /// Whether both the Ext and $\lambda$ parts vanish. + pub fn is_zero(&self) -> bool { + self.ext.is_zero() && self.lambda.is_zero() + } + + /// The element as a linear combination of generators, e.g. `[x_(n, s, 0)] + λx_(n, s+1, 0)`. + /// The Ext part is bracketed, and the $\lambda$ part is parenthesized when it has more than one + /// term. Returns `0` when the element vanishes. + pub fn to_basis_string(&self) -> String { + let has_ext = !self.ext.is_zero(); + let lambda_entries = self.lambda.iter_nonzero().count(); + + let mut out = String::new(); + if has_ext { + out.push_str(&format!("[{}]", self.ext_element().to_basis_string())); + } + if lambda_entries > 0 { + if has_ext { + out.push_str(" + "); + } + let s = self.lambda_element().to_basis_string(); + if lambda_entries == 1 { + out.push_str(&format!("λ{s}")); + } else { + out.push_str(&format!("λ({s})")); + } + } + if out.is_empty() { + out.push('0'); + } + out + } +} + +impl Display for SecondaryElement { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str(&self.to_basis_string()) + } +} + pub type CompositeData = Vec<( u32, Arc>>, @@ -647,7 +899,9 @@ pub trait SecondaryLift: Sync + Sized { let s_range = self.homotopies().range(); let min = Bidegree::s_t(s_range.start + 1, min_t); let max = self.max().restrict(s_range.end); - sseq::coordinates::iter_s_t(&|b| self.compute_homotopy_step(b), min, max); + if min.s() < max.s() { + sseq::coordinates::iter_s_t(&|b| self.compute_homotopy_step(b), min, max); + } } #[tracing::instrument(skip(self))]