From 003704a4fc703dffee7f325b0164e9006e017521 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 00:00:27 +0000 Subject: [PATCH 1/3] Add SecondaryExtAlgebra: the d2 differential layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `ext::ext_algebra::secondary::SecondaryExtAlgebra`, the secondary layer over `ExtAlgebra`. It composes the primary algebra with the secondary resolutions of M and the unit k, and exposes the secondary differential `d2` (with a `survives`/permanent-cycle check), the E3-page `page_data`, and the Mod_Cλ² secondary product `secondary_multiply_into`. These wrap `SecondaryResolution` and `SecondaryResolutionHomomorphism`; no new linear algebra is added. The layer is split from `ExtAlgebra` because the secondary machinery requires `CC::Algebra: PairAlgebra`, a bound the primary layer does not impose. To support this, `ext_algebra.rs` becomes the module directory `ext_algebra/{mod,secondary}.rs`. A `compute_partial` passthrough and a public `secondary_product_lift` preserve the per-`s` sharding workflow, and a unit-free `ExtAlgebra::without_unit` constructor supports the d2-only path. Refactors the `secondary` (d2 lister) and `secondary_product` examples onto the layer; both produce byte-identical output to before (verified on S_2). Adds a unit test asserting the first Adams differential d2(h4) = h0 h3^2 is nonzero and that h0, h1, h2 are permanent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XYWcvWZPm3YJkVpCmeGYsP --- ext/examples/secondary.rs | 32 +- ext/examples/secondary_product.rs | 148 +++------ .../{ext_algebra.rs => ext_algebra/mod.rs} | 17 + ext/src/ext_algebra/secondary.rs | 302 ++++++++++++++++++ 4 files changed, 381 insertions(+), 118 deletions(-) rename ext/src/{ext_algebra.rs => ext_algebra/mod.rs} (92%) create mode 100644 ext/src/ext_algebra/secondary.rs diff --git a/ext/examples/secondary.rs b/ext/examples/secondary.rs index 29a43bc32c..44c5cc3b0d 100644 --- a/ext/examples/secondary.rs +++ b/ext/examples/secondary.rs @@ -50,41 +50,47 @@ use std::sync::Arc; use algebra::module::Module; use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - secondary::*, + ext_algebra::{ExtAlgebra, secondary::SecondaryExtAlgebra}, utils::query_module, }; -use sseq::coordinates::{Bidegree, BidegreeGenerator}; +use sseq::coordinates::Bidegree; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; let resolution = Arc::new(query_module(Some(algebra::AlgebraType::Milnor), true)?); - let lift = SecondaryResolution::new(Arc::clone(&resolution)); + // The d2 differential is intrinsic to the resolution and needs no unit, so we avoid the unit + // setup with `without_unit`. + let sec_e2 = SecondaryExtAlgebra::new(Arc::new(ExtAlgebra::without_unit(resolution))); + if let Some(s) = ext::utils::secondary_job() { - lift.compute_partial(s); + sec_e2.compute_partial(s); return Ok(()); } - lift.extend_all(); + sec_e2.extend_all(); + let e2 = sec_e2.ext_algebra(); let d2_shift = Bidegree::n_s(-1, 2); - // Iterate through target of the d2 - for b in lift.underlying().iter_nonzero_stem() { + // 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; } - if b.t() - 1 > resolution.module(b.s() - 2).max_computed_degree() { + // 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; } - let homotopy = lift.homotopy(b.s()); - let m = homotopy.homotopies.hom_k(b.t() - 1); - for (i, entry) in m.into_iter().enumerate() { - let source_gen = BidegreeGenerator::new(b - d2_shift, i); - println!("d_2 x_{source_gen} = {entry:?}"); + 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:?}"); + } } } diff --git a/ext/examples/secondary_product.rs b/ext/examples/secondary_product.rs index 1509824411..de549ecfc5 100644 --- a/ext/examples/secondary_product.rs +++ b/ext/examples/secondary_product.rs @@ -25,111 +25,61 @@ use std::sync::Arc; use algebra::module::Module; use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - resolution_homomorphism::ResolutionHomomorphism, - secondary::*, + ext_algebra::{ExtAlgebra, secondary::SecondaryExtAlgebra}, + secondary::{LAMBDA_BIDEGREE, SecondaryLift}, utils::query_module, }; -use fp::{matrix::Matrix, prime::Prime, vector::FpVector}; -use itertools::Itertools; -use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator}; +use sseq::coordinates::{Bidegree, BidegreeGenerator}; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; let resolution = Arc::new(query_module(Some(algebra::AlgebraType::Milnor), true)?); - - let (is_unit, unit) = ext::utils::get_unit(Arc::clone(&resolution))?; - - let p = resolution.prime(); + let e2 = Arc::new(ExtAlgebra::from_resolution(Arc::clone(&resolution))?); let name: String = query::raw("Name of product", str::parse); - let shift = Bidegree::n_s( query::raw(&format!("n of Ext class {name}"), str::parse), query::raw(&format!("s of Ext class {name}"), str::parse), ); - let hom = ResolutionHomomorphism::new(name, Arc::clone(&resolution), Arc::clone(&unit), shift); - - let mut matrix = Matrix::new(p, hom.source.number_of_gens_in_bidegree(shift), 1); - - if matrix.rows() == 0 || matrix.columns() == 0 { + let dim = e2.dimension(shift); + if dim == 0 { panic!("No classes in this bidegree"); } - let v: Vec = query::vector("Input ext class", matrix.rows()); - for (i, &x) in v.iter().enumerate() { - matrix.row_mut(i).set_entry(0, x); - } + let v: Vec = query::vector("Input ext class", dim); + let x = e2.element(shift, &v); - if !is_unit { + // Ensure the unit is resolved far enough to support the products. + if !e2.is_unit() { let res_max = Bidegree::n_s( resolution.module(0).max_computed_degree(), resolution.next_homological_degree() - 1, ); - unit.compute_through_stem(res_max - shift); + e2.unit().compute_through_stem(res_max - shift); } - hom.extend_step(shift, Some(&matrix)); - hom.extend_all(); + let sec_e2 = Arc::new(SecondaryExtAlgebra::new(Arc::clone(&e2))); + sec_e2.extend_all(); - let res_lift = SecondaryResolution::new(Arc::clone(&resolution)); - res_lift.extend_all(); + // Check that the class survives to E3 (supports no d2). + assert!(sec_e2.survives(&x), "Class supports a non-zero d2"); - // Check that class survives to E3. - { - let m = res_lift.homotopy(shift.s() + 2).homotopies.hom_k(shift.t()); - assert_eq!(m.len(), v.len()); - let mut sum = vec![0; m[0].len()]; - for (x, d2) in v.iter().zip_eq(&m) { - sum.iter_mut().zip_eq(d2).for_each(|(a, b)| *a += x * b); - } - assert!( - sum.iter().all(|x| x.is_multiple_of(p.as_u32())), - "Class supports a non-zero d2" - ); - } - let res_lift = Arc::new(res_lift); - - let unit_lift = if is_unit { - Arc::clone(&res_lift) - } else { - let lift = SecondaryResolution::new(Arc::clone(&unit)); - lift.extend_all(); - Arc::new(lift) - }; - - let hom = Arc::new(hom); - let hom_lift = SecondaryResolutionHomomorphism::new( - Arc::clone(&res_lift), - Arc::clone(&unit_lift), - Arc::clone(&hom), - ); + let lift = sec_e2.secondary_product_lift(&x); if let Some(s) = ext::utils::secondary_job() { - hom_lift.compute_partial(s); + lift.underlying().extend_all(); + lift.compute_partial(s); return Ok(()); } - hom_lift.extend_all(); - - // Compute E3 page - let res_sseq = Arc::new(res_lift.e3_page()); - let unit_sseq = if is_unit { - Arc::clone(&res_sseq) - } else { - Arc::new(unit_lift.e3_page()) - }; + // `x` multiplies on the left; the printed name is bracketed as in the original output. + let disp = format!("[{name}]"); - fn get_page_data(sseq: &sseq::Sseq<2, sseq::Adams>, b: Bidegree) -> &fp::matrix::Subquotient { - let d = sseq.page_data(b); - &d[std::cmp::min(3, d.len() - 1)] - } - - let name = hom_lift.name(); - // Iterate through the multiplicand - for b in unit.iter_nonzero_stem() { - // The potential target has to be hit, and we need to have computed (the data need for) the - // d2 that hits the potential target. + // Iterate through the multiplicand. + for b in e2.unit().iter_nonzero_stem() { + // The potential target has to be hit, and we need to have computed (the data needed for) + // the d2 that hits the potential target. if !resolution.has_computed_bidegree(b + shift + LAMBDA_BIDEGREE) { continue; } @@ -137,48 +87,36 @@ fn main() -> anyhow::Result<()> { continue; } - let page_data = get_page_data(unit_sseq.as_ref(), b); - - let target_num_gens = resolution.number_of_gens_in_bidegree(b + shift); - let lambda_num_gens = resolution.number_of_gens_in_bidegree(b + shift + LAMBDA_BIDEGREE); - + let target_num_gens = e2.dimension(b + shift); + let lambda_num_gens = e2.dimension(b + shift + LAMBDA_BIDEGREE); if target_num_gens == 0 && lambda_num_gens == 0 { continue; } - // First print the products with non-surviving classes - if target_num_gens > 0 { - let hom_k = hom.get_map((b + shift).s()).hom_k(b.t()); - for i in page_data.complement_pivots() { + 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. + if target_num_gens > 0 + && let Some(rows) = e2.multiply_into(&x, b) + { + for i in page.complement_pivots() { let g = BidegreeGenerator::new(b, i); - println!("{name} λ x_{g} = λ {:?}", hom_k[i]); + let entry: Vec = rows.row(i).iter().collect(); + println!("{disp} λ x_{g} = λ {entry:?}"); } } - // Now print the secondary products - if page_data.subspace_dimension() == 0 { - continue; - } - - let mut outputs = vec![ - FpVector::new(p, target_num_gens + lambda_num_gens); - page_data.subspace_dimension() - ]; - - hom_lift.hom_k( - Some(&res_sseq), - b, - page_data.subspace_gens(), - outputs.iter_mut().map(FpVector::as_slice_mut), - ); - for (g, output) in page_data.subspace_gens().zip_eq(outputs) { + // Now the genuinely secondary products with surviving classes. + for prod in sec_e2.secondary_multiply_into(&x, b) { println!( - "{name} [{basis_string}] = {} + λ {}", - output.slice(0, target_num_gens), - output.slice(target_num_gens, target_num_gens + lambda_num_gens), - basis_string = BidegreeElement::new(b, g.to_owned()).to_basis_string(), + "{disp} [{basis}] = {ext} + λ {lambda}", + basis = prod.source.to_basis_string(), + ext = prod.ext_part.as_slice(), + lambda = prod.lambda_part.as_slice(), ); } } + Ok(()) } diff --git a/ext/src/ext_algebra.rs b/ext/src/ext_algebra/mod.rs similarity index 92% rename from ext/src/ext_algebra.rs rename to ext/src/ext_algebra/mod.rs index 344219a257..c0612ed4a7 100644 --- a/ext/src/ext_algebra.rs +++ b/ext/src/ext_algebra/mod.rs @@ -15,6 +15,11 @@ //! 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. +//! +//! The secondary differential ($d_2$) and the $\Mod_{C\lambda^2}$ secondary product live in the +//! [`secondary`] submodule ([`SecondaryExtAlgebra`]). + +pub mod secondary; use std::sync::Arc; @@ -71,6 +76,18 @@ impl ExtAlgebra { } } + /// Build an [`ExtAlgebra`] for resolution-*intrinsic* operations that do not involve products + /// (notably the secondary `d2` differential), using the resolution itself in place of a unit. + /// + /// This avoids the unit-resolution setup (and any associated prompt) that + /// [`from_resolution`](Self::from_resolution) performs. The product methods + /// ([`multiply`](Self::multiply) etc.) and the unit-side queries are only meaningful here when + /// `M == k`; for products with `M != k`, build with [`from_resolution`](Self::from_resolution) + /// or [`new`](Self::new) instead. + pub fn without_unit(resolution: Arc) -> Self { + Self::new(Arc::clone(&resolution), resolution) + } + pub fn resolution(&self) -> &Arc { &self.resolution } diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs new file mode 100644 index 0000000000..a6378a1a13 --- /dev/null +++ b/ext/src/ext_algebra/secondary.rs @@ -0,0 +1,302 @@ +//! The secondary ($d_2$) layer of [`ExtAlgebra`]. +//! +//! [`SecondaryExtAlgebra`] composes an [`ExtAlgebra`] with the secondary resolutions of `M` and +//! the unit `k`, and exposes: +//! - the secondary differential [`d2`](SecondaryExtAlgebra::d2) (and the survival check +//! [`survives`](SecondaryExtAlgebra::survives)), +//! - the $E_3$-page data [`page_data`](SecondaryExtAlgebra::page_data), and +//! - the $\Mod_{C\lambda^2}$ secondary product +//! [`secondary_multiply_into`](SecondaryExtAlgebra::secondary_multiply_into). +//! +//! These wrap [`SecondaryResolution`] and [`SecondaryResolutionHomomorphism`]; no new linear +//! 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 algebra::pair_algebra::PairAlgebra; +use dashmap::DashMap; +use fp::{matrix::Subquotient, prime::Prime, vector::FpVector}; +use sseq::coordinates::{Bidegree, BidegreeElement}; + +use super::ExtAlgebra; +use crate::{ + chain_complex::FreeChainComplex, + resolution_homomorphism::ResolutionHomomorphism, + secondary::{ + LAMBDA_BIDEGREE, SecondaryLift, SecondaryResolution, SecondaryResolutionHomomorphism, + }, +}; + +/// 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 { + /// 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 secondary layer over an [`ExtAlgebra`]: the $d_2$ differential and the $\Mod_{C\lambda^2}$ +/// product. See the [module documentation](self). +pub struct SecondaryExtAlgebra +where + CC::Algebra: PairAlgebra, +{ + alg: Arc>, + res_lift: Arc>, + /// `Arc`-shared with `res_lift` when `M == k`. + unit_lift: Arc>, + /// $E_3$ page of the resolution, filled by [`extend_all`](Self::extend_all). + res_sseq: Mutex>>>, + /// $E_3$ page of the unit, filled by [`extend_all`](Self::extend_all). + unit_sseq: Mutex>>>, + /// Secondary lift of the multiplication map, cached per multiplier class `(degree, coords)`. + secondary_products: DashMap>>, +} + +impl SecondaryExtAlgebra +where + CC::Algebra: PairAlgebra, +{ + /// Build the secondary layer over `alg`. Construction is cheap; call [`extend_all`](Self::extend_all) + /// to actually compute the secondary resolutions and $E_3$ pages. + pub fn new(alg: Arc>) -> Self { + let res_lift = Arc::new(SecondaryResolution::new(Arc::clone(alg.resolution()))); + let unit_lift = if alg.is_unit() { + Arc::clone(&res_lift) + } else { + Arc::new(SecondaryResolution::new(Arc::clone(alg.unit()))) + }; + Self { + alg, + res_lift, + unit_lift, + res_sseq: Mutex::new(None), + unit_sseq: Mutex::new(None), + secondary_products: DashMap::new(), + } + } + + /// Extend the secondary resolutions as far as the underlying resolutions allow, then compute + /// the $E_3$ pages. Must be called before [`d2`](Self::d2), [`page_data`](Self::page_data) or + /// [`secondary_multiply_into`](Self::secondary_multiply_into). + pub fn extend_all(&self) { + self.res_lift.extend_all(); + if !self.alg.is_unit() { + self.unit_lift.extend_all(); + } + + *self.res_sseq.lock().unwrap() = Some(Arc::new(self.res_lift.e3_page())); + let unit = if self.alg.is_unit() { + Arc::clone(self.res_sseq.lock().unwrap().as_ref().unwrap()) + } else { + Arc::new(self.unit_lift.e3_page()) + }; + *self.unit_sseq.lock().unwrap() = Some(unit); + } + + /// Sharding entry point: compute only the secondary resolution data for filtration `s`, + /// distributed across machines sharing a save directory (see the `secondary` example docs). + /// Mirrors [`SecondaryLift::compute_partial`]. Returns before any $E_3$ page is built. + pub fn compute_partial(&self, s: i32) { + self.res_lift.compute_partial(s); + if !self.alg.is_unit() { + self.unit_lift.compute_partial(s); + } + } + + /// The primary [`ExtAlgebra`] this is built on. + pub fn ext_algebra(&self) -> &Arc> { + &self.alg + } + + fn prime(&self) -> fp::prime::ValidPrime { + self.alg.prime() + } + + /// The secondary differential $d_2(x)$, a class in bidegree `(n - 1, s + 2)`. + /// + /// Returns `None` if the target bidegree has not been computed (so $d_2$ is unknown). A + /// computed-but-zero differential is `Some` of a zero class. + pub fn d2(&self, x: &BidegreeElement) -> Option { + let b = x.degree(); + let target = b + Bidegree::n_s(-1, 2); + let res = self.res_lift.underlying(); + if !(b.t() > 0 && res.has_computed_bidegree(target)) { + return None; + } + + let target_dim = res.number_of_gens_in_bidegree(target); + let mut out = FpVector::new(self.prime(), target_dim); + + // `m[i]` is the d2 of the i-th generator of `b`, as a vector at `target`. This is exactly + // the matrix `SecondaryResolution::e3_page` reads to install d2 differentials. + let m = self.res_lift.homotopy(b.s() + 2).homotopies.hom_k(b.t()); + if !m.is_empty() && !m[0].is_empty() { + let p = self.prime().as_u32(); + for (i, c) in x.vec().iter_nonzero() { + for (k, &v) in m[i].iter().enumerate() { + out.add_basis_element(k, (c * v) % p); + } + } + } + Some(BidegreeElement::new(target, out)) + } + + /// Whether `x` is a $d_2$-cycle (a permanent class through $E_3$). Treats an uncomputed $d_2$ + /// target as "survives" (there is nothing for it to hit). + pub fn survives(&self, x: &BidegreeElement) -> bool { + self.d2(x).is_none_or(|d| d.vec().is_zero()) + } + + /// The $E_3$-page subquotient of $\Ext(M, k)$ at bidegree `b`. + pub fn page_data(&self, b: Bidegree) -> Subquotient { + let g = self.res_sseq.lock().unwrap(); + Self::e3_page_data(g.as_ref().expect("call extend_all() first"), b).clone() + } + + /// The $E_3$-page subquotient of the unit $\Ext(k, k)$ at bidegree `b`. + pub fn unit_page_data(&self, b: Bidegree) -> Subquotient { + let g = self.unit_sseq.lock().unwrap(); + Self::e3_page_data(g.as_ref().expect("call extend_all() first"), b).clone() + } + + 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)] + } +} + +impl SecondaryExtAlgebra +where + CC::Algebra: PairAlgebra, +{ + /// The secondary lift of multiplication by `x`, built and cached per multiplier class. The + /// returned lift is *not* extended; [`secondary_multiply_into`](Self::secondary_multiply_into) + /// extends it as needed. Exposed so callers can drive sharded computation + /// (`lift.underlying().extend_all()` then `lift.compute_partial(s)`). + pub fn secondary_product_lift( + &self, + x: &BidegreeElement, + ) -> Arc> { + if let Some(map) = self.secondary_products.get(x) { + return Arc::clone(&map); + } + + let name = format!("prod_{x}",); + let underlying = Arc::new(ResolutionHomomorphism::from_class( + name, + Arc::clone(self.alg.resolution()), + Arc::clone(self.alg.unit()), + x.degree(), + &x.vec().iter().collect::>(), + )); + let lift = Arc::new(SecondaryResolutionHomomorphism::new( + Arc::clone(&self.res_lift), + Arc::clone(&self.unit_lift), + underlying, + )); + + Arc::clone( + self.secondary_products + .entry(x.clone()) + .or_insert(lift) + .value(), + ) + } + + /// The secondary product of `x` with every $E_3$-surviving class of the unit at bidegree `b`, + /// computed in $\Mod_{C\lambda^2}$. + /// + /// Returns one [`SecondaryProduct`] per surviving generator at `b`; the $\lambda$ part is + /// already reduced by the image of $d_2$. The caller must have run [`extend_all`](Self::extend_all) + /// and computed both resolutions far enough. + pub fn secondary_multiply_into( + &self, + x: &BidegreeElement, + b: Bidegree, + ) -> Vec { + let p = self.prime(); + let shift = x.degree(); + let res_sseq = Arc::clone( + self.res_sseq + .lock() + .unwrap() + .as_ref() + .expect("call extend_all() first"), + ); + + let ext_dim = self.alg.resolution().number_of_gens_in_bidegree(b + shift); + let lambda_dim = self + .alg + .resolution() + .number_of_gens_in_bidegree(b + shift + LAMBDA_BIDEGREE); + + let page = self.unit_page_data(b); + let n = page.subspace_dimension(); + if n == 0 { + return Vec::new(); + } + + let lift = self.secondary_product_lift(x); + lift.underlying().extend_all(); + lift.extend_all(); + + let mut outputs = vec![FpVector::new(p, ext_dim + lambda_dim); n]; + lift.hom_k( + Some(&res_sseq), + b, + page.subspace_gens(), + outputs.iter_mut().map(FpVector::as_slice_mut), + ); + + page.subspace_gens() + .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(), + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use sseq::coordinates::BidegreeGenerator; + + use super::*; + use crate::utils::construct_standard; + + #[test] + fn test_sphere_d2() { + let res = Arc::new(construct_standard::("S_2", None).unwrap()); + // Far enough to reach the first Adams differential d2(h4) = h0 h3^2 at (14, 3). + 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(); + + // h_0, h_1, h_2 are permanent cycles. + for (n, s) in [(0, 1), (1, 1), (3, 1)] { + let h = e2.generator(BidegreeGenerator::new(Bidegree::n_s(n, s), 0)); + assert!(sec_e2.survives(&h), "h at (n={n}, s={s}) should survive d2"); + assert!( + sec_e2.d2(&h).is_none_or(|d| d.vec().is_zero()), + "d2 of a permanent class should vanish" + ); + } + + // The first Adams differential: d2(h4) = h0 h3^2, the generator of Ext^{3,17} at (14, 3). + let h4 = e2.generator(BidegreeGenerator::new(Bidegree::n_s(15, 1), 0)); + let d = sec_e2.d2(&h4).expect("d2(h4) target should be computed"); + assert_eq!(d.degree(), Bidegree::n_s(14, 3)); + assert_eq!(e2.dimension(Bidegree::n_s(14, 3)), 1); + assert!(!d.vec().is_zero(), "d2(h4) = h0 h3^2 should be nonzero"); + assert!(!sec_e2.survives(&h4), "h4 should not survive d2"); + } +} From 5d17388bad9f619dc86e372cf8195d1c764c8809 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer <57161378+JoeyBF@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:21:05 -0400 Subject: [PATCH 2/3] Re-export `SecondaryExtAlgebra` Co-authored-by: Hood Chatham --- ext/examples/secondary_product.rs | 2 +- ext/src/ext_algebra/mod.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/examples/secondary_product.rs b/ext/examples/secondary_product.rs index de549ecfc5..dc3a2ebd30 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, secondary::SecondaryExtAlgebra}, + ext_algebra::{ExtAlgebra, SecondaryExtAlgebra}, secondary::{LAMBDA_BIDEGREE, SecondaryLift}, utils::query_module, }; diff --git a/ext/src/ext_algebra/mod.rs b/ext/src/ext_algebra/mod.rs index c0612ed4a7..a0200a28fd 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -27,6 +27,7 @@ use dashmap::DashMap; use fp::{matrix::Matrix, prime::ValidPrime, vector::FpVector}; use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator}; +pub use self::secondary::{SecondaryExtAlgebra, SecondaryProduct}; use crate::{ chain_complex::{AugmentedChainComplex, FreeChainComplex}, resolution_homomorphism::ResolutionHomomorphism, From 9d473571b2fb8280dc33846f07441233fa5e4d30 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer Date: Thu, 25 Jun 2026 13:57:45 -0400 Subject: [PATCH 3/3] Treat uncomputed targets as undefined, not 0 --- ext/examples/secondary_product.rs | 5 ++++- ext/src/ext_algebra/secondary.rs | 20 +++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ext/examples/secondary_product.rs b/ext/examples/secondary_product.rs index dc3a2ebd30..b024a8ce2e 100644 --- a/ext/examples/secondary_product.rs +++ b/ext/examples/secondary_product.rs @@ -63,7 +63,10 @@ fn main() -> anyhow::Result<()> { sec_e2.extend_all(); // Check that the class survives to E3 (supports no d2). - assert!(sec_e2.survives(&x), "Class supports a non-zero d2"); + let x_survives = sec_e2 + .survives(&x) + .expect("Class differential is not computed"); + assert!(x_survives, "Class supports a non-zero d2"); let lift = sec_e2.secondary_product_lift(&x); diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index a6378a1a13..983a7c60ac 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -147,10 +147,9 @@ where Some(BidegreeElement::new(target, out)) } - /// Whether `x` is a $d_2$-cycle (a permanent class through $E_3$). Treats an uncomputed $d_2$ - /// target as "survives" (there is nothing for it to hit). - pub fn survives(&self, x: &BidegreeElement) -> bool { - self.d2(x).is_none_or(|d| d.vec().is_zero()) + /// Whether `x` is a $d_2$-cycle (a permanent class through $E_3$). + pub fn survives(&self, x: &BidegreeElement) -> Option { + self.d2(x).map(|d| d.vec().is_zero()) } /// The $E_3$-page subquotient of $\Ext(M, k)$ at bidegree `b`. @@ -284,9 +283,15 @@ mod tests { // h_0, h_1, h_2 are permanent cycles. for (n, s) in [(0, 1), (1, 1), (3, 1)] { let h = e2.generator(BidegreeGenerator::new(Bidegree::n_s(n, s), 0)); - assert!(sec_e2.survives(&h), "h at (n={n}, s={s}) should survive d2"); + let h_survives = sec_e2 + .survives(&h) + .unwrap_or_else(|| panic!("h at (n={n}, s={s}) should have a computed d2")); + assert!(h_survives, "h at (n={n}, s={s}) should survive d2"); + let h_d2 = sec_e2 + .d2(&h) + .unwrap_or_else(|| panic!("h at (n={n}, s={s}) should have a computed d2")); assert!( - sec_e2.d2(&h).is_none_or(|d| d.vec().is_zero()), + h_d2.vec().is_zero(), "d2 of a permanent class should vanish" ); } @@ -297,6 +302,7 @@ mod tests { assert_eq!(d.degree(), Bidegree::n_s(14, 3)); assert_eq!(e2.dimension(Bidegree::n_s(14, 3)), 1); assert!(!d.vec().is_zero(), "d2(h4) = h0 h3^2 should be nonzero"); - assert!(!sec_e2.survives(&h4), "h4 should not survive d2"); + let h4_survives = sec_e2.survives(&h4).expect("h4 should have a computed d2"); + assert!(!h4_survives, "h4 should not survive d2"); } }