From 264e1ce9f00d6f6291b0b46d5ff8977b43829072 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 05:39:38 +0000 Subject: [PATCH] =?UTF-8?q?Add=20coordinate=20types=20to=20index=20Seconda?= =?UTF-8?q?ryExtAlgebra=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>>,