From 0f42179c72dfb9fc5a73bd240ab81fbe51f4ad83 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 23:04:06 +0000 Subject: [PATCH 1/3] Add ExtAlgebra: a bigraded-algebra view over resolutions Introduce `ext::ext_algebra::ExtAlgebra`, a thin ergonomics layer that presents Ext(M, k) as a bigraded module over the bigraded algebra Ext(k, k). It wraps a resolution (and the unit resolution) and exposes the bigraded basis plus products, so computing a product is a single `multiply`/`multiply_into` call instead of the manual ResolutionHomomorphism + extend + hom_k plumbing the examples re-derive. Products reuse the existing machinery: one ResolutionHomomorphism is built and cached per generator of Ext(M, k) (keyed by BidegreeGenerator), and a product by a general class is assembled at request time as the matching linear combination of generator maps. No linear-algebra core is reimplemented. `multiply_into` returns `Option` (rows = unit generators, columns = target generators), yielding `None` when a bidegree is out of the computed range rather than silently returning zeros; `try_multiply` is the corresponding safe variant and `multiply` panics out of range. Adds a streamlined `product` example and a unit test on S_2 (h_0^2 != 0, h_0 h_1 = h_1 h_0 = 0). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XYWcvWZPm3YJkVpCmeGYsP --- ext/examples/product.rs | 45 +++++++ ext/src/ext_algebra.rs | 273 ++++++++++++++++++++++++++++++++++++++++ ext/src/lib.rs | 1 + 3 files changed, 319 insertions(+) create mode 100644 ext/examples/product.rs create mode 100644 ext/src/ext_algebra.rs diff --git a/ext/examples/product.rs b/ext/examples/product.rs new file mode 100644 index 0000000000..5226f46309 --- /dev/null +++ b/ext/examples/product.rs @@ -0,0 +1,45 @@ +//! Computes products in Ext by left-multiplication by a fixed class. +//! +//! The program asks for a module `M` and a class `x ∈ Ext(M, k)`. It then prints the products of +//! `x` with every basis class of `Ext(k, k)` that lands in a computed bidegree. +//! +//! This is the primary (i.e. non-secondary) analogue of [`secondary_product`](../secondary_product), +//! written against the [`ExtAlgebra`] abstraction so the plumbing stays out of the way. + +use std::sync::Arc; + +use ext::{chain_complex::FreeChainComplex, ext_algebra::ExtAlgebra, utils::query_module}; +use sseq::coordinates::Bidegree; + +fn main() -> anyhow::Result<()> { + ext::utils::init_logging()?; + + let resolution = Arc::new(query_module(None, true)?); + let alg = ExtAlgebra::from_resolution(resolution)?; + + let shift = Bidegree::n_s( + query::raw("n of Ext class", str::parse), + query::raw("s of Ext class", str::parse), + ); + + let dim = alg.dimension(shift); + if dim == 0 { + panic!("No classes in bidegree {shift}"); + } + let v: Vec = query::vector("Input Ext class", dim); + let x = alg.element(shift, &v); + + for b in alg.unit().iter_nonzero_stem() { + // `None` means `b + shift` is out of the computed range, so skip it. + let Some(rows) = alg.multiply_into(&x, b) else { + continue; + }; + for (g, row) in alg.unit_basis(b).into_iter().zip(rows.iter()) { + let coords: Vec = row.iter().collect(); + if coords.iter().any(|&c| c != 0) { + println!("x · x_{g} = {coords:?}"); + } + } + } + Ok(()) +} diff --git a/ext/src/ext_algebra.rs b/ext/src/ext_algebra.rs new file mode 100644 index 0000000000..dc1aacb736 --- /dev/null +++ b/ext/src/ext_algebra.rs @@ -0,0 +1,273 @@ +//! A bigraded-algebra view of a resolution. +//! +//! [`ExtAlgebra`] wraps a resolution of a module `M` together with the resolution of the base +//! field `k` (the "unit"), and presents $\Ext(M, k)$ as a bigraded module over the bigraded +//! algebra $\Ext(k, k)$. When `M == k` this is the algebra $\Ext(k, k)$ itself. +//! +//! The goal is ergonomics: computing a product of Ext classes is a single [`ExtAlgebra::multiply`] +//! call instead of the manual [`ResolutionHomomorphism`] + `extend` + `hom_k` plumbing that the +//! examples currently re-derive. This is the foundational layer; the secondary differential ($d_2$) +//! and Massey products are planned follow-ups. +//! +//! # Conventions +//! A product is realised by a [`ResolutionHomomorphism`] built from a fixed multiplier class living +//! in $\Ext(M, k)$ (source = resolution of `M`, target = resolution of `k`). That single chain map +//! computes the products of the multiplier with *all* classes of $\Ext(k, k)$. We cache one such +//! map per *generator* of $\Ext(M, k)$ (keyed by [`BidegreeGenerator`]); a product by a general +//! class is assembled at request time as the corresponding linear combination of generator maps. + +use std::sync::Arc; + +use dashmap::DashMap; +use fp::{matrix::Matrix, prime::ValidPrime, vector::FpVector}; +use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator}; + +use crate::{ + chain_complex::{AugmentedChainComplex, FreeChainComplex}, + resolution_homomorphism::ResolutionHomomorphism, + utils::{QueryModuleResolution, get_unit}, +}; + +/// $\Ext(M, k)$ as a bigraded module over the bigraded algebra $\Ext(k, k)$, backed by a +/// resolution. See the [module-level documentation](self) for conventions. +pub struct ExtAlgebra { + /// Resolution of `M`; products land in its Ext. + resolution: Arc, + /// Resolution of the base field `k`. `Arc`-shared with `resolution` when `M == k`. + unit: Arc, + is_unit: bool, + /// One multiplication map per generator of $\Ext(M, k)$, built and extended on demand. + products: DashMap>>, +} + +impl ExtAlgebra { + /// Build an [`ExtAlgebra`] from a resolution, deriving the unit via [`get_unit`]. + /// + /// This may prompt for the unit's save directory when `M != k` (see [`get_unit`]); for a fully + /// non-interactive setup, use [`ExtAlgebra::new`] with an explicit unit instead. + pub fn from_resolution(resolution: Arc) -> anyhow::Result { + let (is_unit, unit) = get_unit(Arc::clone(&resolution))?; + Ok(Self::new(resolution, unit, is_unit)) + } + + /// Ensure both the resolution and the unit are computed through the given stem. + pub fn compute_through_stem(&self, max: Bidegree) { + self.unit.compute_through_stem(max); + if !self.is_unit { + self.resolution.compute_through_stem(max); + } + } +} + +impl ExtAlgebra { + /// Build an [`ExtAlgebra`] from an explicit `(resolution, unit)` pair. Pass `is_unit = true` + /// (and the same `Arc` for both) exactly when `M == k`. + pub fn new(resolution: Arc, unit: Arc, is_unit: bool) -> Self { + Self { + resolution, + unit, + is_unit, + products: DashMap::new(), + } + } + + pub fn resolution(&self) -> &Arc { + &self.resolution + } + + pub fn unit(&self) -> &Arc { + &self.unit + } + + pub fn is_unit(&self) -> bool { + self.is_unit + } + + pub fn prime(&self) -> ValidPrime { + self.resolution.prime() + } + + /// 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); + if !self.is_unit { + self.resolution.compute_through_bidegree(b); + } + } + + /// The dimension of $\Ext^{s,t}(M, k)$ at the given bidegree. + pub fn dimension(&self, b: Bidegree) -> usize { + self.resolution.number_of_gens_in_bidegree(b) + } + + /// The basis generators of $\Ext(M, k)$ at the given bidegree. + pub fn basis(&self, b: Bidegree) -> Vec { + (0..self.dimension(b)) + .map(|i| BidegreeGenerator::new(b, i)) + .collect() + } + + /// A class in $\Ext(M, k)$ from its coordinates in the generator basis at bidegree `b`. + pub fn element(&self, b: Bidegree, coords: &[u32]) -> BidegreeElement { + BidegreeElement::new(b, FpVector::from_slice(self.prime(), coords)) + } + + /// A single generator of $\Ext(M, k)$ as a class. + pub fn generator(&self, g: BidegreeGenerator) -> BidegreeElement { + g.into_element(self.prime(), self.dimension(g.degree())) + } + + /// The dimension of $\Ext(k, k)$ at the given bidegree (the multiplicand/"scalar" side). + pub fn unit_dimension(&self, b: Bidegree) -> usize { + self.unit.number_of_gens_in_bidegree(b) + } + + /// The basis generators of $\Ext(k, k)$ at the given bidegree. + pub fn unit_basis(&self, b: Bidegree) -> Vec { + (0..self.unit_dimension(b)) + .map(|i| BidegreeGenerator::new(b, i)) + .collect() + } + + /// A class in $\Ext(k, k)$ from its coordinates in the generator basis at bidegree `b`. + pub fn unit_element(&self, b: Bidegree, coords: &[u32]) -> BidegreeElement { + BidegreeElement::new(b, FpVector::from_slice(self.prime(), coords)) + } +} + +impl ExtAlgebra +where + CC: FreeChainComplex + AugmentedChainComplex, +{ + /// The multiplication map for a single generator `g` of $\Ext(M, k)$, built and cached on + /// first use. The returned map is *not* guaranteed to be extended; [`ExtAlgebra::multiply_into`] + /// extends it as needed. + pub fn generator_product_map( + &self, + g: BidegreeGenerator, + ) -> Arc> { + if let Some(map) = self.products.get(&g) { + return Arc::clone(&map); + } + + let dim = self.resolution.number_of_gens_in_bidegree(g.degree()); + let mut class = vec![0u32; dim]; + class[g.idx()] = 1; + + let name = format!("prod_{}_{}_{}", g.n(), g.s(), g.idx()); + let hom = Arc::new(ResolutionHomomorphism::from_class( + name, + Arc::clone(&self.resolution), + Arc::clone(&self.unit), + g.degree(), + &class, + )); + + Arc::clone(self.products.entry(g).or_insert(hom).value()) + } + + /// Left-multiplication by the class `x` (in $\Ext(M, k)$), applied to every basis generator of + /// $\Ext(k, k)$ at bidegree `b`. + /// + /// Returns `None` when the product is out of the computed range — that is, when `b` or + /// `b + x.degree()` has not been resolved — so callers never mistake an uncomputed product for a + /// zero one. Otherwise returns a matrix with one row per generator of $\Ext(k, k)$ at `b`; row + /// `j` is the product `x · g_j` expressed in the generator basis of $\Ext(M, k)$ at bidegree + /// `b + x.degree()`. A computed-but-empty bidegree yields a valid zero-dimension matrix, not + /// `None`. + pub fn multiply_into(&self, x: &BidegreeElement, b: Bidegree) -> Option { + let shift = x.degree(); + let target = b + shift; + + if !self.unit.has_computed_bidegree(b) || !self.resolution.has_computed_bidegree(target) { + return None; + } + + let unit_dim = self.unit.number_of_gens_in_bidegree(b); + let res_dim = self.resolution.number_of_gens_in_bidegree(target); + let mut matrix = Matrix::new(self.prime(), unit_dim, res_dim); + + for (i, c) in x.vec().iter_nonzero() { + let map = self.generator_product_map(BidegreeGenerator::new(shift, i)); + map.extend_all(); + + // `hom_k(b.t())[j][k]`: `j` indexes the multiplicand generator of the unit at `b`, `k` + // indexes the result generator of the resolution at `target`. + let hom_k = map.get_map(target.s()).hom_k(b.t()); + for (j, row) in hom_k.iter().enumerate() { + for (k, &v) in row.iter().enumerate() { + matrix.row_mut(j).add_basis_element(k, c * v); + } + } + } + Some(matrix) + } + + /// The product `x · y` if it lies in the computed range, else `None`. See + /// [`multiply_into`](Self::multiply_into) for the operand conventions. The result lies in + /// bidegree `x.degree() + y.degree()`. + pub fn try_multiply( + &self, + x: &BidegreeElement, + y: &BidegreeElement, + ) -> Option { + let target = x.degree() + y.degree(); + let matrix = self.multiply_into(x, y.degree())?; + let mut out = FpVector::new(self.prime(), matrix.columns()); + for (j, c) in y.vec().iter_nonzero() { + out.as_slice_mut().add(matrix.row(j), c); + } + Some(BidegreeElement::new(target, out)) + } + + /// The product `x · y`, where `x ∈ Ext(M, k)` and `y ∈ Ext(k, k)`. When `M == k` both operands + /// live in the same algebra $\Ext(k, k)$. The result lies in bidegree `x.degree() + y.degree()`. + /// + /// Panics if the product is out of the computed range; use + /// [`try_multiply`](Self::try_multiply) to handle that case. + pub fn multiply(&self, x: &BidegreeElement, y: &BidegreeElement) -> BidegreeElement { + self.try_multiply(x, y).expect( + "multiply: product is out of the computed range; compute further or use try_multiply", + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::construct_standard; + + #[test] + fn test_sphere_products() { + let res = Arc::new(construct_standard::("S_2", None).unwrap()); + res.compute_through_stem(Bidegree::n_s(8, 8)); + let alg = ExtAlgebra::new(Arc::clone(&res), res, true); + + // h_i live in Ext^{1, *}: h_0 = (n=0, s=1), h_1 = (n=1, s=1), h_2 = (n=3, s=1). + let h0 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); + let h1 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(1, 1), 0)); + + // h_0^2 is the nonzero generator of Ext^{2,2} = (n=0, s=2). + let h0_sq = alg.multiply(&h0, &h0); + assert_eq!(h0_sq.degree(), Bidegree::n_s(0, 2)); + assert_eq!(alg.dimension(Bidegree::n_s(0, 2)), 1); + assert!(!h0_sq.vec().is_zero(), "h_0^2 should be nonzero"); + + // The Adams relations h_0 h_1 = 0 = h_1 h_0. + assert!( + alg.multiply(&h0, &h1).vec().is_zero(), + "h_0 h_1 should vanish" + ); + assert!( + alg.multiply(&h1, &h0).vec().is_zero(), + "h_1 h_0 should vanish" + ); + + // Cross-check `multiply` against a direct `hom_k` read for h_0 · h_1. + let rows = alg + .multiply_into(&h0, h1.degree()) + .expect("h_0 · h_1 is in range"); + let direct: u32 = rows.row(0).iter().sum(); + assert_eq!(direct, 0); + } +} diff --git a/ext/src/lib.rs b/ext/src/lib.rs index ad5a6afb26..a0b9b6fd7b 100644 --- a/ext/src/lib.rs +++ b/ext/src/lib.rs @@ -168,6 +168,7 @@ #![deny(clippy::use_self, unsafe_op_in_unsafe_fn)] pub mod chain_complex; +pub mod ext_algebra; pub mod resolution; pub mod resolution_homomorphism; pub mod save; From d48a421583901d1fe8066dc518b0c31fc0c0f8e5 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer Date: Thu, 25 Jun 2026 09:59:42 -0400 Subject: [PATCH 2/3] Various streamlining --- ext/src/ext_algebra.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ext/src/ext_algebra.rs b/ext/src/ext_algebra.rs index dc1aacb736..27c3432233 100644 --- a/ext/src/ext_algebra.rs +++ b/ext/src/ext_algebra.rs @@ -46,8 +46,8 @@ impl ExtAlgebra { /// This may prompt for the unit's save directory when `M != k` (see [`get_unit`]); for a fully /// non-interactive setup, use [`ExtAlgebra::new`] with an explicit unit instead. pub fn from_resolution(resolution: Arc) -> anyhow::Result { - let (is_unit, unit) = get_unit(Arc::clone(&resolution))?; - Ok(Self::new(resolution, unit, is_unit)) + let (_, unit) = get_unit(Arc::clone(&resolution))?; + Ok(Self::new(resolution, unit)) } /// Ensure both the resolution and the unit are computed through the given stem. @@ -60,13 +60,12 @@ impl ExtAlgebra { } impl ExtAlgebra { - /// Build an [`ExtAlgebra`] from an explicit `(resolution, unit)` pair. Pass `is_unit = true` - /// (and the same `Arc` for both) exactly when `M == k`. - pub fn new(resolution: Arc, unit: Arc, is_unit: bool) -> Self { + /// Build an [`ExtAlgebra`] from an explicit `(resolution, unit)` pair. + pub fn new(resolution: Arc, unit: Arc) -> Self { Self { + is_unit: Arc::ptr_eq(&resolution, &unit), resolution, unit, - is_unit, products: DashMap::new(), } } @@ -109,11 +108,14 @@ impl ExtAlgebra { /// A class in $\Ext(M, k)$ from its coordinates in the generator basis at bidegree `b`. pub fn element(&self, b: Bidegree, coords: &[u32]) -> BidegreeElement { + assert_eq!(self.dimension(b), coords.len()); BidegreeElement::new(b, FpVector::from_slice(self.prime(), coords)) } /// A single generator of $\Ext(M, k)$ as a class. pub fn generator(&self, g: BidegreeGenerator) -> BidegreeElement { + let ambient = self.dimension(g.degree()); + assert!(ambient >= g.idx()); g.into_element(self.prime(), self.dimension(g.degree())) } @@ -131,6 +133,7 @@ impl ExtAlgebra { /// A class in $\Ext(k, k)$ from its coordinates in the generator basis at bidegree `b`. pub fn unit_element(&self, b: Bidegree, coords: &[u32]) -> BidegreeElement { + assert_eq!(self.unit_dimension(b), coords.len()); BidegreeElement::new(b, FpVector::from_slice(self.prime(), coords)) } } @@ -241,7 +244,7 @@ mod tests { fn test_sphere_products() { let res = Arc::new(construct_standard::("S_2", None).unwrap()); res.compute_through_stem(Bidegree::n_s(8, 8)); - let alg = ExtAlgebra::new(Arc::clone(&res), res, true); + let alg = ExtAlgebra::new(Arc::clone(&res), res); // h_i live in Ext^{1, *}: h_0 = (n=0, s=1), h_1 = (n=1, s=1), h_2 = (n=3, s=1). let h0 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); From 1500abb8019fc42e0300dcf3412cf39d21580c58 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer Date: Thu, 25 Jun 2026 10:30:43 -0400 Subject: [PATCH 3/3] Quick fixup --- ext/src/ext_algebra.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/src/ext_algebra.rs b/ext/src/ext_algebra.rs index 27c3432233..344219a257 100644 --- a/ext/src/ext_algebra.rs +++ b/ext/src/ext_algebra.rs @@ -62,6 +62,7 @@ impl ExtAlgebra { 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()); Self { is_unit: Arc::ptr_eq(&resolution, &unit), resolution, @@ -115,7 +116,7 @@ impl ExtAlgebra { /// A single generator of $\Ext(M, k)$ as a class. pub fn generator(&self, g: BidegreeGenerator) -> BidegreeElement { let ambient = self.dimension(g.degree()); - assert!(ambient >= g.idx()); + assert!(ambient > g.idx()); g.into_element(self.prime(), self.dimension(g.degree())) }