From 264e1ce9f00d6f6291b0b46d5ff8977b43829072 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 05:39:38 +0000 Subject: [PATCH 01/11] =?UTF-8?q?Add=20coordinate=20types=20to=20index=20S?= =?UTF-8?q?econdaryExtAlgebra=20as=20a=20C=CE=BB=C2=B2-module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExtAlgebra is a first-class Cλ-module, indexed by the coordinate triple Bidegree / BidegreeGenerator / BidegreeElement. SecondaryExtAlgebra is the analogous Cλ²-module but lacked the matching coordinate types; this adds them so you can index into it the same way. New secondary coordinate triple in ext/src/secondary.rs: - Weight: the λ-adic weight (Ext = 0, Lambda = 1; only two weights since λ² = 0), with an as_i32 synthetic coordinate. - SecondaryDegree: a base bidegree b — weight-0 part at b, weight-1 (λ) part at b + LAMBDA_BIDEGREE. Analog of Bidegree. - SecondaryGenerator: base + Weight + idx; into_element places the unit entry in the ext or λ part. Analog of BidegreeGenerator. - SecondaryElement: base + ext/λ FpVectors (promoted from the earlier SyntheticElement). A generic class is not weight-homogeneous, so both parts are stored. Analog of BidegreeElement. Indexing methods on SecondaryExtAlgebra mirror ExtAlgebra (dimension/basis/element/generator, weight_dimension, and unit_* variants). They read ambient generator counts like ExtAlgebra::dimension, so the secondary d2/E3 structure stays a separate computation; undefined bidegrees panic rather than yielding a silent 0. SecondaryProduct.value and both examples use SecondaryElement. The Weight enum is designed to feed a future 3-graded synthetic spectral sequence; the B/Z/E differential filtration is left for that later layer. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0125sG5YPRQVfTs5eZ7hN7hA --- ext/examples/secondary_massey.rs | 43 +---- ext/examples/secondary_product.rs | 5 +- ext/src/ext_algebra/mod.rs | 1 + ext/src/ext_algebra/secondary.rs | 188 +++++++++++++++++++++- ext/src/secondary.rs | 254 +++++++++++++++++++++++++++++- 5 files changed, 440 insertions(+), 51 deletions(-) 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..9af5ee8bba 100644 --- a/ext/examples/secondary_product.rs +++ b/ext/examples/secondary_product.rs @@ -113,10 +113,9 @@ fn main() -> anyhow::Result<()> { // Now the genuinely secondary products with surviving classes. for prod in sec_e2.secondary_multiply_into(&x, b) { println!( - "{disp} [{basis}] = {ext} + λ {lambda}", + "{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..96f910cd74 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -29,6 +29,7 @@ use fp::{matrix::Matrix, prime::ValidPrime, vector::FpVector}; use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator}; pub use self::secondary::{SecondaryExtAlgebra, SecondaryProduct}; +pub use crate::secondary::{SecondaryDegree, SecondaryElement, SecondaryGenerator, Weight}; use crate::{ chain_complex::{AugmentedChainComplex, FreeChainComplex}, resolution_homomorphism::ResolutionHomomorphism, diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index 983a7c60ac..f1871abc60 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -24,7 +24,8 @@ 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 +34,9 @@ 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, } /// The secondary layer over an [`ExtAlgebra`]: the $d_2$ differential and the $\Mod_{C\lambda^2}$ @@ -164,6 +163,128 @@ 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)] @@ -257,8 +378,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() } @@ -305,4 +425,56 @@ 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 + ); + } + } + } } diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index 34aaa6dd7f..91b84ec1ef 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>>, From 915ef3996c8c6837bd3458666b5b456cded97362 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:05:29 +0000 Subject: [PATCH 02/11] =?UTF-8?q?Add=20trigraded=20S/=CE=BB=C2=B2=20spectr?= =?UTF-8?q?al=20sequence=20and=20lambda2=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AdamsLambda2 profile (SseqProfile<3>) for the trigraded Adams spectral sequence of S/λ². Extend SecondaryExtAlgebra with a lambda2_sseq field that builds the E2 page (two copies of Ext at Bockstein degrees 0 and 1) and installs d2 differentials from (n, s, 0) → (n-1, s+2, 1) using the hom_k data. The E3 = E∞ page gives π(S/λ²). New API: lambda2_sseq(), lambda2_page_data(), lambda2_e3_dimension(). New example: lambda2.rs with Table I (B/Z/E decomposition), Table II (Ext products), Table III (π(S/λ²) basis), and Table IV (secondary products with commutators). Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/crates/sseq/src/sseq.rs | 22 +++ ext/examples/lambda2.rs | 244 +++++++++++++++++++++++++++++++ ext/src/ext_algebra/secondary.rs | 165 ++++++++++++++++++++- 3 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 ext/examples/lambda2.rs diff --git a/ext/crates/sseq/src/sseq.rs b/ext/crates/sseq/src/sseq.rs index a1d70e8e14..1c157865d1 100644 --- a/ext/crates/sseq/src/sseq.rs +++ b/ext/crates/sseq/src/sseq.rs @@ -39,6 +39,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. diff --git a/ext/examples/lambda2.rs b/ext/examples/lambda2.rs new file mode 100644 index 0000000000..0a631c70a0 --- /dev/null +++ b/ext/examples/lambda2.rs @@ -0,0 +1,244 @@ +//! 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 algebra::module::Module; +use ext::{ + chain_complex::{ChainComplex, FreeChainComplex}, + ext_algebra::{ExtAlgebra, secondary::SecondaryExtAlgebra}, + utils::query_module, +}; +use fp::matrix::Subquotient; +use sseq::coordinates::{Bidegree, BidegreeGenerator, MultiDegree}; + +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))?); + + if !e2.is_unit() { + let max = Bidegree::n_s( + resolution.module(0).max_computed_degree(), + resolution.next_homological_degree() - 1, + ); + e2.unit().compute_through_stem(max); + } + + 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 page = sec_e2.page_data(b); + let dim = e2.dimension(b); + if dim == 0 { + continue; + } + + for i in 0..dim { + let g = BidegreeGenerator::new(b, i); + let class_type = classify_generator(&page, i); + + match class_type { + 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)"); + } + } + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum BZE { + B, + Z, + E, +} + +fn classify_generator(page: &Subquotient, idx: usize) -> BZE { + // B: in the quotient (boundary) + // Z: a gen (cycle, not boundary) + // E: in the complement (supports d2) + + // Check if this generator index is a pivot of the quotient (boundary) + if page.zeros().pivots()[idx] >= 0 { + return BZE::B; + } + // Check if this generator index is in the complement (not a cycle) + if page.complement_pivots().any(|p| p == idx) { + return BZE::E; + } + // Otherwise it's a cycle that's not a boundary + BZE::Z +} + +/// 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). +/// Elements of the conical basis Xπ come in four types per the paper's Condition 3.2: +/// - x⁰_π for x ∈ B: bockstein = 0 +/// - x⁰_π for x ∈ Z: bockstein = 0 +/// - x¹_π for x ∈ Z: bockstein = 1 +/// - x¹_π for x ∈ E: bockstein = 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() { + let [n, s] = b.coords(); + + for bock in [0, 1] { + let dim = sec_e2.lambda2_e3_dimension(n, s, bock); + if dim > 0 { + if let Some(pd) = sec_e2.lambda2_page_data(MultiDegree::new([n, s, bock])) { + for (idx, v) in pd.gens().enumerate() { + let coords: Vec = v.iter().collect(); + println!("({n}, {s}, {bock}) gen {idx} {coords:?}"); + } + } + } + } + } +} + +/// Table IV: Products in π(S/λ²) with commutators. +/// +/// For each pair (α, β) of conical basis elements with both in bockstein 0, +/// computes α · β using the secondary product and the commutator α·β - β·α. +fn print_table_iv(e2: &ExtAlgebra, sec_e2: &SecondaryExtAlgebra) +where + CC: FreeChainComplex + ext::chain_complex::AugmentedChainComplex, + CC::Algebra: algebra::pair_algebra::PairAlgebra, +{ + // Collect all surviving generators (Z classes) at bock=0 in the bigraded page. + let survivors: Vec = e2 + .resolution() + .iter_nonzero_stem() + .flat_map(|b| { + let page = sec_e2.page_data(b); + let dim = e2.dimension(b); + (0..dim) + .filter(move |&i| classify_generator(&page, i) != BZE::E) + .map(move |i| BidegreeGenerator::new(b, i)) + }) + .collect(); + + for (idx_x, &x_gen) in survivors.iter().enumerate() { + let x = e2.generator(x_gen); + // Only compute secondary products for d2-cycles (Z generators, not B). + let x_page = sec_e2.page_data(x_gen.degree()); + if classify_generator(&x_page, x_gen.idx()) != BZE::Z { + continue; + } + for &y_gen in &survivors[idx_x..] { + // The secondary product x · y: iterate over the secondary multiply output. + for prod in sec_e2.secondary_multiply_into(&x, y_gen.degree()) { + let ext: Vec = prod.ext_part.iter().collect(); + let lambda: Vec = prod.lambda_part.iter().collect(); + if ext.iter().any(|&c| c != 0) || lambda.iter().any(|&c| c != 0) { + println!( + "x_{x_gen} · [{src}] = {ext:?} + λ {lambda:?}", + src = prod.source.to_basis_string(), + ); + } + } + } + } +} diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index f1871abc60..e65a2f0ea7 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -17,7 +17,7 @@ use std::sync::{Arc, Mutex}; use algebra::pair_algebra::PairAlgebra; use dashmap::DashMap; use fp::{matrix::Subquotient, prime::Prime, vector::FpVector}; -use sseq::coordinates::{Bidegree, BidegreeElement}; +use sseq::coordinates::{Bidegree, BidegreeElement, MultiDegree, MultiDegreeElement}; use super::ExtAlgebra; use crate::{ @@ -53,6 +53,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>>, } @@ -76,6 +79,7 @@ where unit_lift, res_sseq: Mutex::new(None), unit_sseq: Mutex::new(None), + lambda2_sseq: Mutex::new(None), secondary_products: DashMap::new(), } } @@ -96,6 +100,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`, @@ -289,6 +295,94 @@ where let d = sseq.page_data(b); &d[std::cmp::min(3, d.len() - 1)] } + + /// 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 @@ -389,7 +483,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() { @@ -477,4 +571,71 @@ mod tests { } } } + + #[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); + } } From 11942f3f1a96f88540d87bad4fa95035acbd857f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:26:45 +0000 Subject: [PATCH 03/11] Add BZE classify API, fix secondary lift edge case, improve lambda2 example Move the B/Z/E classification into SecondaryExtAlgebra as a public API (BZE enum + classify method) rather than keeping it inline in the example. Fix a panic in SecondaryLift::compute_homotopies when the homotopy range is empty (min.s >= max.s), which occurred for multipliers at high filtration near the resolution boundary. Rewrite the lambda2 example's Table IV to use proper range guards matching secondary_product. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/examples/lambda2.rs | 125 ++++++++++++++----------------- ext/src/ext_algebra/mod.rs | 2 +- ext/src/ext_algebra/secondary.rs | 39 ++++++++++ ext/src/secondary.rs | 4 +- 4 files changed, 100 insertions(+), 70 deletions(-) diff --git a/ext/examples/lambda2.rs b/ext/examples/lambda2.rs index 0a631c70a0..1e56b36e1a 100644 --- a/ext/examples/lambda2.rs +++ b/ext/examples/lambda2.rs @@ -20,10 +20,13 @@ 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, + }, + secondary::LAMBDA_BIDEGREE, utils::query_module, }; -use fp::matrix::Subquotient; use sseq::coordinates::{Bidegree, BidegreeGenerator, MultiDegree}; fn main() -> anyhow::Result<()> { @@ -85,23 +88,12 @@ where CC::Algebra: algebra::pair_algebra::PairAlgebra, { for b in e2.resolution().iter_nonzero_stem() { - let page = sec_e2.page_data(b); let dim = e2.dimension(b); - if dim == 0 { - continue; - } - for i in 0..dim { let g = BidegreeGenerator::new(b, i); - let class_type = classify_generator(&page, i); - - match class_type { - BZE::Z => { - println!("Z x_{g}"); - } - BZE::B => { - println!("B x_{g}"); - } + 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) { @@ -116,30 +108,6 @@ where } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum BZE { - B, - Z, - E, -} - -fn classify_generator(page: &Subquotient, idx: usize) -> BZE { - // B: in the quotient (boundary) - // Z: a gen (cycle, not boundary) - // E: in the complement (supports d2) - - // Check if this generator index is a pivot of the quotient (boundary) - if page.zeros().pivots()[idx] >= 0 { - return BZE::B; - } - // Check if this generator index is in the complement (not a cycle) - if page.complement_pivots().any(|p| p == idx) { - return BZE::E; - } - // Otherwise it's a cycle that's not a boundary - BZE::Z -} - /// 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. @@ -170,12 +138,12 @@ where /// Table III: Conical basis for π(S/λ²). /// -/// Lists the E3 = E∞ generators of S/λ² at each tridegree (n, s, bock). -/// Elements of the conical basis Xπ come in four types per the paper's Condition 3.2: -/// - x⁰_π for x ∈ B: bockstein = 0 -/// - x⁰_π for x ∈ Z: bockstein = 0 -/// - x¹_π for x ∈ Z: bockstein = 1 -/// - x¹_π for x ∈ E: bockstein = 1 +/// 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, @@ -183,15 +151,23 @@ where { for b in e2.resolution().iter_stem() { let [n, s] = b.coords(); + let dim = e2.dimension(b); + if dim == 0 { + continue; + } + // bock=0: surviving classes are B ⨿ Z (d2-cycles). + // bock=1: surviving classes are Z ⨿ E (cokernel of d2). for bock in [0, 1] { - let dim = sec_e2.lambda2_e3_dimension(n, s, bock); - if dim > 0 { - if let Some(pd) = sec_e2.lambda2_page_data(MultiDegree::new([n, s, bock])) { - for (idx, v) in pd.gens().enumerate() { - let coords: Vec = v.iter().collect(); - println!("({n}, {s}, {bock}) gen {idx} {coords:?}"); - } + let e3_dim = sec_e2.lambda2_e3_dimension(n, s, bock); + if e3_dim == 0 { + continue; + } + + if let Some(pd) = sec_e2.lambda2_page_data(MultiDegree::new([n, s, bock])) { + for (idx, v) in pd.gens().enumerate() { + let coords: Vec = v.iter().collect(); + println!("({n}, {s}, {bock}) gen {idx} {coords:?}"); } } } @@ -200,36 +176,49 @@ where /// Table IV: Products in π(S/λ²) with commutators. /// -/// For each pair (α, β) of conical basis elements with both in bockstein 0, -/// computes α · β using the secondary product and the commutator α·β - β·α. +/// 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. The secondary product lives +/// in Mod_{Cλ²}: it has an Ext part and a λ part. +/// +/// The commutator x·y − y·x = 2·x·y when both stems are odd (Proposition 6.4). fn print_table_iv(e2: &ExtAlgebra, sec_e2: &SecondaryExtAlgebra) where CC: FreeChainComplex + ext::chain_complex::AugmentedChainComplex, CC::Algebra: algebra::pair_algebra::PairAlgebra, { - // Collect all surviving generators (Z classes) at bock=0 in the bigraded page. - let survivors: Vec = e2 + // Secondary products need s ≥ 1 for the multiplier (the secondary lift's shift is s+1, + // and secondary homotopies start at s=2). + let z_gens: Vec = e2 .resolution() .iter_nonzero_stem() + .filter(|b| b.s() >= 1) .flat_map(|b| { - let page = sec_e2.page_data(b); let dim = e2.dimension(b); (0..dim) - .filter(move |&i| classify_generator(&page, i) != BZE::E) + .filter(move |&i| sec_e2.classify(BidegreeGenerator::new(b, i)) == BZE::Z) .map(move |i| BidegreeGenerator::new(b, i)) }) .collect(); - for (idx_x, &x_gen) in survivors.iter().enumerate() { + for &x_gen in &z_gens { let x = e2.generator(x_gen); - // Only compute secondary products for d2-cycles (Z generators, not B). - let x_page = sec_e2.page_data(x_gen.degree()); - if classify_generator(&x_page, x_gen.idx()) != BZE::Z { - continue; - } - for &y_gen in &survivors[idx_x..] { - // The secondary product x · y: iterate over the secondary multiply output. - for prod in sec_e2.secondary_multiply_into(&x, y_gen.degree()) { + 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; + } + + for prod in sec_e2.secondary_multiply_into(&x, b) { let ext: Vec = prod.ext_part.iter().collect(); let lambda: Vec = prod.lambda_part.iter().collect(); if ext.iter().any(|&c| c != 0) || lambda.iter().any(|&c| c != 0) { diff --git a/ext/src/ext_algebra/mod.rs b/ext/src/ext_algebra/mod.rs index 96f910cd74..e014697c8b 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -28,7 +28,7 @@ use dashmap::DashMap; use fp::{matrix::Matrix, prime::ValidPrime, vector::FpVector}; use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator}; -pub use self::secondary::{SecondaryExtAlgebra, SecondaryProduct}; +pub use self::secondary::{BZE, SecondaryExtAlgebra, SecondaryProduct}; pub use crate::secondary::{SecondaryDegree, SecondaryElement, SecondaryGenerator, Weight}; use crate::{ chain_complex::{AugmentedChainComplex, FreeChainComplex}, diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index e65a2f0ea7..46847c4a69 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -19,6 +19,8 @@ use dashmap::DashMap; use fp::{matrix::Subquotient, prime::Prime, vector::FpVector}; use sseq::coordinates::{Bidegree, BidegreeElement, MultiDegree, MultiDegreeElement}; +use sseq::coordinates::BidegreeGenerator; + use super::ExtAlgebra; use crate::{ chain_complex::FreeChainComplex, @@ -29,6 +31,30 @@ use crate::{ }, }; +/// The classification of an Ext generator in the $d_2$-adapted B/Z/E decomposition. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BZE { + /// Boundary: in the image of $d_2$ from another bidegree. + B, + /// Cycle mod boundary: survives to $E_3$. + Z, + /// Supports $d_2$: $d_2(x) \neq 0$. + E, +} + +impl BZE { + /// Classify a standard-basis generator `idx` from its $E_3$-page subquotient. + 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 + } +} + /// A single secondary product `x · y` in $\Mod_{C\lambda^2}$, where `y` is an $E_3$-surviving /// class. See [`SecondaryExtAlgebra::secondary_multiply_into`]. pub struct SecondaryProduct { @@ -296,6 +322,19 @@ where &d[std::cmp::min(3, d.len() - 1)] } + /// Classify a generator at bidegree `b` 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)}$. + pub fn classify(&self, g: BidegreeGenerator) -> BZE { + let page = self.page_data(g.degree()); + BZE::from_page_data(&page, g.idx()) + } + /// 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 diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index 91b84ec1ef..5c4776cb9c 100644 --- a/ext/src/secondary.rs +++ b/ext/src/secondary.rs @@ -899,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))] From cf68a58e113e5024c2e052e0e26d1f7ba0de9906 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:38:50 +0000 Subject: [PATCH 04/11] Use BZE classification in secondary examples, improve lambda2 tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update secondary.rs and secondary_product.rs to use the BZE enum for classified output, labelling each result as B/Z/E. This replaces the old unclassified d2 output in secondary.rs and the complement_pivots iteration in secondary_product.rs with the uniform BZE::from_page_data API. Also improve lambda2 Table III to annotate conical basis entries with their B/Z/E type (bock=0: B and Z survive; bock=1: Z and E survive), and Table IV to include commutator information per Proposition 6.4 ([x,y] = 2(x·y) when both stems are odd, 0 otherwise). Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/examples/lambda2.rs | 49 +++++++++++++++++++------------ ext/examples/secondary.rs | 36 ++++++++++------------- ext/examples/secondary_product.rs | 28 +++++++++++++----- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/ext/examples/lambda2.rs b/ext/examples/lambda2.rs index 1e56b36e1a..3192468df7 100644 --- a/ext/examples/lambda2.rs +++ b/ext/examples/lambda2.rs @@ -27,7 +27,7 @@ use ext::{ secondary::LAMBDA_BIDEGREE, utils::query_module, }; -use sseq::coordinates::{Bidegree, BidegreeGenerator, MultiDegree}; +use sseq::coordinates::{Bidegree, BidegreeGenerator}; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; @@ -156,19 +156,24 @@ where continue; } - // bock=0: surviving classes are B ⨿ Z (d2-cycles). - // bock=1: surviving classes are Z ⨿ E (cokernel of d2). - for bock in [0, 1] { - let e3_dim = sec_e2.lambda2_e3_dimension(n, s, bock); - if e3_dim == 0 { - continue; - } + for i in 0..dim { + let g = BidegreeGenerator::new(b, i); + let bze = sec_e2.classify(g); - if let Some(pd) = sec_e2.lambda2_page_data(MultiDegree::new([n, s, bock])) { - for (idx, v) in pd.gens().enumerate() { - let coords: Vec = v.iter().collect(); - println!("({n}, {s}, {bock}) gen {idx} {coords:?}"); + // bock=0: B and Z survive (d2-cycles). + match bze { + BZE::B | BZE::Z => { + let label = if bze == BZE::B { "B" } else { "Z" }; + println!("{label} x_{g}^0 ({n}, {s}, 0)"); } + BZE::E => {} + } + + // bock=1: Z and E survive (cokernel of d2). + match bze { + BZE::Z => println!("Z x_{g}^1 ({n}, {s}, 1)"), + BZE::E => println!("E x_{g}^1 ({n}, {s}, 1)"), + BZE::B => {} } } } @@ -180,7 +185,8 @@ where /// the unit at each bidegree, computes the secondary product x · y. The secondary product lives /// in Mod_{Cλ²}: it has an Ext part and a λ part. /// -/// The commutator x·y − y·x = 2·x·y when both stems are odd (Proposition 6.4). +/// 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, @@ -218,15 +224,22 @@ where continue; } + // Proposition 6.4: [x, y] = x·y − y·x = 2·(x·y) when both stems are odd, + // and [x, y] = 0 when at least one stem is even. + let both_odd = shift.n() % 2 != 0 && b.n() % 2 != 0; + for prod in sec_e2.secondary_multiply_into(&x, b) { let ext: Vec = prod.ext_part.iter().collect(); let lambda: Vec = prod.lambda_part.iter().collect(); - if ext.iter().any(|&c| c != 0) || lambda.iter().any(|&c| c != 0) { - println!( - "x_{x_gen} · [{src}] = {ext:?} + λ {lambda:?}", - src = prod.source.to_basis_string(), - ); + if ext.iter().all(|&c| c == 0) && lambda.iter().all(|&c| c == 0) { + continue; } + + let comm = if both_odd { "[x,y] = 2(x·y)" } else { "[x,y] = 0" }; + println!( + "x_{x_gen} · [{src}] = {ext:?} + λ {lambda:?} {comm}", + src = prod.source.to_basis_string(), + ); } } } diff --git a/ext/examples/secondary.rs b/ext/examples/secondary.rs index 44c5cc3b0d..ee9f3eb91a 100644 --- a/ext/examples/secondary.rs +++ b/ext/examples/secondary.rs @@ -47,13 +47,12 @@ use std::sync::Arc; -use algebra::module::Module; use ext::{ - chain_complex::{ChainComplex, FreeChainComplex}, - ext_algebra::{ExtAlgebra, secondary::SecondaryExtAlgebra}, + chain_complex::FreeChainComplex, + ext_algebra::{BZE, ExtAlgebra, secondary::SecondaryExtAlgebra}, utils::query_module, }; -use sseq::coordinates::Bidegree; +use sseq::coordinates::BidegreeGenerator; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; @@ -72,24 +71,21 @@ fn main() -> anyhow::Result<()> { sec_e2.extend_all(); 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. 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) { + 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 => { + 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_product.rs b/ext/examples/secondary_product.rs index 9af5ee8bba..871c168f84 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,22 +98,36 @@ 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). for prod in sec_e2.secondary_multiply_into(&x, b) { + let bze = prod + .source + .vec() + .iter_nonzero() + .next() + .map(|(i, _)| BZE::from_page_data(&page, i)) + .unwrap_or(BZE::Z); + let label = match bze { + BZE::B => "B", + BZE::Z => "Z", + BZE::E => "E", + }; println!( - "{disp} [{basis}] = {value}", + "{label} {disp} [{basis}] = {value}", basis = prod.source.to_basis_string(), value = prod.value, ); From 7ff7b4f1250271449c9b9d0e9caaf6529a77c8ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 01:13:31 +0000 Subject: [PATCH 05/11] Filter secondary output to nonzero d2 targets, update benchmarks Restore the original filtering in the secondary example: only print generators whose d2 target bidegree has nonzero dimension, keeping the output focused on where d2 can be nontrivial. Each such generator is now labelled B/Z/E. Update the four secondary benchmark files to match the new output format. Fix rustfmt issues in lambda2.rs and secondary.rs. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/examples/benchmarks/secondary-C2 | 22 ++++++------- ext/examples/benchmarks/secondary-C2-nassau | 22 ++++++------- ext/examples/benchmarks/secondary-S_2 | 36 ++++++++++----------- ext/examples/benchmarks/secondary-tmf | 24 +++++++------- ext/examples/lambda2.rs | 21 ++++++++---- ext/examples/secondary.rs | 19 ++++++++--- ext/src/ext_algebra/secondary.rs | 8 ++--- 7 files changed, 83 insertions(+), 69 deletions(-) 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/lambda2.rs b/ext/examples/lambda2.rs index 3192468df7..fa27866010 100644 --- a/ext/examples/lambda2.rs +++ b/ext/examples/lambda2.rs @@ -20,10 +20,7 @@ use std::sync::Arc; use algebra::module::Module; use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - ext_algebra::{ - BZE, ExtAlgebra, - secondary::SecondaryExtAlgebra, - }, + ext_algebra::{BZE, ExtAlgebra, secondary::SecondaryExtAlgebra}, secondary::LAMBDA_BIDEGREE, utils::query_module, }; @@ -211,10 +208,16 @@ where let shift = x.degree(); for b in e2.unit().iter_nonzero_stem() { - if !e2.resolution().has_computed_bidegree(b + shift + LAMBDA_BIDEGREE) { + if !e2 + .resolution() + .has_computed_bidegree(b + shift + LAMBDA_BIDEGREE) + { continue; } - if !e2.resolution().has_computed_bidegree(b + shift - Bidegree::s_t(1, 0)) { + if !e2 + .resolution() + .has_computed_bidegree(b + shift - Bidegree::s_t(1, 0)) + { continue; } @@ -235,7 +238,11 @@ where continue; } - let comm = if both_odd { "[x,y] = 2(x·y)" } else { "[x,y] = 0" }; + let comm = if both_odd { + "[x,y] = 2(x·y)" + } else { + "[x,y] = 0" + }; println!( "x_{x_gen} · [{src}] = {ext:?} + λ {lambda:?} {comm}", src = prod.source.to_basis_string(), diff --git a/ext/examples/secondary.rs b/ext/examples/secondary.rs index ee9f3eb91a..a143f166a9 100644 --- a/ext/examples/secondary.rs +++ b/ext/examples/secondary.rs @@ -47,12 +47,13 @@ use std::sync::Arc; +use algebra::module::Module; use ext::{ - chain_complex::FreeChainComplex, + chain_complex::{ChainComplex, FreeChainComplex}, ext_algebra::{BZE, ExtAlgebra, secondary::SecondaryExtAlgebra}, utils::query_module, }; -use sseq::coordinates::BidegreeGenerator; +use sseq::coordinates::{Bidegree, BidegreeGenerator}; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; @@ -71,10 +72,20 @@ fn main() -> anyhow::Result<()> { sec_e2.extend_all(); let e2 = sec_e2.ext_algebra(); + let d2_shift = Bidegree::n_s(-1, 2); + // Iterate through the target of the d2. We omit elements whose d2 target bidegree is zero. for b in e2.resolution().iter_nonzero_stem() { - for i in 0..e2.dimension(b) { - let g = BidegreeGenerator::new(b, i); + if b.s() < 3 { + continue; + } + + if b.t() - 1 > e2.resolution().module(b.s() - 2).max_computed_degree() { + continue; + } + + 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}"), diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index 46847c4a69..7683706343 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -373,8 +373,7 @@ where source_vec.set_entry(i, 1); target_vec.copy_from_slice(&row); - let source = - MultiDegreeElement::new(MultiDegree::new([n, s, 0]), source_vec); + 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(); @@ -382,10 +381,7 @@ where } } - let invalid: Vec<_> = sseq - .iter_degrees() - .filter(|&b| sseq.invalid(b)) - .collect(); + let invalid: Vec<_> = sseq.iter_degrees().filter(|&b| sseq.invalid(b)).collect(); for b in invalid { sseq.update_degree(b); } From 86912e849a1ebe2362b5c4821b385ddf2736ab5b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 18:47:21 +0000 Subject: [PATCH 06/11] Adapt to SecondaryElement API from PR #250 Use prod.value (SecondaryElement) instead of the removed ext_part/lambda_part fields in lambda2.rs Table IV. Fix nightly rustfmt import grouping. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/examples/lambda2.rs | 7 +++---- ext/src/ext_algebra/secondary.rs | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ext/examples/lambda2.rs b/ext/examples/lambda2.rs index fa27866010..01818774ee 100644 --- a/ext/examples/lambda2.rs +++ b/ext/examples/lambda2.rs @@ -232,9 +232,7 @@ where let both_odd = shift.n() % 2 != 0 && b.n() % 2 != 0; for prod in sec_e2.secondary_multiply_into(&x, b) { - let ext: Vec = prod.ext_part.iter().collect(); - let lambda: Vec = prod.lambda_part.iter().collect(); - if ext.iter().all(|&c| c == 0) && lambda.iter().all(|&c| c == 0) { + if prod.value.is_zero() { continue; } @@ -244,8 +242,9 @@ where "[x,y] = 0" }; println!( - "x_{x_gen} · [{src}] = {ext:?} + λ {lambda:?} {comm}", + "x_{x_gen} · [{src}] = {value} {comm}", src = prod.source.to_basis_string(), + value = prod.value, ); } } diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index 7683706343..f28377b494 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -17,9 +17,9 @@ use std::sync::{Arc, Mutex}; use algebra::pair_algebra::PairAlgebra; use dashmap::DashMap; use fp::{matrix::Subquotient, prime::Prime, vector::FpVector}; -use sseq::coordinates::{Bidegree, BidegreeElement, MultiDegree, MultiDegreeElement}; - -use sseq::coordinates::BidegreeGenerator; +use sseq::coordinates::{ + Bidegree, BidegreeElement, BidegreeGenerator, MultiDegree, MultiDegreeElement, +}; use super::ExtAlgebra; use crate::{ @@ -641,8 +641,8 @@ mod tests { 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(E at ({n},{s})) should equal dim(B at ({tn},{ts})): d2: E → B is an \ + isomorphism" ); } From 9998a8043c119eb9858ee785d5ccea69d3efa993 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 18:50:30 +0000 Subject: [PATCH 07/11] Move BZE to sseq crate, add classify/bze_indices to Sseq BZE classification (boundary/cycle/supports-differential) is intrinsic to any spectral sequence, not specific to the secondary layer. Move the enum to sseq::coordinates::BZE with from_page_data, Display. Add Sseq::classify and Sseq::bze_indices methods that read the page data directly. SecondaryExtAlgebra::classify now delegates to the lambda2 sseq. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/crates/sseq/src/coordinates/bze.rs | 50 ++++++++++++++++++++++++++ ext/crates/sseq/src/coordinates/mod.rs | 2 ++ ext/crates/sseq/src/sseq.rs | 28 ++++++++++++++- ext/src/ext_algebra/secondary.rs | 30 +++------------- 4 files changed, 83 insertions(+), 27 deletions(-) create mode 100644 ext/crates/sseq/src/coordinates/bze.rs 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 1c157865d1..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, }; @@ -390,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/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index f28377b494..a91108158e 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -17,6 +17,7 @@ use std::sync::{Arc, Mutex}; use algebra::pair_algebra::PairAlgebra; use dashmap::DashMap; use fp::{matrix::Subquotient, prime::Prime, vector::FpVector}; +pub use sseq::coordinates::BZE; use sseq::coordinates::{ Bidegree, BidegreeElement, BidegreeGenerator, MultiDegree, MultiDegreeElement, }; @@ -31,30 +32,6 @@ use crate::{ }, }; -/// The classification of an Ext generator in the $d_2$-adapted B/Z/E decomposition. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BZE { - /// Boundary: in the image of $d_2$ from another bidegree. - B, - /// Cycle mod boundary: survives to $E_3$. - Z, - /// Supports $d_2$: $d_2(x) \neq 0$. - E, -} - -impl BZE { - /// Classify a standard-basis generator `idx` from its $E_3$-page subquotient. - 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 - } -} - /// A single secondary product `x · y` in $\Mod_{C\lambda^2}$, where `y` is an $E_3$-surviving /// class. See [`SecondaryExtAlgebra::secondary_multiply_into`]. pub struct SecondaryProduct { @@ -331,8 +308,9 @@ where /// 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)}$. pub fn classify(&self, g: BidegreeGenerator) -> BZE { - let page = self.page_data(g.degree()); - BZE::from_page_data(&page, g.idx()) + let [n, s] = g.degree().coords(); + self.lambda2_sseq() + .classify(MultiDegree::new([n, s, 0]), 3, g.idx()) } /// Build the trigraded spectral sequence for $S/\lambda^2$. From 731f6cbbb093b328e7f04dec7062aa0db3c19fc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 18:56:09 +0000 Subject: [PATCH 08/11] Add Sseq<3, AdamsLambda2> to ExtAlgebra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExtAlgebra now carries a trigraded spectral sequence. With only weight 0 populated, d2 targets the empty weight-1 grading and is trivially zero, so the spectral sequence degenerates at E2 — matching the Adams spectral sequence for M. The secondary layer will clone and extend this base sseq with weight-1 data and d2 differentials on its own copy. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/src/ext_algebra/mod.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/ext/src/ext_algebra/mod.rs b/ext/src/ext_algebra/mod.rs index e014697c8b..ea3d5a3ca8 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -26,7 +26,7 @@ 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::{BZE, SecondaryExtAlgebra, SecondaryProduct}; pub use crate::secondary::{SecondaryDegree, SecondaryElement, SecondaryGenerator, Weight}; @@ -46,6 +46,10 @@ pub struct ExtAlgebra { is_unit: bool, /// One multiplication map per generator of $\Ext(M, k)$, built and extended on demand. products: DashMap>>, + /// Trigraded spectral sequence with the `AdamsLambda2` profile. Generators sit at weight 0; + /// weight 1 is empty, so $d_2$ has no target and the spectral sequence degenerates at $E_2$. + /// The secondary layer populates weight 1 and installs $d_2$ on its own copy. + sseq: sseq::Sseq<3, sseq::AdamsLambda2>, } impl ExtAlgebra { @@ -71,11 +75,13 @@ impl ExtAlgebra { /// Build an [`ExtAlgebra`] from an explicit `(resolution, unit)` pair. pub fn new(resolution: Arc, unit: Arc) -> Self { assert_eq!(resolution.prime(), unit.prime()); + let sseq = Self::build_sseq(&resolution); Self { is_unit: Arc::ptr_eq(&resolution, &unit), resolution, unit, products: DashMap::new(), + sseq, } } @@ -107,6 +113,24 @@ impl ExtAlgebra { self.resolution.prime() } + /// The trigraded spectral sequence for this Ext algebra. 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. + pub fn sseq(&self) -> &sseq::Sseq<3, sseq::AdamsLambda2> { + &self.sseq + } + + fn build_sseq(resolution: &CC) -> sseq::Sseq<3, sseq::AdamsLambda2> { + let p = resolution.prime(); + let mut sseq = sseq::Sseq::new(p); + for b in resolution.iter_stem() { + let dim = 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); From 59e54644196cb30c103ca4de03a8f12422333a3d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 19:08:04 +0000 Subject: [PATCH 09/11] =?UTF-8?q?Add=20PiGenerator=20and=20PiElement=20typ?= =?UTF-8?q?es=20for=20=CF=80(S/=CE=BB=C2=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PiGenerator represents a conical basis generator of π(S/λ²) (Condition 3.2 of the paper), annotated with its Adams BZE classification and weight. PiElement is an element in the E3 subquotient at a specific weight, with coordinates in the surviving-generator basis. New methods on SecondaryExtAlgebra: - adams_classify: full BZE using both weights of the lambda2 sseq - pi_basis: conical basis at a bidegree (B→wt0, Z→both, E→wt1) - to_pi: project a SecondaryElement to E3 subquotients at each weight Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/src/ext_algebra/mod.rs | 2 +- ext/src/ext_algebra/secondary.rs | 267 ++++++++++++++++++++++++++++++- 2 files changed, 267 insertions(+), 2 deletions(-) diff --git a/ext/src/ext_algebra/mod.rs b/ext/src/ext_algebra/mod.rs index ea3d5a3ca8..feac781bca 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -28,7 +28,7 @@ use dashmap::DashMap; use fp::{matrix::Matrix, prime::ValidPrime, vector::FpVector}; use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator, MultiDegree}; -pub use self::secondary::{BZE, 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}, diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index a91108158e..53573169ef 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -12,7 +12,10 @@ //! 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; @@ -42,6 +45,121 @@ pub struct SecondaryProduct { 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 +/// [`adams_classify`](SecondaryExtAlgebra::adams_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 basis of $E_3$ at the given bidegree and weight. Each +/// coordinate corresponds to a surviving generator in the ambient Ext space, identified by +/// [`basis_indices`](Self::basis_indices). +#[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$ subquotient basis. `coords()[i]` is the coefficient of the + /// generator at ambient index [`basis_indices()`](Self::basis_indices)`[i]`. + pub fn coords(&self) -> &[u32] { + &self.coords + } + + /// The ambient Ext generator indices forming the $E_3$ basis. + 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}$ /// product. See the [module documentation](self). pub struct SecondaryExtAlgebra @@ -313,6 +431,102 @@ where .classify(MultiDegree::new([n, s, 0]), 3, g.idx()) } + /// The full Adams BZE classification, combining both weights of the $\lambda^2$ spectral + /// sequence. + /// + /// Unlike [`classify`](Self::classify) (which only inspects weight 0), this checks both: + /// - **E**: supports $d_2$ at weight 0. + /// - **B**: boundary of $d_2$ at weight 1. + /// - **Z**: permanent cycle (neither E nor B). + pub fn adams_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 [`adams_classify`](Self::adams_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.adams_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(); + let coords = sq.reduce(v.as_slice_mut()); + + 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 @@ -651,4 +865,55 @@ mod tests { 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.adams_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.adams_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.adams_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()); + } } From fa37ad91dbd0393793f344392548025dbb122fdc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 19:14:36 +0000 Subject: [PATCH 10/11] Update examples to use PiGenerator/PiElement types; regenerate benchmarks lambda2.rs: Table III now uses pi_basis() for the conical basis listing, and Table IV uses to_pi() to project products into the E3 subquotient. Both tables use adams_classify for proper B/Z/E distinction. Benchmark files regenerated for secondary_product examples to match the SecondaryElement Display format introduced by the secondary_element branch. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- .../benchmarks/secondary_product-h_0-S_2 | 74 +++++++++---------- .../secondary_product-h_0-S_2-nassau | 74 +++++++++---------- .../benchmarks/secondary_product-v_1-C2 | 70 +++++++++--------- ext/examples/lambda2.rs | 56 ++++++-------- 4 files changed, 131 insertions(+), 143 deletions(-) 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 index 01818774ee..bffdaf2bec 100644 --- a/ext/examples/lambda2.rs +++ b/ext/examples/lambda2.rs @@ -88,7 +88,7 @@ where let dim = e2.dimension(b); for i in 0..dim { let g = BidegreeGenerator::new(b, i); - match sec_e2.classify(g) { + match sec_e2.adams_classify(g) { BZE::Z => println!("Z x_{g}"), BZE::B => println!("B x_{g}"), BZE::E => { @@ -147,31 +147,14 @@ where CC::Algebra: algebra::pair_algebra::PairAlgebra, { for b in e2.resolution().iter_stem() { - let [n, s] = b.coords(); - let dim = e2.dimension(b); - if dim == 0 { + if e2.dimension(b) == 0 { continue; } - - for i in 0..dim { - let g = BidegreeGenerator::new(b, i); - let bze = sec_e2.classify(g); - - // bock=0: B and Z survive (d2-cycles). - match bze { - BZE::B | BZE::Z => { - let label = if bze == BZE::B { "B" } else { "Z" }; - println!("{label} x_{g}^0 ({n}, {s}, 0)"); - } - BZE::E => {} - } - - // bock=1: Z and E survive (cokernel of d2). - match bze { - BZE::Z => println!("Z x_{g}^1 ({n}, {s}, 1)"), - BZE::E => println!("E x_{g}^1 ({n}, {s}, 1)"), - BZE::B => {} - } + 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()); } } } @@ -179,8 +162,8 @@ where /// 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. The secondary product lives -/// in Mod_{Cλ²}: it has an Ext part and a λ part. +/// 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. @@ -189,8 +172,6 @@ where CC: FreeChainComplex + ext::chain_complex::AugmentedChainComplex, CC::Algebra: algebra::pair_algebra::PairAlgebra, { - // Secondary products need s ≥ 1 for the multiplier (the secondary lift's shift is s+1, - // and secondary homotopies start at s=2). let z_gens: Vec = e2 .resolution() .iter_nonzero_stem() @@ -198,7 +179,7 @@ where .flat_map(|b| { let dim = e2.dimension(b); (0..dim) - .filter(move |&i| sec_e2.classify(BidegreeGenerator::new(b, i)) == BZE::Z) + .filter(move |&i| sec_e2.adams_classify(BidegreeGenerator::new(b, i)) == BZE::Z) .map(move |i| BidegreeGenerator::new(b, i)) }) .collect(); @@ -227,12 +208,11 @@ where continue; } - // Proposition 6.4: [x, y] = x·y − y·x = 2·(x·y) when both stems are odd, - // and [x, y] = 0 when at least one stem is even. let both_odd = shift.n() % 2 != 0 && b.n() % 2 != 0; for prod in sec_e2.secondary_multiply_into(&x, b) { - if prod.value.is_zero() { + let (pi_ext, pi_lambda) = sec_e2.to_pi(&prod.value); + if pi_ext.is_zero() && pi_lambda.is_zero() { continue; } @@ -241,10 +221,18 @@ where } 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} {comm}", + "x_{x_gen} · [{src}] = {value_str} {comm}", src = prod.source.to_basis_string(), - value = prod.value, ); } } From ef09a7a92bc2bdc1f7b3df4d8d8dc2de8d176dde Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 19:57:01 +0000 Subject: [PATCH 11/11] Address review: boundary-aware classify, on-demand sseq, robust labels - classify() now inspects both weights of the lambda2 sseq so it reports boundaries (B). The weight-0 copy only carries outgoing d2, so the old single-weight version could never return B and mislabeled boundaries as Z. Consolidates the former adams_classify into classify and drops the redundant method. - ExtAlgebra::sseq() is computed on demand from the current resolution instead of cached in new(), so it never goes stale after the resolution is extended. - secondary_product example classifies each source by membership in the boundary subspace rather than by its first nonzero basis index, which was basis-order dependent for mixed sources. - lambda2 example bails on non-unit modules, since its tables read multiplicands from the resolution basis and feed them as unit generators (valid only when M == k). - Clarify PiElement docs: coords are in the E3 generator basis, labeled by each surviving class's leading (pivot) ambient index, which lines up with Subquotient::reduce entry-for-entry. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017kezHiWcYod7hYovRPczz9 --- ext/examples/lambda2.rs | 21 ++++++------ ext/examples/secondary_product.rs | 19 ++++------- ext/src/ext_algebra/mod.rs | 28 ++++++--------- ext/src/ext_algebra/secondary.rs | 57 ++++++++++++++++--------------- 4 files changed, 58 insertions(+), 67 deletions(-) diff --git a/ext/examples/lambda2.rs b/ext/examples/lambda2.rs index bffdaf2bec..7d24ea396d 100644 --- a/ext/examples/lambda2.rs +++ b/ext/examples/lambda2.rs @@ -17,9 +17,8 @@ use std::sync::Arc; -use algebra::module::Module; use ext::{ - chain_complex::{ChainComplex, FreeChainComplex}, + chain_complex::FreeChainComplex, ext_algebra::{BZE, ExtAlgebra, secondary::SecondaryExtAlgebra}, secondary::LAMBDA_BIDEGREE, utils::query_module, @@ -35,13 +34,13 @@ fn main() -> anyhow::Result<()> { let resolution = Arc::new(query_module(Some(algebra::AlgebraType::Milnor), true)?); let e2 = Arc::new(ExtAlgebra::from_resolution(Arc::clone(&resolution))?); - if !e2.is_unit() { - let max = Bidegree::n_s( - resolution.module(0).max_computed_degree(), - resolution.next_homological_degree() - 1, - ); - e2.unit().compute_through_stem(max); - } + // 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))); @@ -88,7 +87,7 @@ where let dim = e2.dimension(b); for i in 0..dim { let g = BidegreeGenerator::new(b, i); - match sec_e2.adams_classify(g) { + match sec_e2.classify(g) { BZE::Z => println!("Z x_{g}"), BZE::B => println!("B x_{g}"), BZE::E => { @@ -179,7 +178,7 @@ where .flat_map(|b| { let dim = e2.dimension(b); (0..dim) - .filter(move |&i| sec_e2.adams_classify(BidegreeGenerator::new(b, i)) == BZE::Z) + .filter(move |&i| sec_e2.classify(BidegreeGenerator::new(b, i)) == BZE::Z) .map(move |i| BidegreeGenerator::new(b, i)) }) .collect(); diff --git a/ext/examples/secondary_product.rs b/ext/examples/secondary_product.rs index 871c168f84..6cce13b2b1 100644 --- a/ext/examples/secondary_product.rs +++ b/ext/examples/secondary_product.rs @@ -112,19 +112,14 @@ fn main() -> anyhow::Result<()> { } } - // Secondary products with surviving classes (B and Z). + // 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 bze = prod - .source - .vec() - .iter_nonzero() - .next() - .map(|(i, _)| BZE::from_page_data(&page, i)) - .unwrap_or(BZE::Z); - let label = match bze { - BZE::B => "B", - BZE::Z => "Z", - BZE::E => "E", + let label = if page.zeros().contains(prod.source.vec()) { + "B" + } else { + "Z" }; println!( "{label} {disp} [{basis}] = {value}", diff --git a/ext/src/ext_algebra/mod.rs b/ext/src/ext_algebra/mod.rs index feac781bca..6fe016c07b 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -46,10 +46,6 @@ pub struct ExtAlgebra { is_unit: bool, /// One multiplication map per generator of $\Ext(M, k)$, built and extended on demand. products: DashMap>>, - /// Trigraded spectral sequence with the `AdamsLambda2` profile. Generators sit at weight 0; - /// weight 1 is empty, so $d_2$ has no target and the spectral sequence degenerates at $E_2$. - /// The secondary layer populates weight 1 and installs $d_2$ on its own copy. - sseq: sseq::Sseq<3, sseq::AdamsLambda2>, } impl ExtAlgebra { @@ -75,13 +71,11 @@ impl ExtAlgebra { /// Build an [`ExtAlgebra`] from an explicit `(resolution, unit)` pair. pub fn new(resolution: Arc, unit: Arc) -> Self { assert_eq!(resolution.prime(), unit.prime()); - let sseq = Self::build_sseq(&resolution); Self { is_unit: Arc::ptr_eq(&resolution, &unit), resolution, unit, products: DashMap::new(), - sseq, } } @@ -113,18 +107,18 @@ impl ExtAlgebra { self.resolution.prime() } - /// The trigraded spectral sequence for this Ext algebra. 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. - pub fn sseq(&self) -> &sseq::Sseq<3, sseq::AdamsLambda2> { - &self.sseq - } - - fn build_sseq(resolution: &CC) -> sseq::Sseq<3, sseq::AdamsLambda2> { - let p = 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 resolution.iter_stem() { - let dim = resolution.number_of_gens_in_bidegree(b); + 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); } diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index 53573169ef..0d2ec84d2e 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -49,7 +49,7 @@ pub struct SecondaryProduct { /// /// Each Ext generator at bidegree $(n, s)$ contributes to the conical basis at one or both weights, /// determined by its Adams BZE classification (see -/// [`adams_classify`](SecondaryExtAlgebra::adams_classify)): +/// [`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$). @@ -98,9 +98,13 @@ impl fmt::Display for PiGenerator { /// An element in the $E_3 = E_\infty$ page of $\pi(S/\lambda^2)$ at a specific weight. /// -/// The coordinates are in the subquotient basis of $E_3$ at the given bidegree and weight. Each -/// coordinate corresponds to a surviving generator in the ambient Ext space, identified by -/// [`basis_indices`](Self::basis_indices). +/// 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, @@ -118,13 +122,14 @@ impl PiElement { self.weight } - /// Coordinates in the $E_3$ subquotient basis. `coords()[i]` is the coefficient of the - /// generator at ambient index [`basis_indices()`](Self::basis_indices)`[i]`. + /// 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 ambient Ext generator indices forming the $E_3$ basis. + /// 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 } @@ -417,7 +422,7 @@ where &d[std::cmp::min(3, d.len() - 1)] } - /// Classify a generator at bidegree `b` as B, Z, or E in the $d_2$ decomposition. + /// 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$. @@ -425,20 +430,12 @@ where /// /// 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)}$. - pub fn classify(&self, g: BidegreeGenerator) -> BZE { - let [n, s] = g.degree().coords(); - self.lambda2_sseq() - .classify(MultiDegree::new([n, s, 0]), 3, g.idx()) - } - - /// The full Adams BZE classification, combining both weights of the $\lambda^2$ spectral - /// sequence. /// - /// Unlike [`classify`](Self::classify) (which only inspects weight 0), this checks both: - /// - **E**: supports $d_2$ at weight 0. - /// - **B**: boundary of $d_2$ at weight 1. - /// - **Z**: permanent cycle (neither E nor B). - pub fn adams_classify(&self, g: BidegreeGenerator) -> BZE { + /// 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(); @@ -457,8 +454,8 @@ where /// 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 [`adams_classify`](Self::adams_classify), then - /// placed at the weights it contributes to: + /// 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. @@ -468,7 +465,7 @@ where for i in 0..dim { let g = BidegreeGenerator::new(b, i); - let bze = self.adams_classify(g); + let bze = self.classify(g); if bze != BZE::E { result.push(PiGenerator::new(b, Weight::Ext, bze, i)); @@ -504,8 +501,14 @@ where 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)) @@ -875,7 +878,7 @@ mod tests { sec_e2.extend_all(); // h0 at (0, 1) is a permanent Z-cycle: appears at both weights. - let h0_bze = sec_e2.adams_classify(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); + 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 @@ -885,7 +888,7 @@ mod tests { 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.adams_classify(BidegreeGenerator::new(Bidegree::n_s(15, 1), 0)); + 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); @@ -893,7 +896,7 @@ mod tests { 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.adams_classify(BidegreeGenerator::new(Bidegree::n_s(14, 3), 0)); + 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);