diff --git a/ext/crates/algebra/Cargo.toml b/ext/crates/algebra/Cargo.toml index 3b98825cb5..531abee625 100644 --- a/ext/crates/algebra/Cargo.toml +++ b/ext/crates/algebra/Cargo.toml @@ -15,6 +15,7 @@ bivec = { path = "../bivec" } fp = { path = "../fp", default-features = false } maybe-rayon = { path = "../maybe-rayon" } once = { path = "../once" } +sseq = { path = "../sseq", default-features = false } anyhow = "1.0.98" auto_impl = "1.3.0" @@ -37,8 +38,8 @@ rstest = "0.25.0" [features] default = ["odd-primes"] cache-multiplication = [] -concurrent = ["fp/concurrent", "maybe-rayon/concurrent"] -odd-primes = ["fp/odd-primes"] +concurrent = ["fp/concurrent", "maybe-rayon/concurrent", "sseq/concurrent"] +odd-primes = ["fp/odd-primes", "sseq/odd-primes"] [[bench]] name = "milnor" diff --git a/ext/crates/algebra/benches/common/mod.rs b/ext/crates/algebra/benches/common/mod.rs index 84e93bfb7c..4475229146 100644 --- a/ext/crates/algebra/benches/common/mod.rs +++ b/ext/crates/algebra/benches/common/mod.rs @@ -15,7 +15,7 @@ use std::{hint::black_box, path::PathBuf, sync::Arc}; use algebra::{ Algebra, AlgebraType, SteenrodAlgebra, - module::{Module, SteenrodModule, steenrod_module}, + module::{Module, ModuleExt, SteenrodModule, steenrod_module}, }; use criterion::{BenchmarkGroup, Throughput, measurement::WallTime}; use fp::{ diff --git a/ext/crates/algebra/src/algebra/adem_algebra.rs b/ext/crates/algebra/src/algebra/adem_algebra.rs index 100f42a506..37098be91c 100644 --- a/ext/crates/algebra/src/algebra/adem_algebra.rs +++ b/ext/crates/algebra/src/algebra/adem_algebra.rs @@ -12,6 +12,7 @@ use fp::{ use itertools::Itertools; use once::OnceVec; use rustc_hash::FxHashMap as HashMap; +use sseq::coordinates::MultiDegree; #[cfg(doc)] use crate::algebra::SteenrodAlgebra; @@ -222,7 +223,8 @@ impl Algebra for AdemAlgebra { .collect() } - fn compute_basis(&self, max_degree: i32) { + fn compute_basis(&self, max_degree: impl Into>) { + let max_degree = i32::from(max_degree.into()); if self.generic { self.generate_basis_generic(max_degree); self.generate_basis_element_to_index_map(max_degree); @@ -238,7 +240,8 @@ impl Algebra for AdemAlgebra { } } - fn dimension(&self, degree: i32) -> usize { + fn dimension(&self, degree: impl Into>) -> usize { + let degree = i32::from(degree.into()); if degree < 0 { 0 } else { @@ -250,24 +253,27 @@ impl Algebra for AdemAlgebra { &self, result: FpSliceMut, coeff: u32, - r_degree: i32, + r_degree: impl Into>, r_index: usize, - s_degree: i32, + s_degree: impl Into>, s_index: usize, ) { self.multiply_inner( result, coeff, - r_degree, + i32::from(r_degree.into()), r_index, - s_degree, + i32::from(s_degree.into()), s_index, i32::MAX, ); } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { - format!("{}", self.basis_element_from_index(degree, idx)) + fn basis_element_to_string(&self, degree: impl Into>, idx: usize) -> String { + format!( + "{}", + self.basis_element_from_index(i32::from(degree.into()), idx) + ) } fn basis_element_from_string(&self, mut elt: &str) -> Option<(i32, usize)> { diff --git a/ext/crates/algebra/src/algebra/algebra_trait.rs b/ext/crates/algebra/src/algebra/algebra_trait.rs index cad1ac0f5f..0812c8f411 100644 --- a/ext/crates/algebra/src/algebra/algebra_trait.rs +++ b/ext/crates/algebra/src/algebra/algebra_trait.rs @@ -6,6 +6,7 @@ use fp::{ vector::{FpSlice, FpSliceMut}, }; use itertools::Itertools; +use sseq::coordinates::MultiDegree; /// A graded algebra over $\mathbb{F}_p$. /// @@ -20,8 +21,16 @@ use itertools::Itertools; /// this function before performing other operations at that degree. /// /// Algebras may have a distinguished set of generators; see [`GeneratedAlgebra`]. -#[enum_dispatch] -pub trait Algebra: std::fmt::Display + Send + Sync + 'static { +/// +/// # Grading +/// The trait is generic over the number of gradings `N` (defaulting to `1`, the singly-graded +/// case). Degrees are [`MultiDegree`]; every degree *input* is taken as `impl Into>` +/// so callers in the singly-graded world keep passing bare `i32`s (via `From for +/// MultiDegree<1>`). `MultiDegree<2>` is a bidegree, used by `Ext` (see `ext::ext_algebra`). +/// +/// `Algebra` cannot use `#[enum_dispatch]` because it is now generic; `SteenrodAlgebra` dispatches +/// it by hand (see `dispatch_steenrod!`). +pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// A name for the algebra to use in serialization operations. This defaults to "" for algebras /// that don't care about this problem. fn prefix(&self) -> &str { @@ -49,10 +58,10 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// /// This function must be idempotent and cheap to call again with the /// same argument. - fn compute_basis(&self, degree: i32); + fn compute_basis(&self, degree: impl Into>); /// Returns the dimension of the algebra in degree `degree`. - fn dimension(&self, degree: i32) -> usize; + fn dimension(&self, degree: impl Into>) -> usize; /// Computes the product `r * s` of two basis elements, and adds the /// result to `result`. @@ -62,9 +71,9 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { &self, result: FpSliceMut, coeff: u32, - r_degree: i32, + r_degree: impl Into>, r_idx: usize, - s_degree: i32, + s_degree: impl Into>, s_idx: usize, ); @@ -76,12 +85,14 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { &self, mut result: FpSliceMut, coeff: u32, - r_degree: i32, + r_degree: impl Into>, r_idx: usize, - s_degree: i32, + s_degree: impl Into>, s: FpSlice, ) { let p = self.prime(); + let r_degree = r_degree.into(); + let s_degree = s_degree.into(); for (i, v) in s.iter_nonzero() { self.multiply_basis_elements( result.copy(), @@ -102,12 +113,14 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { &self, mut result: FpSliceMut, coeff: u32, - r_degree: i32, + r_degree: impl Into>, r: FpSlice, - s_degree: i32, + s_degree: impl Into>, s_idx: usize, ) { let p = self.prime(); + let r_degree = r_degree.into(); + let s_degree = s_degree.into(); for (i, v) in r.iter_nonzero() { self.multiply_basis_elements( result.copy(), @@ -128,12 +141,14 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { &self, mut result: FpSliceMut, coeff: u32, - r_degree: i32, + r_degree: impl Into>, r: FpSlice, - s_degree: i32, + s_degree: impl Into>, s: FpSlice, ) { let p = self.prime(); + let r_degree = r_degree.into(); + let s_degree = s_degree.into(); for (i, v) in s.iter_nonzero() { self.multiply_element_by_basis_element( result.copy(), @@ -158,7 +173,7 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { } /// Converts a basis element into a string for display. - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String; + fn basis_element_to_string(&self, degree: impl Into>, idx: usize) -> String; /// Non-panicking variant of [`Self::basis_element_to_string`]. Returns `None` /// when `degree` is negative or `idx` is out of range for that degree, instead @@ -167,8 +182,13 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// These are the usual failure conditions. An implementation of /// [`Self::basis_element_to_string`] that can fail for other reasons should override both /// that method and this one, keeping them consistent. - fn try_basis_element_to_string(&self, degree: i32, idx: usize) -> Option { - if degree < 0 { + fn try_basis_element_to_string( + &self, + degree: impl Into>, + idx: usize, + ) -> Option { + let degree = degree.into(); + if degree.t() < 0 { return None; } self.compute_basis(degree); @@ -187,7 +207,8 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { fn basis_element_from_string(&self, elt: &str) -> Option<(i32, usize)>; /// Converts a general element into a string for display. - fn element_to_string(&self, degree: i32, element: FpSlice) -> String { + fn element_to_string(&self, degree: impl Into>, element: FpSlice) -> String { + let degree = degree.into(); let result = element .iter_nonzero() .map(|(idx, value)| { diff --git a/ext/crates/algebra/src/algebra/field.rs b/ext/crates/algebra/src/algebra/field.rs index 6f18ebfe4a..6e9e8c2e3e 100644 --- a/ext/crates/algebra/src/algebra/field.rs +++ b/ext/crates/algebra/src/algebra/field.rs @@ -4,6 +4,7 @@ use fp::{ prime::ValidPrime, vector::{FpSlice, FpSliceMut}, }; +use sseq::coordinates::MultiDegree; use crate::algebra::{Algebra, Bialgebra}; @@ -34,19 +35,19 @@ impl Algebra for Field { self.prime } - fn compute_basis(&self, _degree: i32) {} + fn compute_basis(&self, _degree: impl Into>) {} - fn dimension(&self, degree: i32) -> usize { - usize::from(degree == 0) + fn dimension(&self, degree: impl Into>) -> usize { + usize::from(i32::from(degree.into()) == 0) } fn multiply_basis_elements( &self, mut result: FpSliceMut, coeff: u32, - _r_degree: i32, + _r_degree: impl Into>, _r_idx: usize, - _s_degree: i32, + _s_degree: impl Into>, _s_idx: usize, ) { result.add_basis_element(0, coeff) @@ -56,13 +57,13 @@ impl Algebra for Field { vec![] } - fn basis_element_to_string(&self, degree: i32, _idx: usize) -> String { - assert!(degree == 0); + fn basis_element_to_string(&self, degree: impl Into>, _idx: usize) -> String { + assert!(i32::from(degree.into()) == 0); "1".to_string() } - fn element_to_string(&self, degree: i32, element: FpSlice) -> String { - assert!(degree == 0); + fn element_to_string(&self, degree: impl Into>, element: FpSlice) -> String { + assert!(i32::from(degree.into()) == 0); format!("{}", element.entry(0)) } diff --git a/ext/crates/algebra/src/algebra/milnor_algebra.rs b/ext/crates/algebra/src/algebra/milnor_algebra.rs index 07b3bd35f8..ab24f9d2a3 100644 --- a/ext/crates/algebra/src/algebra/milnor_algebra.rs +++ b/ext/crates/algebra/src/algebra/milnor_algebra.rs @@ -8,6 +8,7 @@ use itertools::Itertools; use once::OnceVec; use rustc_hash::FxHashMap as HashMap; use serde::{Deserialize, Serialize}; +use sseq::coordinates::MultiDegree; use crate::algebra::{Algebra, Bialgebra, GeneratedAlgebra, UnstableAlgebra, combinatorics}; @@ -416,7 +417,8 @@ impl Algebra for MilnorAlgebra { .collect() } - fn compute_basis(&self, max_degree: i32) { + fn compute_basis(&self, max_degree: impl Into>) { + let max_degree = i32::from(max_degree.into()); self.compute_ppart(max_degree); if self.generic() { @@ -470,7 +472,8 @@ impl Algebra for MilnorAlgebra { } } - fn dimension(&self, degree: i32) -> usize { + fn dimension(&self, degree: impl Into>) -> usize { + let degree = i32::from(degree.into()); if degree < 0 { return 0; } @@ -482,16 +485,16 @@ impl Algebra for MilnorAlgebra { &self, result: FpSliceMut, coef: u32, - r_degree: i32, + r_degree: impl Into>, r_idx: usize, - s_degree: i32, + s_degree: impl Into>, s_idx: usize, ) { self.multiply( result, coef, - self.basis_element_from_index(r_degree, r_idx), - self.basis_element_from_index(s_degree, s_idx), + self.basis_element_from_index(i32::from(r_degree.into()), r_idx), + self.basis_element_from_index(i32::from(s_degree.into()), s_idx), ); } @@ -500,11 +503,13 @@ impl Algebra for MilnorAlgebra { &self, mut result: FpSliceMut, coef: u32, - r_degree: i32, + r_degree: impl Into>, r_idx: usize, - s_degree: i32, + s_degree: impl Into>, s_idx: usize, ) { + let r_degree = i32::from(r_degree.into()); + let s_degree = i32::from(s_degree.into()); result.add( self.multiplication_table[r_degree as usize][s_degree as usize][r_idx][s_idx] .as_slice(), @@ -516,12 +521,14 @@ impl Algebra for MilnorAlgebra { &self, mut result: FpSliceMut, coeff: u32, - r_degree: i32, + r_degree: impl Into>, r_idx: usize, - s_degree: i32, + s_degree: impl Into>, s: FpSlice, ) { let p = self.prime(); + let r_degree = i32::from(r_degree.into()); + let s_degree = i32::from(s_degree.into()); let r = self.basis_element_from_index(r_degree, r_idx); PPartAllocation::with_local(|mut allocation| { for (i, v) in s.iter_nonzero() { @@ -542,11 +549,13 @@ impl Algebra for MilnorAlgebra { &self, mut res: FpSliceMut, coef: u32, - r_deg: i32, + r_deg: impl Into>, r: FpSlice, - s_deg: i32, + s_deg: impl Into>, s: FpSlice, ) { + let r_deg = i32::from(r_deg.into()); + let s_deg = i32::from(s_deg.into()); PPartAllocation::with_local(|mut allocation| { for (i, c) in r.iter_nonzero() { allocation = self.multiply_basis_by_element_with_allocation( @@ -562,8 +571,11 @@ impl Algebra for MilnorAlgebra { }) } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { - format!("{}", self.basis_element_from_index(degree, idx)) + fn basis_element_to_string(&self, degree: impl Into>, idx: usize) -> String { + format!( + "{}", + self.basis_element_from_index(i32::from(degree.into()), idx) + ) } fn basis_element_from_string(&self, elt: &str) -> Option<(i32, usize)> { diff --git a/ext/crates/algebra/src/algebra/steenrod_algebra.rs b/ext/crates/algebra/src/algebra/steenrod_algebra.rs index 04c65a7e4e..0a502dfec8 100644 --- a/ext/crates/algebra/src/algebra/steenrod_algebra.rs +++ b/ext/crates/algebra/src/algebra/steenrod_algebra.rs @@ -7,6 +7,7 @@ use fp::{ }; use serde::Deserialize; use serde_json::Value; +use sseq::coordinates::MultiDegree; use crate::{ algebra::{AdemAlgebra, Algebra, Bialgebra, GeneratedAlgebra, MilnorAlgebra, UnstableAlgebra}, @@ -53,7 +54,10 @@ impl std::str::FromStr for AlgebraType { } #[allow(clippy::large_enum_variant)] -#[enum_dispatch::enum_dispatch(Algebra, Bialgebra, GeneratedAlgebra, UnstableAlgebra)] +// `Algebra` is now generic (`Algebra`), which `enum_dispatch` cannot handle, so it is +// dispatched by hand below via `dispatch_steenrod!`. The remaining (non-generic) traits still use +// `enum_dispatch`. +#[enum_dispatch::enum_dispatch(Bialgebra, GeneratedAlgebra, UnstableAlgebra)] pub enum SteenrodAlgebra { AdemAlgebra(AdemAlgebra), MilnorAlgebra(MilnorAlgebra), @@ -142,6 +146,22 @@ macro_rules! dispatch_steenrod { }; } +impl Algebra for SteenrodAlgebra { + dispatch_steenrod! { + fn prime(&self) -> ValidPrime; + fn prefix(&self) -> &str; + fn magic(&self) -> u32; + fn compute_basis(&self, degree: impl Into>); + fn dimension(&self, degree: impl Into>) -> usize; + fn multiply_basis_elements(&self, result: FpSliceMut, coeff: u32, r_degree: impl Into>, r_idx: usize, s_degree: impl Into>, s_idx: usize); + fn multiply_basis_element_by_element(&self, result: FpSliceMut, coeff: u32, r_degree: impl Into>, r_idx: usize, s_degree: impl Into>, s: FpSlice); + fn multiply_element_by_element(&self, result: FpSliceMut, coeff: u32, r_degree: impl Into>, r: FpSlice, s_degree: impl Into>, s: FpSlice); + fn default_filtration_one_products(&self) -> Vec<(String, i32, usize)>; + fn basis_element_to_string(&self, degree: impl Into>, idx: usize) -> String; + fn basis_element_from_string(&self, elt: &str) -> Option<(i32, usize)>; + } +} + impl PairAlgebra for AdemAlgebra { type Element = crate::pair_algebra::MilnorPairElement; diff --git a/ext/crates/algebra/src/module/finite_dimensional_module.rs b/ext/crates/algebra/src/module/finite_dimensional_module.rs index 58fed33b6f..84fae59999 100644 --- a/ext/crates/algebra/src/module/finite_dimensional_module.rs +++ b/ext/crates/algebra/src/module/finite_dimensional_module.rs @@ -5,10 +5,11 @@ use bivec::BiVec; use fp::vector::{FpSliceMut, FpVector}; use serde::Deserialize; use serde_json::{json, value::Value}; +use sseq::coordinates::MultiDegree; use crate::{ algebra::{Algebra, GeneratedAlgebra}, - module::{Module, ModuleFailedRelationError, ZeroModule}, + module::{Module, ModuleExt, ModuleFailedRelationError, ZeroModule}, }; pub struct FiniteDimensionalModule { @@ -137,9 +138,10 @@ impl Module for FiniteDimensionalModule { i32::MAX } - fn compute_basis(&self, _degree: i32) {} + fn compute_basis_multi(&self, _degree: MultiDegree<1>) {} - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); if degree < self.graded_dimension.min_degree() { return 0; } @@ -149,19 +151,22 @@ impl Module for FiniteDimensionalModule { self.graded_dimension[degree] } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); self.gen_names[degree][idx].clone() } - fn act_on_basis( + fn act_on_basis_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); assert!(op_index < self.algebra().dimension(op_degree)); assert!(mod_index < self.dimension(mod_degree)); let output_dimension = self.dimension(mod_degree + op_degree); diff --git a/ext/crates/algebra/src/module/finitely_presented_module.rs b/ext/crates/algebra/src/module/finitely_presented_module.rs index d8af3d11e1..e61cf7d0c9 100644 --- a/ext/crates/algebra/src/module/finitely_presented_module.rs +++ b/ext/crates/algebra/src/module/finitely_presented_module.rs @@ -4,11 +4,12 @@ use fp::vector::{FpSliceMut, FpVector}; use itertools::Itertools; use once::OnceBiVec; use serde_json::Value; +use sseq::coordinates::MultiDegree; use crate::{ algebra::Algebra, module::{ - FreeModule, Module, ZeroModule, + FreeModule, Module, ModuleExt, ZeroModule, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, }, }; @@ -185,7 +186,8 @@ impl Module for FinitelyPresentedModule { self.generators.max_computed_degree() } - fn compute_basis(&self, degree: i32) { + fn compute_basis_multi(&self, degree: MultiDegree<1>) { + let degree = i32::from(degree); self.generators.extend_by_zero(degree); self.relations.extend_by_zero(degree); self.map.compute_auxiliary_data_through_degree(degree); @@ -209,20 +211,23 @@ impl Module for FinitelyPresentedModule { }); } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); assert!(degree >= self.min_degree); self.index_table[degree].fp_idx_to_gen_idx.len() } - fn act_on_basis( + fn act_on_basis_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); let p = self.prime(); let gen_idx = self.fp_idx_to_gen_idx(mod_degree, mod_index); let out_deg = mod_degree + op_degree; @@ -244,7 +249,8 @@ impl Module for FinitelyPresentedModule { } } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); let gen_idx = self.fp_idx_to_gen_idx(degree, idx); self.generators.basis_element_to_string(degree, gen_idx) } diff --git a/ext/crates/algebra/src/module/free_module.rs b/ext/crates/algebra/src/module/free_module.rs index 45af27b2c8..4f71719f8b 100644 --- a/ext/crates/algebra/src/module/free_module.rs +++ b/ext/crates/algebra/src/module/free_module.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use fp::vector::{FpSlice, FpSliceMut}; use once::{OnceBiVec, OnceVec}; +use sseq::coordinates::MultiDegree; use crate::{ algebra::MuAlgebra, - module::{Module, ZeroModule}, + module::{Module, ModuleExt, ZeroModule}, }; #[derive(Clone, Debug)] @@ -83,7 +84,8 @@ impl> Module for MuFreeModule { Some(self.min_degree) } - fn compute_basis(&self, max_degree: i32) { + fn compute_basis_multi(&self, max_degree: MultiDegree<1>) { + let max_degree = i32::from(max_degree); let algebra = self.algebra(); self.basis_element_to_opgen.extend(max_degree, |degree| { let new_row = OnceVec::new(); @@ -110,7 +112,8 @@ impl> Module for MuFreeModule { }); } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); if degree < self.min_degree { return 0; } @@ -121,7 +124,8 @@ impl> Module for MuFreeModule { self.basis_element_to_opgen[degree].len() } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); let opgen = self.index_to_op_gen(degree, idx); let mut op_str = self .algebra() @@ -137,15 +141,17 @@ impl> Module for MuFreeModule { ) } - fn act_on_basis( + fn act_on_basis_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); let OperationGeneratorPair { operation_degree: module_operation_degree, operation_index: module_operation_index, @@ -177,15 +183,17 @@ impl> Module for MuFreeModule { ); } - fn act( + fn act_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - input_degree: i32, + input_degree: MultiDegree<1>, input: FpSlice, ) { + let op_degree = i32::from(op_degree); + let input_degree = i32::from(input_degree); for GeneratorData { gen_deg, start: [input_start, output_start], diff --git a/ext/crates/algebra/src/module/hom_module.rs b/ext/crates/algebra/src/module/hom_module.rs index e192a92143..8bd74f1436 100644 --- a/ext/crates/algebra/src/module/hom_module.rs +++ b/ext/crates/algebra/src/module/hom_module.rs @@ -3,10 +3,11 @@ use std::sync::Arc; use bivec::BiVec; use fp::vector::FpSliceMut; use once::OnceBiVec; +use sseq::coordinates::MultiDegree; use crate::{ algebra::Field, - module::{FreeModule, Module, block_structure::BlockStructure}, + module::{FreeModule, Module, ModuleExt, block_structure::BlockStructure}, }; /// Given a module N and a free module M, this is the module Hom(M, N) as a module over the ground @@ -76,7 +77,8 @@ impl Module for HomModule { self.source.max_computed_degree() - self.target.max_degree().unwrap() } - fn compute_basis(&self, degree: i32) { + fn compute_basis_multi(&self, degree: MultiDegree<1>) { + let degree = i32::from(degree); self.source .compute_basis(degree + self.target.max_degree().unwrap()); self.block_structures.extend(degree, |d| { @@ -95,25 +97,28 @@ impl Module for HomModule { }); } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); self.block_structures[degree].total_dimension() } - fn act_on_basis( + fn act_on_basis_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - _mod_degree: i32, + _mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); assert_eq!(op_degree, 0); assert_eq!(op_index, 0); result.add_basis_element(mod_index, coeff); } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); let gen_basis_elt = self.block_structures[degree].index_to_generator_basis_elt(idx); let gen_deg = gen_basis_elt.generator_degree; let gen_idx = gen_basis_elt.generator_index; diff --git a/ext/crates/algebra/src/module/homomorphism/free_module_homomorphism.rs b/ext/crates/algebra/src/module/homomorphism/free_module_homomorphism.rs index cee0d6ef8b..bafec13f3f 100644 --- a/ext/crates/algebra/src/module/homomorphism/free_module_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/free_module_homomorphism.rs @@ -9,7 +9,7 @@ use once::OnceBiVec; use crate::{ algebra::MuAlgebra, module::{ - Module, MuFreeModule, + Module, ModuleExt, MuFreeModule, free_module::OperationGeneratorPair, homomorphism::{ModuleHomomorphism, ZeroHomomorphism}, }, diff --git a/ext/crates/algebra/src/module/homomorphism/full_module_homomorphism.rs b/ext/crates/algebra/src/module/homomorphism/full_module_homomorphism.rs index 424e45809f..ae236e3e19 100644 --- a/ext/crates/algebra/src/module/homomorphism/full_module_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/full_module_homomorphism.rs @@ -10,7 +10,7 @@ use once::OnceBiVec; use crate::{ algebra::Algebra, module::{ - Module, + Module, ModuleExt, homomorphism::{IdentityHomomorphism, ModuleHomomorphism, ZeroHomomorphism}, }, }; @@ -145,11 +145,11 @@ where for target_deg in min_degree..=max_degree { let source_deg = target_deg + degree_shift; - // Here we use `Module::dimension(&*m, i)` instead of `m.dimension(i)` because there are + // Here we use `ModuleExt::dimension(&*m, i)` instead of `m.dimension(i)` because there are // multiple `dimension` methods in scope and rust-analyzer gets confused if we're not // explicit enough. - let source_dim = Module::dimension(&*source, source_deg); - let target_dim = Module::dimension(&*target, target_deg); + let source_dim = ModuleExt::dimension(&*source, source_deg); + let target_dim = ModuleExt::dimension(&*target, target_deg); let mut matrix = Matrix::new(p, source_dim, target_dim); f.get_matrix(matrix.as_slice_mut(), source_deg); diff --git a/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs b/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs index 197050fb24..269124d324 100644 --- a/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs +++ b/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs @@ -7,7 +7,7 @@ use fp::{ use once::OnceBiVec; use crate::module::{ - FreeModule, HomModule, Module, + FreeModule, HomModule, Module, ModuleExt, block_structure::GeneratorBasisEltPair, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, }; diff --git a/ext/crates/algebra/src/module/homomorphism/mod.rs b/ext/crates/algebra/src/module/homomorphism/mod.rs index 540b34ac4b..4fef229492 100644 --- a/ext/crates/algebra/src/module/homomorphism/mod.rs +++ b/ext/crates/algebra/src/module/homomorphism/mod.rs @@ -6,7 +6,7 @@ use fp::{ vector::{FpSlice, FpSliceMut}, }; -use crate::module::Module; +use crate::module::{Module, ModuleExt}; mod free_module_homomorphism; mod full_module_homomorphism; diff --git a/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs b/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs index e815b17304..233aa37207 100644 --- a/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use fp::vector::{FpSliceMut, FpVector}; -use crate::module::{Module, QuotientModule, homomorphism::ModuleHomomorphism}; +use crate::module::{ModuleExt, QuotientModule, homomorphism::ModuleHomomorphism}; pub struct QuotientHomomorphism { f: Arc, diff --git a/ext/crates/algebra/src/module/mod.rs b/ext/crates/algebra/src/module/mod.rs index 2bd760fdc6..b8fa9a301d 100644 --- a/ext/crates/algebra/src/module/mod.rs +++ b/ext/crates/algebra/src/module/mod.rs @@ -21,7 +21,7 @@ pub use free_module::{ FreeModule, GeneratorData, MuFreeModule, OperationGeneratorPair, UnstableFreeModule, }; pub use hom_module::HomModule; -pub use module_trait::{ActError, Module, ModuleFailedRelationError}; +pub use module_trait::{ActError, Module, ModuleExt, ModuleFailedRelationError}; pub use quotient_module::QuotientModule; pub use rpn::RealProjectiveSpace; pub use steenrod_module::SteenrodModule; diff --git a/ext/crates/algebra/src/module/module_trait.rs b/ext/crates/algebra/src/module/module_trait.rs index 9ef4ee6400..28325b875c 100644 --- a/ext/crates/algebra/src/module/module_trait.rs +++ b/ext/crates/algebra/src/module/module_trait.rs @@ -6,6 +6,7 @@ use fp::{ vector::{FpSlice, FpSliceMut}, }; use itertools::Itertools; +use sseq::coordinates::MultiDegree; use crate::algebra::Algebra; @@ -18,7 +19,7 @@ use crate::algebra::Algebra; /// - [`Module::max_computed_degree`] gives the maximum degree for which the module is fully /// defined. It is guaranteed that the module will never change up to this degree in the future. /// -/// - [`Module::compute_basis`] extends the internal data to support querying data up to (and +/// - [`Module::compute_basis_multi`] extends the internal data to support querying data up to (and /// including) a given degree. In general, we can run this beyond the max computed degree. /// /// A useful example to keep in mind is a [`FreeModule`](crate::module::FreeModule), where we have @@ -26,8 +27,8 @@ use crate::algebra::Algebra; /// `compute_basis` computes data such as the offset of existing generators in potentially higher /// degrees. #[auto_impl(Arc, Box)] -pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { - type Algebra: Algebra; +pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { + type Algebra: Algebra; /// The algebra the module is over. fn algebra(&self) -> Arc; @@ -42,37 +43,37 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { /// /// See [`Module`] documentation for more details. #[allow(unused_variables)] - fn compute_basis(&self, degree: i32) {} + fn compute_basis_multi(&self, degree: MultiDegree) {} /// The maximum `t` for which the module is fully defined at `t`. See [`Module`] documentation /// for more details. fn max_computed_degree(&self) -> i32; /// The dimension of a module at the given degree - fn dimension(&self, degree: i32) -> usize; - fn act_on_basis( + fn dimension_multi(&self, degree: MultiDegree) -> usize; + fn act_on_basis_multi( &self, result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree, mod_index: usize, ); - /// Non-panicking variant of [`Module::act_on_basis`]. Validates the operation and + /// Non-panicking variant of [`Module::act_on_basis_multi`]. Validates the operation and /// module degrees/indices and returns an [`ActError`] describing the problem instead of - /// panicking. On success it delegates to [`Module::act_on_basis`] and returns `Ok(())`. - fn try_act_on_basis( + /// panicking. On success it delegates to [`Module::act_on_basis_multi`] and returns `Ok(())`. + fn try_act_on_basis_multi( &self, result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree, mod_index: usize, ) -> Result<(), ActError> { - if op_degree < 0 { + if op_degree.t() < 0 { return Err(ActError::IndexOutOfRange(format!( "op_degree {op_degree} is negative" ))); @@ -86,36 +87,36 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { ))); } let min_degree = self.min_degree(); - if mod_degree < min_degree { + if mod_degree.t() < min_degree { return Err(ActError::IndexOutOfRange(format!( "mod_degree {mod_degree} is below the module's min degree {min_degree}" ))); } - self.compute_basis(mod_degree); - let mod_dim = self.dimension(mod_degree); + self.compute_basis_multi(mod_degree); + let mod_dim = self.dimension_multi(mod_degree); if mod_index >= mod_dim { return Err(ActError::IndexOutOfRange(format!( "mod_index {mod_index} out of range for module dimension {mod_dim} in degree \ {mod_degree}" ))); } - self.act_on_basis(result, coeff, op_degree, op_index, mod_degree, mod_index); + self.act_on_basis_multi(result, coeff, op_degree, op_index, mod_degree, mod_index); Ok(()) } - /// Non-panicking variant of [`Module::act`]. Validates the operation degree/index and + /// Non-panicking variant of [`Module::act_multi`]. Validates the operation degree/index and /// the input degree/length and returns an [`ActError`] describing the problem instead of - /// panicking. On success it delegates to [`Module::act`] and returns `Ok(())`. - fn try_act( + /// panicking. On success it delegates to [`Module::act_multi`] and returns `Ok(())`. + fn try_act_multi( &self, result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree, op_index: usize, - input_degree: i32, + input_degree: MultiDegree, input: FpSlice, ) -> Result<(), ActError> { - if op_degree < 0 { + if op_degree.t() < 0 { return Err(ActError::IndexOutOfRange(format!( "op_degree {op_degree} is negative" ))); @@ -129,29 +130,39 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { ))); } let min_degree = self.min_degree(); - if input_degree < min_degree { + if input_degree.t() < min_degree { return Err(ActError::IndexOutOfRange(format!( "input_degree {input_degree} is below the module's min degree {min_degree}" ))); } - self.compute_basis(input_degree); - let input_dim = self.dimension(input_degree); + self.compute_basis_multi(input_degree); + let input_dim = self.dimension_multi(input_degree); if input.len() > input_dim { return Err(ActError::InvalidInput(format!( "input length {} exceeds module dimension {input_dim} in degree {input_degree}", input.len() ))); } - self.act(result, coeff, op_degree, op_index, input_degree, input); + self.act_multi(result, coeff, op_degree, op_index, input_degree, input); Ok(()) } /// The name of a basis element. This is useful for debugging and printing results. - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String; + fn basis_element_to_string_multi(&self, degree: MultiDegree, idx: usize) -> String; /// Whether this is the unit module. + /// + /// The default answers this from `min_degree`/`max_degree`, which are the (single) `i32` + /// filtration direction. That cannot characterize the unit when `N > 1` (distinct multidegrees + /// can share a `t`, so an extra component is invisible here), so the default conservatively + /// returns `false` for multigraded modules; such modules should override this if they need it. fn is_unit(&self) -> bool { - self.min_degree() == 0 && self.max_degree() == Some(0) && self.dimension(0) == 1 + if N > 1 { + return false; + } + self.min_degree() == 0 + && self.max_degree() == Some(0) + && self.dimension_multi(MultiDegree::zero()) == 1 } /// The prime the module is over, which should be equal to the prime of the algebra. @@ -159,7 +170,7 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { self.algebra().prime() } - /// `max_degree` is the a degree such that if t > `max_degree`, then `self.dimension(t) = 0`. + /// `max_degree` is the a degree such that if t > `max_degree`, then `self.dimension_multi(t) = 0`. fn max_degree(&self) -> Option { None } @@ -170,13 +181,16 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { self.max_degree() } - fn total_dimension(&self) -> usize { + fn total_dimension(&self) -> usize + where + MultiDegree: From, + { let max_degree = self .max_degree() .expect("total_dimension requires module to be bounded"); (self.min_degree()..=max_degree) - .map(|i| self.dimension(i)) + .map(|i| self.dimension_multi(MultiDegree::from(i))) .sum() } @@ -186,19 +200,19 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { /// This flexibility is useful when resolving to a stem. The point is that we have elements in /// degree `t` that are guaranteed to not contain generators of degree `t`, and we don't know /// what generators will be added in degree `t` yet. - fn act( + fn act_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree, op_index: usize, - input_degree: i32, + input_degree: MultiDegree, input: FpSlice, ) { - assert!(input.len() <= self.dimension(input_degree)); + assert!(input.len() <= self.dimension_multi(input_degree)); let p = self.prime(); for (i, v) in input.iter_nonzero() { - self.act_on_basis( + self.act_on_basis_multi( result.copy(), (coeff * v) % p, op_degree, @@ -209,20 +223,20 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { } } - fn act_by_element( + fn act_by_element_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree, op: FpSlice, - input_degree: i32, + input_degree: MultiDegree, input: FpSlice, ) { - assert_eq!(input.len(), self.dimension(input_degree)); + assert_eq!(input.len(), self.dimension_multi(input_degree)); assert_eq!(op.len(), self.algebra().dimension(op_degree)); let p = self.prime(); for (i, v) in op.iter_nonzero() { - self.act( + self.act_multi( result.copy(), (coeff * v) % p, op_degree, @@ -233,19 +247,19 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { } } - fn act_by_element_on_basis( + fn act_by_element_on_basis_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree, op: FpSlice, - input_degree: i32, + input_degree: MultiDegree, input_index: usize, ) { assert_eq!(op.len(), self.algebra().dimension(op_degree)); let p = self.prime(); for (i, v) in op.iter_nonzero() { - self.act_on_basis( + self.act_on_basis_multi( result.copy(), (coeff * v) % p, op_degree, @@ -257,8 +271,8 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { } /// Gives the name of an element. The default implementation is derived from - /// [`Module::basis_element_to_string`] in the obvious way. - fn element_to_string(&self, degree: i32, element: FpSlice) -> String { + /// [`Module::basis_element_to_string_multi`] in the obvious way. + fn element_to_string_multi(&self, degree: MultiDegree, element: FpSlice) -> String { let result = element .iter_nonzero() .map(|(idx, value)| { @@ -267,7 +281,7 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { } else { format!("{value} ") }; - let basis_elt = self.basis_element_to_string(degree, idx); + let basis_elt = self.basis_element_to_string_multi(degree, idx); format!("{coeff}{basis_elt}") }) .join(" + "); @@ -279,6 +293,159 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { } } +/// Ergonomic, singly-graded-friendly wrappers over [`Module`]. +/// +/// The object-safe [`Module`] trait takes concrete [`MultiDegree`] degrees, so it stays usable +/// as `dyn Module`. This blanket extension exposes the same operations under their canonical names +/// (`dimension`, `act_on_basis`, …) taking `impl Into>`, so callers keep passing +/// bare `i32`s in the singly-graded (`N = 1`) world. It is implemented for every module, including +/// `dyn Module` (via `?Sized`), so the ergonomic names are always available. +pub trait ModuleExt: Module { + /// See [`Module::compute_basis_multi`]. + fn compute_basis(&self, degree: impl Into>) { + self.compute_basis_multi(degree.into()) + } + + /// See [`Module::dimension_multi`]. + fn dimension(&self, degree: impl Into>) -> usize { + self.dimension_multi(degree.into()) + } + + /// See [`Module::act_on_basis_multi`]. + #[allow(clippy::too_many_arguments)] + fn act_on_basis( + &self, + result: FpSliceMut, + coeff: u32, + op_degree: impl Into>, + op_index: usize, + mod_degree: impl Into>, + mod_index: usize, + ) { + self.act_on_basis_multi( + result, + coeff, + op_degree.into(), + op_index, + mod_degree.into(), + mod_index, + ) + } + + /// See [`Module::try_act_on_basis_multi`]. + #[allow(clippy::too_many_arguments)] + fn try_act_on_basis( + &self, + result: FpSliceMut, + coeff: u32, + op_degree: impl Into>, + op_index: usize, + mod_degree: impl Into>, + mod_index: usize, + ) -> Result<(), ActError> { + self.try_act_on_basis_multi( + result, + coeff, + op_degree.into(), + op_index, + mod_degree.into(), + mod_index, + ) + } + + /// See [`Module::try_act_multi`]. + fn try_act( + &self, + result: FpSliceMut, + coeff: u32, + op_degree: impl Into>, + op_index: usize, + input_degree: impl Into>, + input: FpSlice, + ) -> Result<(), ActError> { + self.try_act_multi( + result, + coeff, + op_degree.into(), + op_index, + input_degree.into(), + input, + ) + } + + /// See [`Module::basis_element_to_string_multi`]. + fn basis_element_to_string(&self, degree: impl Into>, idx: usize) -> String { + self.basis_element_to_string_multi(degree.into(), idx) + } + + /// See [`Module::act_multi`]. + fn act( + &self, + result: FpSliceMut, + coeff: u32, + op_degree: impl Into>, + op_index: usize, + input_degree: impl Into>, + input: FpSlice, + ) { + self.act_multi( + result, + coeff, + op_degree.into(), + op_index, + input_degree.into(), + input, + ) + } + + /// See [`Module::act_by_element_multi`]. + fn act_by_element( + &self, + result: FpSliceMut, + coeff: u32, + op_degree: impl Into>, + op: FpSlice, + input_degree: impl Into>, + input: FpSlice, + ) { + self.act_by_element_multi( + result, + coeff, + op_degree.into(), + op, + input_degree.into(), + input, + ) + } + + /// See [`Module::act_by_element_on_basis_multi`]. + fn act_by_element_on_basis( + &self, + result: FpSliceMut, + coeff: u32, + op_degree: impl Into>, + op: FpSlice, + input_degree: impl Into>, + input_index: usize, + ) { + self.act_by_element_on_basis_multi( + result, + coeff, + op_degree.into(), + op, + input_degree.into(), + input_index, + ) + } + + /// See [`Module::element_to_string_multi`]. + fn element_to_string(&self, degree: impl Into>, element: FpSlice) -> String { + self.element_to_string_multi(degree.into(), element) + } +} + +impl + ?Sized> ModuleExt for M {} + #[derive(Debug)] pub struct ModuleFailedRelationError { pub relation: String, @@ -297,7 +464,7 @@ impl std::fmt::Display for ModuleFailedRelationError { impl std::error::Error for ModuleFailedRelationError {} -/// Error returned by [`Module::try_act`] and [`Module::try_act_on_basis`]. +/// Error returned by [`Module::try_act_multi`] and [`Module::try_act_on_basis_multi`]. /// /// The variants separate the distinct failure categories so callers (e.g. the /// Python bindings) can map them to different error types. diff --git a/ext/crates/algebra/src/module/quotient_module.rs b/ext/crates/algebra/src/module/quotient_module.rs index ba983bce69..d53d38f182 100644 --- a/ext/crates/algebra/src/module/quotient_module.rs +++ b/ext/crates/algebra/src/module/quotient_module.rs @@ -6,8 +6,9 @@ use fp::{ matrix::Subspace, vector::{FpSlice, FpSliceMut, FpVector}, }; +use sseq::coordinates::MultiDegree; -use crate::module::{Module, ZeroModule}; +use crate::module::{Module, ModuleExt, ZeroModule}; /// A quotient of a module truncated below a fix degree. pub struct QuotientModule { @@ -173,7 +174,8 @@ impl Module for QuotientModule { self.module.max_computed_degree() } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); if degree > self.truncation { 0 } else { @@ -181,15 +183,17 @@ impl Module for QuotientModule { } } - fn act_on_basis( + fn act_on_basis_multi( &self, result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); let target_deg = op_degree + mod_degree; if target_deg > self.truncation { return; @@ -208,7 +212,8 @@ impl Module for QuotientModule { self.old_basis_to_new(target_deg, result, result_.as_slice()); } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); self.module .basis_element_to_string(degree, self.basis_list[degree][idx]) } diff --git a/ext/crates/algebra/src/module/rpn.rs b/ext/crates/algebra/src/module/rpn.rs index 6d42a1f9f8..5b5b0d0a98 100644 --- a/ext/crates/algebra/src/module/rpn.rs +++ b/ext/crates/algebra/src/module/rpn.rs @@ -6,6 +6,7 @@ use fp::{ }; use serde::Deserialize; use serde_json::Value; +use sseq::coordinates::MultiDegree; use crate::{ algebra::{ @@ -13,7 +14,7 @@ use crate::{ adem_algebra::AdemBasisElement, milnor_algebra::{MilnorBasisElement, PPartEntry}, }, - module::{Module, ZeroModule}, + module::{Module, ModuleExt, ZeroModule}, }; /// This is $\mathbb{RP}_{\mathrm{min}}^{\mathrm{max}}$. The cohomology is the subquotient of @@ -72,7 +73,8 @@ where i32::MAX } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); if degree < self.min { return 0; } @@ -94,20 +96,23 @@ where 1 } - fn basis_element_to_string(&self, degree: i32, _idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, _idx: usize) -> String { + let degree = i32::from(degree); // It is an error to call the function if self.dimension(degree) == 0 format!("x^{{{degree}}}") } - fn act_on_basis( + fn act_on_basis_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); assert!(op_index < self.algebra().dimension(op_degree)); assert!(mod_index < self.dimension(mod_degree)); diff --git a/ext/crates/algebra/src/module/suspension_module.rs b/ext/crates/algebra/src/module/suspension_module.rs index 8b92ac1f5e..7036cfb49d 100644 --- a/ext/crates/algebra/src/module/suspension_module.rs +++ b/ext/crates/algebra/src/module/suspension_module.rs @@ -1,6 +1,8 @@ use std::sync::Arc; -use crate::module::{Module, ZeroModule}; +use sseq::coordinates::MultiDegree; + +use crate::module::{Module, ModuleExt, ZeroModule}; pub struct SuspensionModule { inner: Arc, @@ -27,7 +29,8 @@ impl std::fmt::Display for SuspensionModule { impl Module for SuspensionModule { type Algebra = M::Algebra; - fn compute_basis(&self, degree: i32) { + fn compute_basis_multi(&self, degree: MultiDegree<1>) { + let degree = i32::from(degree); self.inner.compute_basis(degree - self.shift); } @@ -51,15 +54,17 @@ impl Module for SuspensionModule { self.inner.total_dimension() } - fn act( + fn act_multi( &self, result: fp::vector::FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - input_degree: i32, + input_degree: MultiDegree<1>, input: fp::vector::FpSlice, ) { + let op_degree = i32::from(op_degree); + let input_degree = i32::from(input_degree); self.inner.act( result, coeff, @@ -70,15 +75,17 @@ impl Module for SuspensionModule { ); } - fn act_by_element( + fn act_by_element_multi( &self, result: fp::vector::FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op: fp::vector::FpSlice, - input_degree: i32, + input_degree: MultiDegree<1>, input: fp::vector::FpSlice, ) { + let op_degree = i32::from(op_degree); + let input_degree = i32::from(input_degree); self.inner.act_by_element( result, coeff, @@ -89,15 +96,17 @@ impl Module for SuspensionModule { ); } - fn act_by_element_on_basis( + fn act_by_element_on_basis_multi( &self, result: fp::vector::FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op: fp::vector::FpSlice, - input_degree: i32, + input_degree: MultiDegree<1>, input_index: usize, ) { + let op_degree = i32::from(op_degree); + let input_degree = i32::from(input_degree); self.inner.act_by_element_on_basis( result, coeff, @@ -108,7 +117,12 @@ impl Module for SuspensionModule { ); } - fn element_to_string(&self, degree: i32, element: fp::vector::FpSlice) -> String { + fn element_to_string_multi( + &self, + degree: MultiDegree<1>, + element: fp::vector::FpSlice, + ) -> String { + let degree = i32::from(degree); self.inner.element_to_string(degree - self.shift, element) } @@ -124,19 +138,22 @@ impl Module for SuspensionModule { self.inner.max_computed_degree() + self.shift } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); self.inner.dimension(degree - self.shift) } - fn act_on_basis( + fn act_on_basis_multi( &self, result: fp::vector::FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); self.inner.act_on_basis( result, coeff, @@ -147,7 +164,8 @@ impl Module for SuspensionModule { ); } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); self.inner.basis_element_to_string(degree - self.shift, idx) } } diff --git a/ext/crates/algebra/src/module/tensor_module.rs b/ext/crates/algebra/src/module/tensor_module.rs index 64f4def256..a7b5e283a5 100644 --- a/ext/crates/algebra/src/module/tensor_module.rs +++ b/ext/crates/algebra/src/module/tensor_module.rs @@ -6,10 +6,11 @@ use fp::{ vector::{FpSlice, FpSliceMut, FpVector}, }; use once::OnceBiVec; +use sseq::coordinates::MultiDegree; use crate::{ algebra::{Algebra, Bialgebra}, - module::{Module, ZeroModule, block_structure::BlockStructure}, + module::{Module, ModuleExt, ZeroModule, block_structure::BlockStructure}, }; // This really only makes sense when the algebra is a bialgebra, but associated type bounds are @@ -76,14 +77,14 @@ where for left_deg in self.left.min_degree()..=(mod_degree - self.right.min_degree()) { let right_deg = mod_degree - left_deg; - // Here we use `Module::dimension(&*m, i)` instead of `m.dimension(i)` because there are + // Here we use `ModuleExt::dimension(&*m, i)` instead of `m.dimension(i)` because there are // multiple `dimension` methods in scope and rust-analyzer gets confused if we're not // explicit enough. - let left_source_dim = Module::dimension(&*self.left, left_deg); - let right_source_dim = Module::dimension(&*self.right, right_deg); + let left_source_dim = ModuleExt::dimension(&*self.left, left_deg); + let right_source_dim = ModuleExt::dimension(&*self.right, right_deg); - let left_target_dim = Module::dimension(&*self.left, left_deg + op_deg_l); - let right_target_dim = Module::dimension(&*self.right, right_deg + op_deg_r); + let left_target_dim = ModuleExt::dimension(&*self.left, left_deg + op_deg_l); + let right_target_dim = ModuleExt::dimension(&*self.right, right_deg + op_deg_r); if left_target_dim == 0 || right_target_dim == 0 @@ -165,19 +166,21 @@ where self.block_structures.len() } - fn compute_basis(&self, degree: i32) { + fn compute_basis_multi(&self, degree: MultiDegree<1>) { + let degree = i32::from(degree); self.left.compute_basis(degree - self.right.min_degree()); self.right.compute_basis(degree - self.left.min_degree()); self.block_structures.extend(degree, |i| { let mut block_sizes = BiVec::with_capacity(self.left.min_degree(), i - self.right.min_degree() + 1); for j in self.left.min_degree()..=i - self.right.min_degree() { - // Here we use `Module::dimension(&*m, i)` instead of `m.dimension(i)` because there are + // Here we use `ModuleExt::dimension(&*m, i)` instead of `m.dimension(i)` because there are // multiple `dimension` methods in scope and rust-analyzer gets confused if we're not // explicit enough. - let mut block_sizes_entry = Vec::with_capacity(Module::dimension(&*self.left, j)); - for _ in 0..Module::dimension(&*self.left, j) { - block_sizes_entry.push(Module::dimension(&*self.right, i - j)) + let mut block_sizes_entry = + Vec::with_capacity(ModuleExt::dimension(&*self.left, j)); + for _ in 0..ModuleExt::dimension(&*self.left, j) { + block_sizes_entry.push(ModuleExt::dimension(&*self.right, i - j)) } block_sizes.push(block_sizes_entry); } @@ -186,19 +189,22 @@ where }); } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); self.block_structures[degree].total_dimension() } - fn act_on_basis( + fn act_on_basis_multi( &self, result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); let mut working_element = FpVector::new(self.prime(), self.dimension(mod_degree)); working_element.set_entry(mod_index, 1); @@ -212,15 +218,17 @@ where ); } - fn act( + fn act_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, input: FpSlice, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); if op_degree == 0 { result.add(input, coeff); return; @@ -276,15 +284,16 @@ where } } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); let left_degree = self.seek_module_num(degree, idx); let right_degree = degree - left_degree; let inner_index = idx - self.offset(degree, left_degree); - // Here we use `Module::dimension(&*m, i)` instead of `m.dimension(i)` because there are + // Here we use `ModuleExt::dimension(&*m, i)` instead of `m.dimension(i)` because there are // multiple `dimension` methods in scope and rust-analyzer gets confused if we're not // explicit enough. - let right_dim = Module::dimension(&*self.right, right_degree); + let right_dim = ModuleExt::dimension(&*self.right, right_degree); let left_index = inner_index / right_dim; let right_index = inner_index % right_dim; diff --git a/ext/crates/sseq/src/coordinates/degree.rs b/ext/crates/sseq/src/coordinates/degree.rs index d9fbe51c0c..9d71921e61 100644 --- a/ext/crates/sseq/src/coordinates/degree.rs +++ b/ext/crates/sseq/src/coordinates/degree.rs @@ -66,6 +66,21 @@ impl From<[i32; N]> for MultiDegree { } } +/// A single-graded degree is just an `i32`. This lets every `i32` degree in the singly-graded +/// (`N == 1`) world coerce into a `MultiDegree<1>`, so callers can keep passing bare `i32`s to the +/// multigraded trait methods (which take `impl Into>`). +impl From for MultiDegree<1> { + fn from(t: i32) -> Self { + Self { coords: [t] } + } +} + +impl From> for i32 { + fn from(d: MultiDegree<1>) -> Self { + d.coords[0] + } +} + impl From> for [i32; N] { fn from(d: MultiDegree) -> [i32; N] { d.coords diff --git a/ext/examples/bruner.rs b/ext/examples/bruner.rs index 71c5c0e22a..ba6887da39 100644 --- a/ext/examples/bruner.rs +++ b/ext/examples/bruner.rs @@ -26,7 +26,7 @@ use std::{ use algebra::{ Algebra, MilnorAlgebra, milnor_algebra::MilnorBasisElement, - module::{FreeModule as FM, Module, homomorphism::FreeModuleHomomorphism as FMH}, + module::{FreeModule as FM, ModuleExt, homomorphism::FreeModuleHomomorphism as FMH}, }; use anyhow::{Context, Error, Result}; use ext::{ diff --git a/ext/examples/ext_m_n.rs b/ext/examples/ext_m_n.rs index 0796354f3d..a142796f8e 100644 --- a/ext/examples/ext_m_n.rs +++ b/ext/examples/ext_m_n.rs @@ -52,7 +52,7 @@ mod hom_cochain_complex { use std::sync::Arc; use algebra::module::{ - HomModule, Module, + HomModule, Module, ModuleExt, homomorphism::{HomPullback, ModuleHomomorphism}, }; use ext::chain_complex::FreeChainComplex; diff --git a/ext/examples/lift_hom.rs b/ext/examples/lift_hom.rs index 3958a6069c..aa79b4272c 100644 --- a/ext/examples/lift_hom.rs +++ b/ext/examples/lift_hom.rs @@ -41,7 +41,7 @@ use std::{path::PathBuf, sync::Arc}; -use algebra::module::Module; +use algebra::module::{Module, ModuleExt}; use anyhow::{Context, anyhow}; use ext::{ chain_complex::{AugmentedChainComplex, ChainComplex, FreeChainComplex}, diff --git a/ext/examples/massey.rs b/ext/examples/massey.rs index 78b9c077d1..2417646a90 100644 --- a/ext/examples/massey.rs +++ b/ext/examples/massey.rs @@ -5,14 +5,14 @@ use std::sync::Arc; -use ext::{chain_complex::ChainComplex, ext_algebra::ExtAlgebra}; +use ext::{chain_complex::ChainComplex, ext_algebra::ExtModule}; use sseq::coordinates::Bidegree; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; let resolution = Arc::new(ext::utils::query_module(None, true)?); - let e2 = ExtAlgebra::from_resolution(Arc::clone(&resolution))?; + let e2 = ExtModule::from_resolution(Arc::clone(&resolution))?; eprintln!("\nComputing Massey products "); eprintln!("\nEnter a:"); @@ -21,9 +21,9 @@ fn main() -> anyhow::Result<()> { query::raw("n of Ext class a", str::parse), query::raw("s of Ext class a", str::parse::).get(), ); - e2.unit().compute_through_stem(a_deg); - let a_class = query::vector("Input Ext class a", e2.unit_dimension(a_deg)); - let a = e2.unit_element(a_deg, &a_class); + e2.algebra().resolution().compute_through_stem(a_deg); + let a_class = query::vector("Input Ext class a", e2.algebra().dimension(a_deg)); + let a = e2.algebra().element(a_deg, &a_class); eprintln!("\nEnter b:"); @@ -31,15 +31,15 @@ fn main() -> anyhow::Result<()> { query::raw("n of Ext class b", str::parse), query::raw("s of Ext class b", str::parse::).get(), ); - e2.unit().compute_through_stem(b_deg); - let b_class = query::vector("Input Ext class b", e2.unit_dimension(b_deg)); - let b = e2.unit_element(b_deg, &b_class); + e2.algebra().resolution().compute_through_stem(b_deg); + let b_class = query::vector("Input Ext class b", e2.algebra().dimension(b_deg)); + let b = e2.algebra().element(b_deg, &b_class); // The Massey product shifts the bidegree by this amount. let shift = a_deg + b_deg - Bidegree::s_t(1, 0); if !e2.is_unit() { - e2.unit().compute_through_stem(shift); + e2.algebra().resolution().compute_through_stem(shift); } if !resolution.has_computed_bidegree(shift + Bidegree::s_t(0, resolution.min_degree())) { diff --git a/ext/examples/product.rs b/ext/examples/product.rs index 16d2a70445..0074ae8d10 100644 --- a/ext/examples/product.rs +++ b/ext/examples/product.rs @@ -4,18 +4,18 @@ //! `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. +//! written against the [`ExtModule`] 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 ext::{chain_complex::FreeChainComplex, ext_algebra::ExtModule, utils::query_module}; use sseq::coordinates::Bidegree; fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; let resolution = Arc::new(query_module(None, true)?); - let e2 = ExtAlgebra::from_resolution(resolution)?; + let e2 = ExtModule::from_resolution(resolution)?; let shift = Bidegree::n_s( query::raw("n of Ext class", str::parse), @@ -29,12 +29,12 @@ fn main() -> anyhow::Result<()> { let v: Vec = query::vector("Input Ext class", dim); let x = e2.element(shift, &v); - for b in e2.unit().iter_nonzero_stem() { + for b in e2.algebra().resolution().iter_nonzero_stem() { // `None` means `b + shift` is out of the computed range, so skip it. let Some(rows) = e2.multiply_into(&x, b) else { continue; }; - for (g, row) in e2.unit_basis(b).into_iter().zip(rows.iter()) { + for (g, row) in e2.algebra().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:?}"); diff --git a/ext/examples/resolution_size.rs b/ext/examples/resolution_size.rs index 50df7eb63a..d2be52f422 100644 --- a/ext/examples/resolution_size.rs +++ b/ext/examples/resolution_size.rs @@ -1,4 +1,4 @@ -use algebra::module::Module; +use algebra::module::{Module, ModuleExt}; use ext::{chain_complex::ChainComplex, utils::query_module}; fn main() -> anyhow::Result<()> { diff --git a/ext/examples/secondary.rs b/ext/examples/secondary.rs index 44c5cc3b0d..3d479afb7e 100644 --- a/ext/examples/secondary.rs +++ b/ext/examples/secondary.rs @@ -50,7 +50,7 @@ use std::sync::Arc; use algebra::module::Module; use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - ext_algebra::{ExtAlgebra, secondary::SecondaryExtAlgebra}, + ext_algebra::{ExtModule, secondary::SecondaryExtAlgebra}, utils::query_module, }; use sseq::coordinates::Bidegree; @@ -61,8 +61,8 @@ fn main() -> anyhow::Result<()> { let resolution = Arc::new(query_module(Some(algebra::AlgebraType::Milnor), true)?); // 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))); + // setup with `ExtModule::intrinsic` (the module is its own `k`). + let sec_e2 = SecondaryExtAlgebra::new(Arc::new(ExtModule::intrinsic(resolution))); if let Some(s) = ext::utils::secondary_job() { sec_e2.compute_partial(s); @@ -71,7 +71,7 @@ fn main() -> anyhow::Result<()> { sec_e2.extend_all(); - let e2 = sec_e2.ext_algebra(); + let e2 = sec_e2.module(); let d2_shift = Bidegree::n_s(-1, 2); // Iterate through the target of the d2, in the same order as before. diff --git a/ext/examples/secondary_product.rs b/ext/examples/secondary_product.rs index b024a8ce2e..1ac163dad2 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::{ExtModule, SecondaryExtAlgebra}, secondary::{LAMBDA_BIDEGREE, SecondaryLift}, utils::query_module, }; @@ -35,7 +35,7 @@ fn main() -> anyhow::Result<()> { ext::utils::init_logging()?; let resolution = Arc::new(query_module(Some(algebra::AlgebraType::Milnor), true)?); - let e2 = Arc::new(ExtAlgebra::from_resolution(Arc::clone(&resolution))?); + let e2 = Arc::new(ExtModule::from_resolution(Arc::clone(&resolution))?); let name: String = query::raw("Name of product", str::parse); let shift = Bidegree::n_s( @@ -56,7 +56,9 @@ fn main() -> anyhow::Result<()> { resolution.module(0).max_computed_degree(), resolution.next_homological_degree() - 1, ); - e2.unit().compute_through_stem(res_max - shift); + e2.algebra() + .resolution() + .compute_through_stem(res_max - shift); } let sec_e2 = Arc::new(SecondaryExtAlgebra::new(Arc::clone(&e2))); @@ -80,7 +82,7 @@ fn main() -> anyhow::Result<()> { let disp = format!("[{name}]"); // Iterate through the multiplicand. - for b in e2.unit().iter_nonzero_stem() { + for b in e2.algebra().resolution().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) { diff --git a/ext/examples/sq0.rs b/ext/examples/sq0.rs index dc671c159b..dd799fa2ca 100644 --- a/ext/examples/sq0.rs +++ b/ext/examples/sq0.rs @@ -144,11 +144,12 @@ mod double { pub mod double_module { use std::sync::Arc; - use algebra::module::{Module, homomorphism::ModuleHomomorphism}; + use algebra::module::{Module, ModuleExt, homomorphism::ModuleHomomorphism}; use fp::{ matrix::{Matrix, MatrixSliceMut, QuasiInverse, Subspace}, vector::{FpSlice, FpSliceMut}, }; + use sseq::coordinates::MultiDegree; use super::DoubleAlgebra; @@ -189,7 +190,8 @@ mod double { self.inner.max_computed_degree() * 2 } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); if degree % 2 == 0 { self.inner.dimension(degree / 2) } else { @@ -197,15 +199,17 @@ mod double { } } - fn act_on_basis( + fn act_on_basis_multi( &self, result: fp::vector::FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); if op_degree % 2 == 1 { return; } @@ -221,7 +225,8 @@ mod double { } } - fn basis_element_to_string(&self, degree: i32, idx: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, idx: usize) -> String { + let degree = i32::from(degree); self.inner.basis_element_to_string(degree / 2, idx) } @@ -251,15 +256,17 @@ mod double { /// This flexibility is useful when resolving to a stem. The point is that we have elements in /// degree `t` that are guaranteed to not contain generators of degree `t`, and we don't know /// what generators will be added in degree `t` yet. - fn act( + fn act_multi( &self, result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - input_degree: i32, + input_degree: MultiDegree<1>, input: FpSlice, ) { + let op_degree = i32::from(op_degree); + let input_degree = i32::from(input_degree); if op_degree % 2 == 1 { return; } @@ -277,8 +284,9 @@ mod double { /// Gives the name of an element. The default implementation is derived from /// [`Module::basis_element_to_string`] in the obvious way. - fn element_to_string(&self, degree: i32, element: FpSlice) -> String { - self.inner.element_to_string(degree, element) + fn element_to_string_multi(&self, degree: MultiDegree<1>, element: FpSlice) -> String { + let degree = i32::from(degree); + self.inner.element_to_string(degree / 2, element) } } diff --git a/ext/examples/steenrod.rs b/ext/examples/steenrod.rs index e94b301d6e..d4a18c4e21 100644 --- a/ext/examples/steenrod.rs +++ b/ext/examples/steenrod.rs @@ -4,7 +4,7 @@ use std::{ }; use algebra::module::{ - Module, + Module, ModuleExt, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, }; use ext::{ @@ -190,12 +190,13 @@ mod sum_module { use std::sync::Arc; use algebra::module::{ - Module, ZeroModule, + Module, ModuleExt, ZeroModule, block_structure::{BlockStructure, GeneratorBasisEltPair}, }; use bivec::BiVec; use fp::vector::FpSliceMut; use once::OnceBiVec; + use sseq::coordinates::MultiDegree; pub struct SumModule { // We need these because modules might be empty @@ -253,7 +254,8 @@ mod sum_module { self.min_degree } - fn compute_basis(&self, degree: i32) { + fn compute_basis_multi(&self, degree: MultiDegree<1>) { + let degree = i32::from(degree); for module in &self.modules { module.compute_basis(degree); } @@ -269,22 +271,25 @@ mod sum_module { self.block_structures.len() } - fn dimension(&self, degree: i32) -> usize { + fn dimension_multi(&self, degree: MultiDegree<1>) -> usize { + let degree = i32::from(degree); self.block_structures .get(degree) .map(BlockStructure::total_dimension) .unwrap_or(0) } - fn act_on_basis( + fn act_on_basis_multi( &self, mut result: FpSliceMut, coeff: u32, - op_degree: i32, + op_degree: MultiDegree<1>, op_index: usize, - mod_degree: i32, + mod_degree: MultiDegree<1>, mod_index: usize, ) { + let op_degree = i32::from(op_degree); + let mod_degree = i32::from(mod_degree); let target_degree = mod_degree + op_degree; let GeneratorBasisEltPair { generator_index: module_num, @@ -305,7 +310,8 @@ mod sum_module { ); } - fn basis_element_to_string(&self, degree: i32, index: usize) -> String { + fn basis_element_to_string_multi(&self, degree: MultiDegree<1>, index: usize) -> String { + let degree = i32::from(degree); let GeneratorBasisEltPair { generator_index: module_num, basis_index, @@ -379,7 +385,7 @@ mod tensor_product_chain_complex { use algebra::{ Algebra, Bialgebra, - module::{Module, TensorModule, ZeroModule, homomorphism::ModuleHomomorphism}, + module::{ModuleExt, TensorModule, ZeroModule, homomorphism::ModuleHomomorphism}, }; use ext::chain_complex::ChainComplex; use fp::{ diff --git a/ext/src/chain_complex/chain_homotopy.rs b/ext/src/chain_complex/chain_homotopy.rs index c0ec0fd0d2..533f9ba8f5 100644 --- a/ext/src/chain_complex/chain_homotopy.rs +++ b/ext/src/chain_complex/chain_homotopy.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, Mutex}; use algebra::module::{ - Module, + Module, ModuleExt, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, }; use fp::{prime::ValidPrime, vector::FpVector}; @@ -294,7 +294,7 @@ pub(crate) mod secondary { use std::sync::Arc; use algebra::{ - module::{Module, homomorphism::ModuleHomomorphism}, + module::{ModuleExt, homomorphism::ModuleHomomorphism}, pair_algebra::PairAlgebra, }; use dashmap::DashMap; diff --git a/ext/src/chain_complex/finite_chain_complex.rs b/ext/src/chain_complex/finite_chain_complex.rs index e5a068ffd0..c3b7a2e6ef 100644 --- a/ext/src/chain_complex/finite_chain_complex.rs +++ b/ext/src/chain_complex/finite_chain_complex.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use algebra::module::{ - Module, ZeroModule, + Module, ModuleExt, ZeroModule, homomorphism::{FullModuleHomomorphism, ModuleHomomorphism, ZeroHomomorphism}, }; use sseq::coordinates::Bidegree; diff --git a/ext/src/chain_complex/mod.rs b/ext/src/chain_complex/mod.rs index cf9b275801..996b94858e 100644 --- a/ext/src/chain_complex/mod.rs +++ b/ext/src/chain_complex/mod.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use algebra::{ Algebra, MuAlgebra, module::{ - Module, MuFreeModule, + Module, ModuleExt, MuFreeModule, homomorphism::{ModuleHomomorphism, MuFreeModuleHomomorphism}, }, }; diff --git a/ext/src/ext_algebra/massey.rs b/ext/src/ext_algebra/massey.rs index d29acdc403..3d7e5a39d0 100644 --- a/ext/src/ext_algebra/massey.rs +++ b/ext/src/ext_algebra/massey.rs @@ -1,7 +1,7 @@ //! Primary Massey products in $\Ext$. //! -//! [`ExtAlgebra::massey`] computes a single triple Massey product $\langle a, b, c\rangle$, while -//! [`ExtAlgebra::massey_iter_c`] and [`ExtAlgebra::massey_iter_a`] sweep a whole family at once: +//! [`ExtModule::massey`] computes a single triple Massey product $\langle a, b, c\rangle$, while +//! [`ExtModule::massey_iter_c`] and [`ExtModule::massey_iter_a`] sweep a whole family at once: //! the former fixes $a, b$ and ranges over every valid third factor $\langle a, b, -\rangle$, the //! latter fixes $b, c$ and ranges over every valid first factor $\langle -, b, c\rangle$. The two //! directions differ in whether the `b ∘ c` null-homotopy is rebuilt per `c` or reused for fixed @@ -24,7 +24,7 @@ use fp::{ }; use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator}; -use super::ExtAlgebra; +use super::ExtModule; use crate::{ chain_complex::{AugmentedChainComplex, ChainHomotopy, FreeChainComplex}, resolution_homomorphism::ResolutionHomomorphism, @@ -50,7 +50,7 @@ impl MasseyResult { } } -impl ExtAlgebra +impl ExtModule where CC: FreeChainComplex + AugmentedChainComplex, { @@ -60,23 +60,17 @@ where a.degree() + b.degree() - Bidegree::s_t(1, 0) } - /// The multiplication-by-`b` chain self-map of the unit, extended far enough for brackets - /// landing at `shift`. + /// The multiplication-by-`b` chain self-map of the unit (`res(k) → res(k)`), extended far enough + /// for brackets landing at `shift`. This comes from + /// [`ExtAlgebra::class_product_map`](super::ExtAlgebra::class_product_map), which caches and + /// shares the per-*generator* product maps; a general (multi-generator) class assembles a fresh + /// combined map from them on every call. fn massey_b_hom( &self, b: &BidegreeElement, shift: Bidegree, ) -> Arc> { - let b_coords: Vec = b.vec().iter().collect(); - let hom = Arc::new(ResolutionHomomorphism::from_class( - String::new(), - Arc::clone(self.unit()), - Arc::clone(self.unit()), - b.degree(), - &b_coords, - )); - hom.extend_through_stem(shift); - hom + self.algebra().class_product_map(b, shift) } /// The kernel of multiplication by `b` at bidegree `c_deg`: the valid third factors of @@ -129,7 +123,7 @@ where ) -> Option { let p = self.prime(); let resolution = self.resolution(); - let unit = self.unit(); + let unit = self.algebra().resolution(); let c_deg = c.degree(); let tot = c_deg + shift; @@ -228,8 +222,8 @@ where } } // Ext(k, k)^{tot - c.degree()} · c, computed as c · x (equal up to sign). - for x in self.unit_basis(tot - c.degree()) { - if let Some(prod) = self.try_multiply(c, &self.unit_generator(x)) { + for x in self.algebra().basis(tot - c.degree()) { + if let Some(prod) = self.try_multiply(c, &self.algebra().generator(x)) { sub.add_vector(prod.vec()); } } @@ -293,13 +287,15 @@ where ) -> Vec<(BidegreeElement, MasseyResult)> { let p = self.prime(); let resolution = self.resolution(); - let unit = self.unit(); + let unit = self.algebra().resolution(); // The bracket of a first factor `a` lands at `tot = a.degree() + bc_shift`. let bc_shift = b.degree() + c.degree() - Bidegree::s_t(1, 0); // `f_c` realises `c` (resolution of `M` → unit); `f_b` is multiplication by `b` (in the - // unit). The single null-homotopy `s_bc` of `b ∘ c` is reused for every first factor. + // unit), from `class_product_map` (per-generator maps are cached and shared, but a + // multi-generator `b` assembles a fresh combined map here). The single null-homotopy `s_bc` + // of `b ∘ c` is reused for every first factor. let c_coords: Vec = c.vec().iter().collect(); let f_c = Arc::new(ResolutionHomomorphism::from_class( String::new(), @@ -308,14 +304,7 @@ where c.degree(), &c_coords, )); - let b_coords: Vec = b.vec().iter().collect(); - let f_b = Arc::new(ResolutionHomomorphism::from_class( - String::new(), - Arc::clone(unit), - Arc::clone(unit), - b.degree(), - &b_coords, - )); + let f_b = self.algebra().class_product_map(b, bc_shift); let s_bc = ChainHomotopy::new(Arc::clone(&f_c), Arc::clone(&f_b)); let mut results = Vec::new(); @@ -392,7 +381,12 @@ where // `shift`, so `b_hom` must be extended one step further than `massey_b_hom` built it. let ab_deg = a.degree() + b.degree(); b_hom.extend_through_stem(ab_deg); - let mut ab = FpVector::new(self.prime(), self.unit().number_of_gens_in_bidegree(ab_deg)); + let mut ab = FpVector::new( + self.prime(), + self.algebra() + .resolution() + .number_of_gens_in_bidegree(ab_deg), + ); for (j, coef) in a.vec().iter_nonzero() { b_hom.act( ab.as_slice_mut(), @@ -427,7 +421,7 @@ mod tests { fn test_sphere_massey() { let res = Arc::new(construct_standard::("S_2", None).unwrap()); res.compute_through_stem(Bidegree::n_s(6, 5)); - let alg = ExtAlgebra::new(Arc::clone(&res), res); + let alg = ExtModule::intrinsic(res); let h0 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); let h1 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(1, 1), 0)); @@ -482,7 +476,7 @@ mod tests { fn test_iter_a_matches_iter_c() { let res = Arc::new(construct_standard::("S_2", None).unwrap()); res.compute_through_stem(Bidegree::n_s(6, 5)); - let alg = ExtAlgebra::new(Arc::clone(&res), res); + let alg = ExtModule::intrinsic(res); let h0 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); let h1 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(1, 1), 0)); @@ -516,7 +510,7 @@ mod tests { fn test_iter_c_proper_kernel() { let res = Arc::new(construct_standard::("S_2", None).unwrap()); res.compute_through_stem(Bidegree::n_s(6, 5)); - let alg = ExtAlgebra::new(Arc::clone(&res), res); + let alg = ExtModule::intrinsic(res); let h0 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); let h1 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(1, 1), 0)); diff --git a/ext/src/ext_algebra/mod.rs b/ext/src/ext_algebra/mod.rs index f229886636..e9be5dffdf 100644 --- a/ext/src/ext_algebra/mod.rs +++ b/ext/src/ext_algebra/mod.rs @@ -1,20 +1,34 @@ -//! A bigraded-algebra view of a resolution. +//! Ext as a bigraded algebra and its modules. //! -//! [`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. +//! This splits the two objects that a resolution computes: //! -//! 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. +//! - [`ExtAlgebra`] is the **ring** $\Ext(k, k)$, backed by a resolution of the base field `k`. It +//! owns the ring-product cache (multiplication maps among $\Ext(k, k)$ generators, `res(k) → +//! res(k)`), and in particular is the single home for the multiply-by-a-class maps that Massey +//! products need (see [`ExtAlgebra::class_product_map`]). +//! - [`ExtModule`] is a **module** $\Ext(M, k)$ over that ring, backed by a resolution of `M`. It +//! holds a shared [`Arc`] to the [`ExtAlgebra`] (so every module over the same `k` shares one ring +//! cache) and its own module-action cache (`M`'s Ext-generators acted on by ring elements, `res(M) +//! → res(k)`). When `M == k` the module shares its resolution with the ring, so "a module over +//! itself" is just an [`ExtModule`] whose resolution is `Arc`-equal to the ring's (see +//! [`ExtModule::is_unit`]); there is no `is_unit` special-casing baked into the ring. +//! +//! [`ExtAlgebra`] implements the [`algebra::Algebra`] trait (as `Algebra<2>`, i.e. bigraded) and +//! [`ExtModule`] implements [`algebra::module::Module`] (as `Module<2>`) with `Algebra = +//! ExtAlgebra`. The product/action trait methods are *total* (they panic if the relevant bidegree +//! has not been resolved — call [`ExtAlgebra::compute_through_bidegree`] / +//! [`ExtModule::compute_through_bidegree`] first). The inherent `multiply_into` / `try_multiply` +//! helpers return [`Option`] instead, for the "maybe out of computed range" ergonomics the examples +//! rely on. //! //! # 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. +//! A product is realised by a [`ResolutionHomomorphism`] built from a fixed multiplier class. For a +//! module product the multiplier lives in $\Ext(M, k)$ (source = resolution of `M`, target = +//! resolution of `k`); for a ring product it lives in $\Ext(k, k)$ (source = 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* (keyed by [`BidegreeGenerator`]); a product +//! by a general class is assembled at request time from the generator maps. Products are computed +//! up to a sign (as `y · x` where convenient), matching the existing example scripts. //! //! The secondary differential ($d_2$) and the $\Mod_{C\lambda^2}$ secondary product live in the //! [`secondary`] submodule ([`SecondaryExtAlgebra`]). @@ -35,81 +49,263 @@ use crate::{ 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. +/// The ring $\Ext(k, k)$, backed by a resolution of the base field `k`. +/// +/// See the [module-level documentation](self) for how this relates to [`ExtModule`]. pub struct ExtAlgebra { - /// Resolution of `M`; products land in its Ext. + /// Resolution of the base field `k`. Ring products live here. + resolution: Arc, + /// One multiplication map per generator of $\Ext(k, k)$, `res(k) → res(k)`, built on demand. + products: DashMap>>, +} + +impl ExtAlgebra { + /// Build the ring $\Ext(k, k)$ from a resolution of `k`. + pub fn new(resolution: Arc) -> Self { + Self { + resolution, + products: DashMap::new(), + } + } + + /// The resolution of `k` backing this ring. + pub fn resolution(&self) -> &Arc { + &self.resolution + } + + pub fn prime(&self) -> ValidPrime { + self.resolution.prime() + } + + /// Ensure the resolution is computed through the given bidegree. + pub fn compute_through_bidegree(&self, b: Bidegree) { + self.resolution.compute_through_bidegree(b); + } + + /// The dimension of $\Ext^{s,t}(k, 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(k, 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(k, 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(k, 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(), ambient) + } +} + +impl ExtAlgebra +where + CC: FreeChainComplex + AugmentedChainComplex, +{ + /// The multiplication map for a single generator `g` of $\Ext(k, k)$ (`res(k) → res(k)`), built + /// and cached on first use. The returned map is *not* guaranteed to be extended. + pub fn generator_product_map( + &self, + g: BidegreeGenerator, + ) -> Arc> { + cached_generator_product_map(&self.products, &self.resolution, &self.resolution, g) + } + + /// The multiply-by-`x` chain self-map of `res(k)` (`res(k) → res(k)`), extended through `max`. + /// + /// This is the single home for the ring-side multiplication maps that Massey products need + /// (`massey_b_hom`). For a single generator it returns the cached + /// [`generator_product_map`](Self::generator_product_map); for a general class it *adds* the + /// cached generator maps via [`ResolutionHomomorphism::linear_combination`] (no quasi-inverse + /// lift). The degenerate zero class falls back to [`ResolutionHomomorphism::from_class`]. + pub fn class_product_map( + &self, + x: &BidegreeElement, + max: Bidegree, + ) -> Arc> { + let nonzero: Vec<(usize, u32)> = x.vec().iter_nonzero().collect(); + match nonzero.as_slice() { + [(idx, 1)] => { + let map = self.generator_product_map(BidegreeGenerator::new(x.degree(), *idx)); + map.extend_through_stem(max); + map + } + [_, _, ..] => { + let summands: Vec<(u32, Arc>)> = nonzero + .iter() + .map(|&(idx, c)| { + let map = + self.generator_product_map(BidegreeGenerator::new(x.degree(), idx)); + map.extend_through_stem(max); + (c, map) + }) + .collect(); + Arc::new(ResolutionHomomorphism::linear_combination( + String::new(), + &summands, + max, + )) + } + _ => { + // Zero class, or a single generator with coefficient != 1. + let coords: Vec = x.vec().iter().collect(); + let hom = Arc::new(ResolutionHomomorphism::from_class( + String::new(), + Arc::clone(&self.resolution), + Arc::clone(&self.resolution), + x.degree(), + &coords, + )); + hom.extend_through_stem(max); + hom + } + } + } + + /// Left-multiplication by `x ∈ Ext(k, k)`, applied to every basis generator of $\Ext(k, k)$ at + /// bidegree `b`. See [`ExtModule::multiply_into`] for the return convention. + pub fn multiply_into(&self, x: &BidegreeElement, b: Bidegree) -> Option { + products_into( + &self.resolution, + &self.resolution, + &self.products, + self.prime(), + x, + b, + ) + } + + /// The ring product `x · y` (both in $\Ext(k, k)$) if it lies in the computed range, else + /// `None`. The result lies in bidegree `x.degree() + y.degree()`. + pub fn try_multiply( + &self, + x: &BidegreeElement, + y: &BidegreeElement, + ) -> Option { + let matrix = self.multiply_into(x, y.degree())?; + Some(combine_product( + &matrix, + y, + x.degree() + y.degree(), + self.prime(), + )) + } + + /// The ring product `x · y`, both in $\Ext(k, k)$. Panics if 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", + ) + } +} + +/// The module $\Ext(M, k)$ over the ring [`ExtAlgebra`] $\Ext(k, k)$, backed by a resolution of +/// `M`. +/// +/// See the [module-level documentation](self) for conventions. +pub struct ExtModule { + /// Resolution of `M`; the module's classes and the module-action 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. + /// Shared handle to the ring $\Ext(k, k)$. `Arc`-shared so all modules over the same `k` reuse + /// one ring cache. + algebra: Arc>, + /// One multiplication map per generator of $\Ext(M, k)$, `res(M) → res(k)`, built on demand. products: DashMap>>, } impl ExtAlgebra { - /// Build an [`ExtAlgebra`] from a resolution, deriving the unit via [`get_unit`]. + /// Ensure the resolution of `k` is computed through the given stem. + pub fn compute_through_stem(&self, max: Bidegree) { + self.resolution.compute_through_stem(max); + } +} + +impl ExtModule { + /// Build an [`ExtModule`] from a resolution of `M`, deriving the unit `k` 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. + /// non-interactive setup, use [`ExtModule::new`] with an explicit ring instead. pub fn from_resolution(resolution: Arc) -> anyhow::Result { let (_, unit) = get_unit(Arc::clone(&resolution))?; - Ok(Self::new(resolution, unit)) + Ok(Self::new(resolution, Arc::new(ExtAlgebra::new(unit)))) } - /// Ensure both the resolution and the unit are computed through the given stem. + /// Ensure both the module's resolution and the ring's resolution 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.algebra.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. - pub fn new(resolution: Arc, unit: Arc) -> Self { - assert_eq!(resolution.prime(), unit.prime()); +impl ExtModule { + /// Build $\Ext(M, k)$ from a resolution of `M` and the ring $\Ext(k, k)$. + pub fn new(resolution: Arc, algebra: Arc>) -> Self { + assert_eq!(resolution.prime(), algebra.prime()); Self { - is_unit: Arc::ptr_eq(&resolution, &unit), resolution, - unit, + algebra, products: DashMap::new(), } } - /// 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. + /// Build the module `M == k`, i.e. $\Ext(k, k)$ as a module over itself, sharing one resolution + /// (and hence one ring cache) between the module and its ring. + pub fn over_unit(algebra: Arc>) -> Self { + let resolution = Arc::clone(algebra.resolution()); + Self::new(resolution, algebra) + } + + /// Build a module for resolution-*intrinsic* operations that do not involve the unit (notably + /// the secondary `d2` differential), using the resolution itself as its own `k`. /// /// 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) + /// [`from_resolution`](Self::from_resolution) performs. The product/action methods and the + /// ring-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 intrinsic(resolution: Arc) -> Self { + let algebra = Arc::new(ExtAlgebra::new(Arc::clone(&resolution))); + Self::new(resolution, algebra) } + /// The resolution of `M` backing this module. pub fn resolution(&self) -> &Arc { &self.resolution } - pub fn unit(&self) -> &Arc { - &self.unit + /// The ring $\Ext(k, k)$ this is a module over. + pub fn algebra(&self) -> &Arc> { + &self.algebra } + /// Whether `M == k`, i.e. the module shares its resolution with its ring. This is the structural + /// replacement for the old `is_unit` flag. pub fn is_unit(&self) -> bool { - self.is_unit + Arc::ptr_eq(&self.resolution, self.algebra.resolution()) } pub fn prime(&self) -> ValidPrime { self.resolution.prime() } - /// Ensure both the resolution and the unit are computed through the given bidegree. + /// Ensure both the module's resolution and the ring's resolution 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.algebra.compute_through_bidegree(b); + if !self.is_unit() { self.resolution.compute_through_bidegree(b); } } @@ -136,64 +332,27 @@ impl ExtAlgebra { 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())) - } - - /// 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 { - assert_eq!(self.unit_dimension(b), coords.len()); - BidegreeElement::new(b, FpVector::from_slice(self.prime(), coords)) - } - - /// A single generator of $\Ext(k, k)$ as a class. - pub fn unit_generator(&self, g: BidegreeGenerator) -> BidegreeElement { - let ambient = self.unit_dimension(g.degree()); - assert!(ambient > g.idx()); g.into_element(self.prime(), ambient) } } -impl ExtAlgebra +impl ExtModule 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. + /// The multiplication map for a single generator `g` of $\Ext(M, k)$ (`res(M) → res(k)`), built + /// and cached on first use. The returned map is *not* guaranteed to be extended; + /// [`multiply_into`](Self::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()) + cached_generator_product_map( + &self.products, + &self.resolution, + self.algebra.resolution(), + g, + ) } /// Left-multiplication by the class `x` (in $\Ext(M, k)$), applied to every basis generator of @@ -206,48 +365,31 @@ where /// `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) + products_into( + &self.resolution, + self.algebra.resolution(), + &self.products, + self.prime(), + x, + b, + ) } /// 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()`. + /// [`multiply_into`](Self::multiply_into) for the operand conventions (`x ∈ Ext(M, k)`, `y ∈ + /// Ext(k, k)`). 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)) + Some(combine_product( + &matrix, + y, + x.degree() + y.degree(), + self.prime(), + )) } /// The product `x · y`, where `x ∈ Ext(M, k)` and `y ∈ Ext(k, k)`. When `M == k` both operands @@ -262,6 +404,229 @@ where } } +/// Build/cache the per-generator product map `res(source) → res(target)` for generator `g`. +fn cached_generator_product_map( + products: &DashMap>>, + source: &Arc, + target: &Arc, + g: BidegreeGenerator, +) -> Arc> +where + CC: FreeChainComplex + AugmentedChainComplex, +{ + if let Some(map) = products.get(&g) { + return Arc::clone(&map); + } + + let dim = source.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(source), + Arc::clone(target), + g.degree(), + &class, + )); + + Arc::clone(products.entry(g).or_insert(hom).value()) +} + +/// The shared body of `multiply_into`: left-multiplication by `x` (a class in `Ext(source, k)`) +/// applied to every generator of `Ext(target, k)` at bidegree `b`. Products land in `Ext(source, +/// k)` at `b + x.degree()`. Returns `None` when out of the computed range. +fn products_into( + source: &Arc, + target: &Arc, + products: &DashMap>>, + prime: ValidPrime, + x: &BidegreeElement, + b: Bidegree, +) -> Option +where + CC: FreeChainComplex + AugmentedChainComplex, +{ + let shift = x.degree(); + let result_deg = b + shift; + + if !target.has_computed_bidegree(b) || !source.has_computed_bidegree(result_deg) { + return None; + } + + let mult_dim = target.number_of_gens_in_bidegree(b); + let res_dim = source.number_of_gens_in_bidegree(result_deg); + let mut matrix = Matrix::new(prime, mult_dim, res_dim); + + for (i, c) in x.vec().iter_nonzero() { + let map = cached_generator_product_map( + products, + source, + target, + BidegreeGenerator::new(shift, i), + ); + map.extend_all(); + + // `hom_k(b.t())[j][k]`: `j` indexes the multiplicand generator of `Ext(k, k)` at `b`, `k` + // indexes the result generator of `Ext(source, k)` at `result_deg`. + let hom_k = map.get_map(result_deg.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) +} + +/// Combine the per-generator product `matrix` (rows indexed by generators of `y`'s bidegree) with +/// the coordinates of `y` into the class `x · y` at bidegree `target`. +fn combine_product( + matrix: &Matrix, + y: &BidegreeElement, + target: Bidegree, + prime: ValidPrime, +) -> BidegreeElement { + let mut out = FpVector::new(prime, matrix.columns()); + for (j, c) in y.vec().iter_nonzero() { + out.as_slice_mut().add(matrix.row(j), c); + } + BidegreeElement::new(target, out) +} + +impl std::fmt::Display for ExtAlgebra { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Ext(k, k)") + } +} + +impl std::fmt::Display for ExtModule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Ext(M, k)") + } +} + +impl algebra::Algebra<2> for ExtAlgebra +where + CC: FreeChainComplex + AugmentedChainComplex + 'static, +{ + fn prime(&self) -> ValidPrime { + self.resolution.prime() + } + + fn compute_basis(&self, degree: impl Into) { + self.compute_through_bidegree(degree.into()); + } + + fn dimension(&self, degree: impl Into) -> usize { + self.resolution.number_of_gens_in_bidegree(degree.into()) + } + + /// The ring product of two generators, **computed up to the Koszul sign** (see the + /// [module-level docs](self)): the underlying [`multiply`](ExtAlgebra::multiply) is sign-exact + /// only at `p = 2`. At odd primes the result may differ from the true product by + /// $(-1)^{|r||s|}$, so do not wire this trait method into odd-prime machinery that depends on + /// the exact sign. + fn multiply_basis_elements( + &self, + mut result: fp::vector::FpSliceMut, + coeff: u32, + r_degree: impl Into, + r_idx: usize, + s_degree: impl Into, + s_idx: usize, + ) { + let r = self.generator(BidegreeGenerator::new(r_degree.into(), r_idx)); + let s = self.generator(BidegreeGenerator::new(s_degree.into(), s_idx)); + let prod = self.multiply(&r, &s); + result.add(prod.vec(), coeff); + } + + fn basis_element_to_string(&self, degree: impl Into, idx: usize) -> String { + let degree = degree.into(); + format!("x_{{{},{}}}^{}", degree.n(), degree.s(), idx) + } + + fn basis_element_from_string(&self, _elt: &str) -> Option<(i32, usize)> { + // A single `i32` degree cannot encode a bidegree, so string parsing is unsupported for Ext. + None + } +} + +impl algebra::module::Module<2> for ExtModule +where + CC: FreeChainComplex + AugmentedChainComplex + 'static, +{ + type Algebra = ExtAlgebra; + + fn algebra(&self) -> Arc { + Arc::clone(&self.algebra) + } + + /// The unit module (`M == k`) in the sense of the `Ext(k, k)`-module category — the module that + /// shares its resolution with the ring. This delegates to the inherent + /// [`ExtModule::is_unit`](ExtModule::is_unit) so the trait and inherent methods agree (the + /// generic `Module::is_unit` default, which inspects `min_degree`/`max_degree`, is meaningless + /// for a bigraded `Ext`). + fn is_unit(&self) -> bool { + // Method-call syntax resolves to the inherent `ExtModule::is_unit` (inherent methods take + // priority over trait methods), so this is the `Arc::ptr_eq` check, not a recursion. + self.is_unit() + } + + /// `Ext` is bigraded, so there is no single meaningful `t`-bound. Per the crate's convention + /// (degree-*returning* trait methods report the filtration `s` axis), this returns the `s` + /// lower bound `0`, **not** a `t`-bound as the [`Module`](algebra::module::Module) trait's prose + /// suggests. The generic `try_act_*`/`total_dimension` defaults that consume it are not used on + /// `Ext`. + fn min_degree(&self) -> i32 { + 0 + } + + /// The maximum filtration `s` for which the resolution of `M` is defined (an `s`-bound, not the + /// `t`-bound the [`Module`](algebra::module::Module) trait's prose describes — see the + /// `min_degree` note above for the bigraded convention). + fn max_computed_degree(&self) -> i32 { + self.resolution.next_homological_degree() - 1 + } + + fn compute_basis_multi(&self, degree: Bidegree) { + self.compute_through_bidegree(degree); + } + + fn dimension_multi(&self, degree: Bidegree) -> usize { + self.dimension(degree) + } + + /// The action of a ring element `op ∈ Ext(k, k)` on a module element `mod ∈ Ext(M, k)`, + /// **computed up to the Koszul sign**. The trait models a *left* action `op · mod`, but this is + /// realised as `mod · op` (the only direction [`ExtModule::multiply`] supports), which equals + /// `op · mod` up to $(-1)^{|op||mod|}$ — **exact only at `p = 2`**. Do not consume this trait + /// action in odd-prime machinery that depends on the exact sign; use it at `p = 2` or where the + /// sign is irrelevant. + fn act_on_basis_multi( + &self, + mut result: fp::vector::FpSliceMut, + coeff: u32, + op_degree: Bidegree, + op_index: usize, + mod_degree: Bidegree, + mod_index: usize, + ) { + let op = self + .algebra + .generator(BidegreeGenerator::new(op_degree, op_index)); + let mod_elt = self.generator(BidegreeGenerator::new(mod_degree, mod_index)); + let prod = self.multiply(&mod_elt, &op); + result.add(prod.vec(), coeff); + } + + fn basis_element_to_string_multi(&self, degree: Bidegree, idx: usize) -> String { + format!("x_{{{},{}}}^{}", degree.n(), degree.s(), idx) + } +} + #[cfg(test)] mod tests { use super::*; @@ -271,33 +636,162 @@ 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); + let module = ExtModule::intrinsic(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)); - let h1 = alg.generator(BidegreeGenerator::new(Bidegree::n_s(1, 1), 0)); + let h0 = module.generator(BidegreeGenerator::new(Bidegree::n_s(0, 1), 0)); + let h1 = module.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); + let h0_sq = module.multiply(&h0, &h0); assert_eq!(h0_sq.degree(), Bidegree::n_s(0, 2)); - assert_eq!(alg.dimension(Bidegree::n_s(0, 2)), 1); + assert_eq!(module.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(), + module.multiply(&h0, &h1).vec().is_zero(), "h_0 h_1 should vanish" ); assert!( - alg.multiply(&h1, &h0).vec().is_zero(), + module.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 + let rows = module .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); } + + /// The bigraded `Algebra<2>` / `Module<2>` trait methods must agree with the inherent product. + /// At `p = 2` there is no sign ambiguity, so `Algebra::multiply_basis_elements`, + /// `Module::act_on_basis_multi`, and the inherent `multiply` all coincide (`M == k` here, so the + /// ring generator and the module generator are the same class). + #[test] + fn test_trait_surface() { + use algebra::{Algebra as _, module::Module as _}; + + let res = Arc::new(construct_standard::("S_2", None).unwrap()); + res.compute_through_stem(Bidegree::n_s(4, 4)); + let module = ExtModule::intrinsic(res); + let algebra = module.algebra(); + + let h0 = Bidegree::n_s(0, 1); + let target = Bidegree::n_s(0, 2); + let p = algebra.prime(); + + // Inherent product h_0 · h_0 (the reference). + let g = algebra.generator(BidegreeGenerator::new(h0, 0)); + let inherent: Vec = algebra.multiply(&g, &g).vec().iter().collect(); + assert_eq!(inherent.len(), algebra.dimension(target)); + + // `Algebra::<2>::multiply_basis_elements`. + let mut ring = FpVector::new(p, algebra.dimension(target)); + algebra.multiply_basis_elements(ring.as_slice_mut(), 1, h0, 0, h0, 0); + assert_eq!(ring.iter().collect::>(), inherent); + + // `Module::<2>::act_on_basis_multi` (op = h_0 in the ring acting on mod = h_0 in the module). + let mut act = FpVector::new(p, module.dimension(target)); + module.act_on_basis_multi(act.as_slice_mut(), 1, h0, 0, h0, 0); + assert_eq!(act.iter().collect::>(), inherent); + } + + /// Exercise the `M != k` path (the whole point of the split): products of `Ext(M, k)` classes + /// use `source = res(M)`, `target = res(k)`, a distinction the `M == k` tests never hit. The + /// unit `1 ∈ Ext^{0,0}(k, k)` acts trivially, so `x · 1 = x` for any `x ∈ Ext(M, k)`. + #[test] + fn test_non_unit_products() { + let max = Bidegree::n_s(8, 8); + let unit = Arc::new(construct_standard::("S_2", None).unwrap()); + let m = Arc::new(construct_standard::("C2", None).unwrap()); + unit.compute_through_stem(max); + m.compute_through_stem(max); + let module = ExtModule::new(m, Arc::new(ExtAlgebra::new(unit))); + assert!(!module.is_unit(), "C2 is not the sphere, so M != k"); + + // The unit class 1 ∈ Ext^{0,0}(k, k). + let unit_deg = Bidegree::n_s(0, 0); + assert_eq!(module.algebra().dimension(unit_deg), 1); + let one = module + .algebra() + .generator(BidegreeGenerator::new(unit_deg, 0)); + + // The bottom class of Ext(C2, k) at (0, 0); x · 1 = x. + assert_eq!(module.dimension(Bidegree::n_s(0, 0)), 1); + let x = module.generator(BidegreeGenerator::new(Bidegree::n_s(0, 0), 0)); + let prod = module.multiply(&x, &one); + assert_eq!(prod.degree(), x.degree()); + assert_eq!( + prod.vec().iter().collect::>(), + x.vec().iter().collect::>(), + "x · 1 = x" + ); + } + + /// Exercise an odd prime (`p = 3`), the regime where the product API's up-to-Koszul-sign caveat + /// bites. We assert only the sign-robust fact that `a_0^2 != 0` (the `a_0`-Bockstein tower). + #[test] + fn test_odd_prime_products() { + let res = Arc::new(construct_standard::("S_3", None).unwrap()); + res.compute_through_stem(Bidegree::n_s(4, 4)); + let module = ExtModule::intrinsic(res); + + // a_0 ∈ Ext^{1,1}(F_3, F_3) at (n = 0, s = 1); a_0^2 ∈ Ext^{2,2} at (0, 2) is nonzero. + let a0_deg = Bidegree::n_s(0, 1); + assert_eq!(module.dimension(a0_deg), 1); + let a0 = module.generator(BidegreeGenerator::new(a0_deg, 0)); + let a0_sq = module.multiply(&a0, &a0); + assert_eq!(a0_sq.degree(), Bidegree::n_s(0, 2)); + assert!( + !a0_sq.vec().is_zero(), + "a_0^2 should be nonzero at p = 3 (up to sign)" + ); + } + + /// `class_product_map` on a multi-generator class assembles the multiply-by-a-class map by + /// *adding* the cached per-generator maps at the chain level + /// ([`ResolutionHomomorphism::linear_combination`]). This must induce the same products as + /// [`ExtModule::multiply_into`], which instead sums the per-generator maps at the `hom_k` level. + /// The two independent linear-combination strategies agreeing pins `linear_combination`. + #[test] + fn test_class_product_map_matches_multiply_into() { + let max = Bidegree::n_s(20, 9); + let res = Arc::new(construct_standard::("S_2", None).unwrap()); + res.compute_through_stem(max); + let module = ExtModule::intrinsic(res); + let algebra = module.algebra(); + + // (n = 15, s = 5) is the first bidegree of Ext(F_2, F_2) with two generators, so this + // exercises the genuine multi-generator `linear_combination` path. + let x_deg = Bidegree::n_s(15, 5); + assert_eq!( + algebra.dimension(x_deg), + 2, + "expected a 2-dimensional bidegree" + ); + let x = algebra.element(x_deg, &[1, 1]); + + let map = algebra.class_product_map(&x, max); + map.extend_all(); + + let mut compared = 0; + for b in algebra.resolution().iter_nonzero_stem() { + // `multiply_into` returns `None` once `b + x_deg` is out of the computed range. + let Some(reference) = module.multiply_into(&x, b) else { + continue; + }; + let target = b + x_deg; + let hom_k = map.get_map(target.s()).hom_k(b.t()); + assert_eq!(reference.rows(), hom_k.len()); + for (j, row) in hom_k.iter().enumerate() { + let via_ref: Vec = reference.row(j).iter().collect(); + assert_eq!(&via_ref, row, "product mismatch at multiplicand {b}"); + compared += 1; + } + } + assert!(compared > 0, "expected at least one product comparison"); + } } diff --git a/ext/src/ext_algebra/secondary.rs b/ext/src/ext_algebra/secondary.rs index 983a7c60ac..bb2cba82d8 100644 --- a/ext/src/ext_algebra/secondary.rs +++ b/ext/src/ext_algebra/secondary.rs @@ -1,6 +1,6 @@ -//! The secondary ($d_2$) layer of [`ExtAlgebra`]. +//! The secondary ($d_2$) layer of [`ExtModule`]. //! -//! [`SecondaryExtAlgebra`] composes an [`ExtAlgebra`] with the secondary resolutions of `M` and +//! [`SecondaryExtAlgebra`] composes an [`ExtModule`] 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)), @@ -9,7 +9,7 @@ //! [`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 +//! algebra is implemented here. The layer is split out from [`ExtModule`] because the secondary //! machinery requires `CC::Algebra: PairAlgebra`, a bound the primary layer does not impose. use std::sync::{Arc, Mutex}; @@ -19,7 +19,7 @@ use dashmap::DashMap; use fp::{matrix::Subquotient, prime::Prime, vector::FpVector}; use sseq::coordinates::{Bidegree, BidegreeElement}; -use super::ExtAlgebra; +use super::ExtModule; use crate::{ chain_complex::FreeChainComplex, resolution_homomorphism::ResolutionHomomorphism, @@ -40,13 +40,13 @@ pub struct SecondaryProduct { pub lambda_part: FpVector, } -/// The secondary layer over an [`ExtAlgebra`]: the $d_2$ differential and the $\Mod_{C\lambda^2}$ +/// The secondary layer over an [`ExtModule`]: 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>, + module: Arc>, res_lift: Arc>, /// `Arc`-shared with `res_lift` when `M == k`. unit_lift: Arc>, @@ -62,17 +62,19 @@ impl SecondaryExtAlgebra where CC::Algebra: PairAlgebra, { - /// Build the secondary layer over `alg`. Construction is cheap; call [`extend_all`](Self::extend_all) + /// Build the secondary layer over `module`. 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() { + pub fn new(module: Arc>) -> Self { + let res_lift = Arc::new(SecondaryResolution::new(Arc::clone(module.resolution()))); + let unit_lift = if module.is_unit() { Arc::clone(&res_lift) } else { - Arc::new(SecondaryResolution::new(Arc::clone(alg.unit()))) + Arc::new(SecondaryResolution::new(Arc::clone( + module.algebra().resolution(), + ))) }; Self { - alg, + module, res_lift, unit_lift, res_sseq: Mutex::new(None), @@ -86,12 +88,12 @@ where /// [`secondary_multiply_into`](Self::secondary_multiply_into). pub fn extend_all(&self) { self.res_lift.extend_all(); - if !self.alg.is_unit() { + if !self.module.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() { + let unit = if self.module.is_unit() { Arc::clone(self.res_sseq.lock().unwrap().as_ref().unwrap()) } else { Arc::new(self.unit_lift.e3_page()) @@ -104,18 +106,18 @@ where /// 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() { + if !self.module.is_unit() { self.unit_lift.compute_partial(s); } } - /// The primary [`ExtAlgebra`] this is built on. - pub fn ext_algebra(&self) -> &Arc> { - &self.alg + /// The primary [`ExtModule`] this is built on. + pub fn module(&self) -> &Arc> { + &self.module } fn prime(&self) -> fp::prime::ValidPrime { - self.alg.prime() + self.module.prime() } /// The secondary differential $d_2(x)$, a class in bidegree `(n - 1, s + 2)`. @@ -189,8 +191,8 @@ where let name = format!("prod_{x}",); let underlying = Arc::new(ResolutionHomomorphism::from_class( name, - Arc::clone(self.alg.resolution()), - Arc::clone(self.alg.unit()), + Arc::clone(self.module.resolution()), + Arc::clone(self.module.algebra().resolution()), x.degree(), &x.vec().iter().collect::>(), )); @@ -229,9 +231,12 @@ where .expect("call extend_all() first"), ); - let ext_dim = self.alg.resolution().number_of_gens_in_bidegree(b + shift); + let ext_dim = self + .module + .resolution() + .number_of_gens_in_bidegree(b + shift); let lambda_dim = self - .alg + .module .resolution() .number_of_gens_in_bidegree(b + shift + LAMBDA_BIDEGREE); @@ -276,7 +281,7 @@ mod tests { 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 e2 = Arc::new(ExtModule::intrinsic(res)); let sec_e2 = SecondaryExtAlgebra::new(Arc::clone(&e2)); sec_e2.extend_all(); diff --git a/ext/src/nassau.rs b/ext/src/nassau.rs index 8d1919136a..4e78afb631 100644 --- a/ext/src/nassau.rs +++ b/ext/src/nassau.rs @@ -22,7 +22,7 @@ use algebra::{ Algebra, combinatorics, milnor_algebra::{MilnorAlgebra, PPartEntry}, module::{ - FreeModule, GeneratorData, Module, ZeroModule, + FreeModule, GeneratorData, Module, ModuleExt, ZeroModule, homomorphism::{FreeModuleHomomorphism, FullModuleHomomorphism, ModuleHomomorphism}, }, }; diff --git a/ext/src/resolution.rs b/ext/src/resolution.rs index d37edd20ed..ac459a5461 100644 --- a/ext/src/resolution.rs +++ b/ext/src/resolution.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex, mpsc}; use algebra::{ Algebra, MuAlgebra, module::{ - Module, MuFreeModule, + ModuleExt, MuFreeModule, homomorphism::{ModuleHomomorphism, MuFreeModuleHomomorphism}, }, }; @@ -985,7 +985,10 @@ where pub(crate) mod secondary { use std::sync::Arc; - use algebra::{module::Module, pair_algebra::PairAlgebra}; + use algebra::{ + module::{Module, ModuleExt}, + pair_algebra::PairAlgebra, + }; use dashmap::DashMap; use fp::vector::FpVector; use once::OnceBiVec; diff --git a/ext/src/resolution_homomorphism.rs b/ext/src/resolution_homomorphism.rs index 271c7318d3..b1274492d5 100644 --- a/ext/src/resolution_homomorphism.rs +++ b/ext/src/resolution_homomorphism.rs @@ -5,7 +5,7 @@ use std::{ops::Range, sync::Arc}; use algebra::{ MuAlgebra, module::{ - Module, + Module, ModuleExt, homomorphism::{ModuleHomomorphism, MuFreeModuleHomomorphism}, }, }; @@ -161,6 +161,88 @@ where )); } + /// Build the chain map `∑_i coeff_i · summand_i` from chain maps that share a source, target, + /// and shift. + /// + /// Every summand must already be defined at least as far as `max` (extend it first). The + /// combined map's image on each generator is the corresponding linear combination of the + /// summands' images, so *no* quasi-inverse lift is performed — assembling a multiply-by-a-class + /// map from cached per-generator maps is just adding maps. This is the cheap alternative to + /// [`from_class`](Self::from_class) used by + /// [`ExtAlgebra::class_product_map`](crate::ext_algebra::ExtAlgebra::class_product_map). + /// + /// Call with an **empty `name`**: this populates the maps directly (not through + /// [`extend_step_raw`](Self::extend_step_raw)), so a non-empty name on a save-enabled source + /// would create a `products/{name}` save directory that is never written to. + pub fn linear_combination(name: String, summands: &[(u32, Arc)], max: Bidegree) -> Self { + assert!( + !summands.is_empty(), + "linear_combination requires at least one summand" + ); + assert!( + name.is_empty(), + "linear_combination populates maps directly and never writes to a save directory; \ + pass an empty name to avoid creating a `products/{{name}}` directory that stays empty" + ); + let shift = summands[0].1.shift; + let source = Arc::clone(&summands[0].1.source); + let target = Arc::clone(&summands[0].1.target); + for (_, m) in summands { + assert_eq!( + m.shift, shift, + "linear_combination summands must share a shift" + ); + assert!( + Arc::ptr_eq(&m.source, &source), + "linear_combination summands must share a source" + ); + assert!( + Arc::ptr_eq(&m.target, &target), + "linear_combination summands must share a target" + ); + } + + let p = source.prime(); + let result = Self::new(name, source, target, shift); + + // The combined map is defined wherever every summand is. Iterate `(s, t)` in the same order + // the summands were built and sum their generator images. + let max_s = summands + .iter() + .map(|(_, m)| m.next_homological_degree()) + .min() + .unwrap() + .min(max.s() + 1); + + for input_s in shift.s()..max_s { + let out_map = result.get_map_ensure_length(input_s); + let src_mod = out_map.source(); + let tgt_mod = out_map.target(); + let summand_maps: Vec<_> = summands + .iter() + .map(|(c, m)| (*c, m.get_map(input_s))) + .collect(); + let next_t = summand_maps + .iter() + .map(|(_, m)| m.next_degree()) + .min() + .unwrap(); + + for t in out_map.next_degree()..next_t { + let num_gens = src_mod.number_of_gens_in_degree(t); + let dim = tgt_mod.dimension(t - shift.t()); + let mut rows = vec![FpVector::new(p, dim); num_gens]; + for (c, m) in &summand_maps { + for (k, row) in rows.iter_mut().enumerate() { + row.add(m.output(t, k), *c); + } + } + out_map.add_generators_from_rows(t, rows); + } + } + result + } + /// Extends the resolution homomorphism up to a given range. This range is first specified by /// the maximum `s`, then the maximum `t` for each `s`. This should rarely be used directly; /// instead one should use [`MuResolutionHomomorphism::extend`], @@ -501,7 +583,7 @@ pub(crate) mod secondary { use std::sync::Arc; use algebra::{ - module::{Module, homomorphism::ModuleHomomorphism}, + module::{ModuleExt, homomorphism::ModuleHomomorphism}, pair_algebra::PairAlgebra, }; use dashmap::DashMap; diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index e04b852a36..aec6fb9817 100644 --- a/ext/src/secondary.rs +++ b/ext/src/secondary.rs @@ -3,7 +3,7 @@ use std::{io, sync::Arc}; use algebra::{ Algebra, module::{ - FreeModule, Module, + FreeModule, Module, ModuleExt, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, }, pair_algebra::PairAlgebra, diff --git a/ext/src/yoneda.rs b/ext/src/yoneda.rs index 2fffe713c9..89f213bece 100644 --- a/ext/src/yoneda.rs +++ b/ext/src/yoneda.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use algebra::{ AdemAlgebra, Algebra, GeneratedAlgebra, MilnorAlgebra, SteenrodAlgebra, module::{ - FDModule, FreeModule, Module, QuotientModule as QM, + FDModule, FreeModule, Module, ModuleExt, QuotientModule as QM, homomorphism::{ FreeModuleHomomorphism, FullModuleHomomorphism, IdentityHomomorphism, ModuleHomomorphism, QuotientHomomorphism, QuotientHomomorphismSource, diff --git a/ext/tests/extend_identity.rs b/ext/tests/extend_identity.rs index 122967fc51..da18d06f7a 100644 --- a/ext/tests/extend_identity.rs +++ b/ext/tests/extend_identity.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use algebra::module::{ - Module, + Module, ModuleExt, homomorphism::{FullModuleHomomorphism, IdentityHomomorphism}, }; use ext::{ diff --git a/ext/tests/non_zero_min_degree.rs b/ext/tests/non_zero_min_degree.rs index 20de0f080c..aa1b414f73 100644 --- a/ext/tests/non_zero_min_degree.rs +++ b/ext/tests/non_zero_min_degree.rs @@ -1,4 +1,4 @@ -use algebra::module::Module; +use algebra::module::ModuleExt; use ext::{ chain_complex::{AugmentedChainComplex, ChainComplex}, utils::construct, diff --git a/web_ext/sseq_gui/src/actions.rs b/web_ext/sseq_gui/src/actions.rs index e1f58774d9..0184c2bd77 100644 --- a/web_ext/sseq_gui/src/actions.rs +++ b/web_ext/sseq_gui/src/actions.rs @@ -1,4 +1,4 @@ -use algebra::module::Module; +use algebra::module::{Module, ModuleExt}; use bivec::BiVec; use enum_dispatch::enum_dispatch; use ext::{CCC, chain_complex::FreeChainComplex};