From d5cb501ac30bbce30851fb7c1f3b2d7535e75892 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 20:38:11 +0000 Subject: [PATCH 1/7] Genericize the algebra crate over the base ring: Ring and GradedDvr The homological algebra was hardcoded to coefficients in the field F_p via the bit-packed `fp` crate. Introduce a base-ring abstraction so it can later run over any graded coefficient ring (e.g. F_2[tau] for C-motivic Ext) while keeping the classical F_p path bit-identical. Two coefficient-ring traits, cut so that defining an algebra needs strictly less than resolving over it: - `Ring` (algebra/base_ring.rs): the coefficient ring -- scalars, their arithmetic, `embed_field` for the F_p inclusion, and the representation of finite free modules over the ring (the `Vector`/`Slice`/`SliceMut` types and their ops). This is enough to define an algebra and act on its modules. - `GradedDvr` (linear_algebra/mod.rs): extends `Ring` with the linear algebra of *solving* -- images, kernels and quasi-inverses -- which is tractable precisely because the ring is a graded DVR (graded Nakayama). It is the base-ring-generic replacement for the `fp` row reduction the engine hardcodes. `Algebra` requires only `type BaseRing: Ring`, so an algebra can be defined over a ring whose solving linear algebra is not yet implemented; the stronger `GradedDvr` bound is imposed where resolution actually happens (`ModuleHomomorphism`, `FreeModuleHomomorphism`, and the types that store them). `Field` implements both traits by forwarding to `fp`, bottoming the recursion at itself. Module and homomorphism signatures are threaded over the base-ring scalar and the `BaseSlice` projections. `FreeModule`/`FreeModuleHomomorphism` store their outputs/kernels/images/quasi-inverses generically over the base ring; the remaining fp-matrix-backed module types stay pinned with `BaseRing = Field` and use the scalar coefficient directly, so relaxing a pin becomes a compile error to resolve rather than a silent bug. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNkiiZghHggyMwDWfDn1y5 --- .../algebra/src/algebra/adem_algebra.rs | 8 +- .../algebra/src/algebra/algebra_trait.rs | 200 +++--- ext/crates/algebra/src/algebra/base_ring.rs | 138 ++++ ext/crates/algebra/src/algebra/field.rs | 10 +- .../algebra/src/algebra/milnor_algebra.rs | 8 +- ext/crates/algebra/src/algebra/mod.rs | 3 + .../algebra/src/algebra/steenrod_algebra.rs | 37 +- ext/crates/algebra/src/lib.rs | 9 +- ext/crates/algebra/src/linear_algebra/mod.rs | 660 ++++++++++++++++++ .../src/module/finite_dimensional_module.rs | 49 +- .../src/module/finitely_presented_module.rs | 30 +- ext/crates/algebra/src/module/free_module.rs | 15 +- .../homomorphism/free_module_homomorphism.rs | 137 ++-- .../homomorphism/full_module_homomorphism.rs | 18 +- .../homomorphism/generic_zero_homomorphism.rs | 25 +- .../src/module/homomorphism/hom_pullback.rs | 31 +- .../algebra/src/module/homomorphism/mod.rs | 114 ++- .../homomorphism/quotient_homomorphism.rs | 19 +- ext/crates/algebra/src/module/module_trait.rs | 72 +- .../algebra/src/module/quotient_module.rs | 24 +- ext/crates/algebra/src/module/rpn.rs | 10 +- .../algebra/src/module/suspension_module.rs | 30 +- .../algebra/src/module/tensor_module.rs | 17 +- 23 files changed, 1359 insertions(+), 305 deletions(-) create mode 100644 ext/crates/algebra/src/algebra/base_ring.rs create mode 100644 ext/crates/algebra/src/linear_algebra/mod.rs diff --git a/ext/crates/algebra/src/algebra/adem_algebra.rs b/ext/crates/algebra/src/algebra/adem_algebra.rs index 100f42a506..882a5b29f3 100644 --- a/ext/crates/algebra/src/algebra/adem_algebra.rs +++ b/ext/crates/algebra/src/algebra/adem_algebra.rs @@ -16,7 +16,7 @@ use rustc_hash::FxHashMap as HashMap; #[cfg(doc)] use crate::algebra::SteenrodAlgebra; use crate::algebra::{ - Algebra, Bialgebra, GeneratedAlgebra, UnstableAlgebra, + Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, combinatorics::{self, MAX_XI_TAU}, }; @@ -160,6 +160,12 @@ impl fmt::Display for AdemAlgebra { } impl Algebra for AdemAlgebra { + type BaseRing = Field; + + fn base_ring(&self) -> Field { + Field::new(self.prime()) + } + fn prefix(&self) -> &str { "adem" } diff --git a/ext/crates/algebra/src/algebra/algebra_trait.rs b/ext/crates/algebra/src/algebra/algebra_trait.rs index cad1ac0f5f..fe1e60a255 100644 --- a/ext/crates/algebra/src/algebra/algebra_trait.rs +++ b/ext/crates/algebra/src/algebra/algebra_trait.rs @@ -1,12 +1,14 @@ use enum_dispatch::enum_dispatch; #[cfg(doc)] use fp::vector::FpVector; -use fp::{ - prime::ValidPrime, - vector::{FpSlice, FpSliceMut}, -}; +use fp::{prime::ValidPrime, vector::FpSlice}; use itertools::Itertools; +use crate::{ + algebra::{Ring, Scalar}, + linear_algebra::{BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf}, +}; + /// A graded algebra over $\mathbb{F}_p$. /// /// Each degree is finite dimensional, and equipped with a distinguished ordered basis. Basis @@ -20,8 +22,40 @@ use itertools::Itertools; /// this function before performing other operations at that degree. /// /// Algebras may have a distinguished set of generators; see [`GeneratedAlgebra`]. -#[enum_dispatch] +/// +/// # Base ring +/// +/// An algebra is linear over a coefficient ring, its [`BaseRing`](Algebra::BaseRing). Classically +/// this is the field $\mathbb{F}_p$ ([`Field`](crate::algebra::Field)); the C-motivic Steenrod +/// algebra is linear over $\mathbb{F}_2[\tau]$. The base ring is intrinsic to the algebra (its +/// products and module actions are $R$-linear and consume $R$-coefficients), so it is declared here +/// as an associated type rather than on a separate trait. +/// +/// This associated type is why [`Algebra`] itself is not `#[enum_dispatch]`ed (`enum_dispatch` +/// cannot handle a trait declaring an associated type); the [`SteenrodAlgebra`] enum's [`Algebra`] +/// impl is hand-rolled with a forwarding macro instead. The subtraits [`Bialgebra`], +/// [`GeneratedAlgebra`] and [`UnstableAlgebra`] declare no associated type and remain +/// `#[enum_dispatch]`ed, even where their methods consume base-ring coefficients. +/// +/// [`SteenrodAlgebra`]: crate::algebra::SteenrodAlgebra +/// [`Bialgebra`]: crate::algebra::Bialgebra pub trait Algebra: std::fmt::Display + Send + Sync + 'static { + /// The coefficient ring this algebra (and its modules) are linear over. + /// + /// This is bounded only by [`Ring`] — enough to represent and multiply elements, and to act on + /// modules. Resolving *over* the algebra additionally needs the base ring to be a + /// [`GradedDvr`](crate::linear_algebra::GradedDvr) (for kernels and quasi-inverses); that + /// stronger bound is imposed where resolution happens (e.g. [`ModuleHomomorphism`] and the chain + /// complex), not here, so that an algebra can be defined over a ring whose solving linear algebra + /// is not yet implemented. + /// + /// [`ModuleHomomorphism`]: crate::module::homomorphism::ModuleHomomorphism + type BaseRing: Ring; + + /// The base-ring handle (carrying any runtime data, e.g. the prime), used for coefficient + /// arithmetic and the linear algebra of resolving over this algebra. + fn base_ring(&self) -> Self::BaseRing; + /// 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 { @@ -60,8 +94,8 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// `result` is not required to be aligned. fn multiply_basis_elements( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_idx: usize, s_degree: i32, @@ -74,18 +108,18 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// Neither `result` nor `s` must be aligned. fn multiply_basis_element_by_element( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_idx: usize, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, ) { - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in s.iter_nonzero() { self.multiply_basis_elements( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), r_degree, r_idx, s_degree, @@ -100,18 +134,18 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// Neither `result` nor `r` must be aligned. fn multiply_element_by_basis_element( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, s_idx: usize, ) { - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in r.iter_nonzero() { self.multiply_basis_elements( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), r_degree, i, s_degree, @@ -126,18 +160,18 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// Neither `result`, `s`, nor `r` must be aligned. fn multiply_element_by_element( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, ) { - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in s.iter_nonzero() { self.multiply_element_by_basis_element( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), r_degree, r, s_degree, @@ -215,8 +249,8 @@ pub trait UnstableAlgebra: Algebra { fn multiply_basis_elements_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_index: usize, s_degree: i32, @@ -230,19 +264,19 @@ pub trait UnstableAlgebra: Algebra { /// Neither `result` nor `s` must be aligned. fn multiply_basis_element_by_element_unstable( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_idx: usize, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, excess: i32, ) { - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in s.iter_nonzero() { self.multiply_basis_elements_unstable( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), r_degree, r_idx, s_degree, @@ -258,19 +292,19 @@ pub trait UnstableAlgebra: Algebra { /// Neither `result` nor `r` must be aligned. fn multiply_element_by_basis_element_unstable( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, s_idx: usize, excess: i32, ) { - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in r.iter_nonzero() { self.multiply_basis_elements_unstable( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), r_degree, i, s_degree, @@ -286,19 +320,19 @@ pub trait UnstableAlgebra: Algebra { /// Neither `result`, `s`, nor `r` must be aligned. fn multiply_element_by_element_unstable( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, excess: i32, ) { - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in s.iter_nonzero() { self.multiply_element_by_basis_element_unstable( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), r_degree, r, s_degree, @@ -319,8 +353,8 @@ pub trait MuAlgebra: Algebra { fn multiply_basis_elements_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_index: usize, s_degree: i32, @@ -330,12 +364,12 @@ pub trait MuAlgebra: Algebra { fn multiply_basis_element_by_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_idx: usize, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, excess: i32, ); @@ -345,10 +379,10 @@ pub trait MuAlgebra: Algebra { /// Neither `result` nor `r` must be aligned. fn multiply_element_by_basis_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, s_idx: usize, excess: i32, @@ -360,12 +394,12 @@ pub trait MuAlgebra: Algebra { /// Neither `result`, `s`, nor `r` must be aligned. fn multiply_element_by_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, excess: i32, ); } @@ -377,8 +411,8 @@ impl MuAlgebra for A { fn multiply_basis_elements_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_index: usize, s_degree: i32, @@ -390,12 +424,12 @@ impl MuAlgebra for A { fn multiply_basis_element_by_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_idx: usize, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, _excess: i32, ) { self.multiply_basis_element_by_element(result, coeff, r_degree, r_idx, s_degree, s) @@ -403,10 +437,10 @@ impl MuAlgebra for A { fn multiply_element_by_basis_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, s_idx: usize, _excess: i32, @@ -416,12 +450,12 @@ impl MuAlgebra for A { fn multiply_element_by_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, _excess: i32, ) { self.multiply_element_by_element(result, coeff, r_degree, r, s_degree, s) @@ -435,8 +469,8 @@ impl MuAlgebra for A { fn multiply_basis_elements_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_index: usize, s_degree: i32, @@ -450,12 +484,12 @@ impl MuAlgebra for A { fn multiply_basis_element_by_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, r_idx: usize, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, excess: i32, ) { UnstableAlgebra::multiply_basis_element_by_element_unstable( @@ -465,10 +499,10 @@ impl MuAlgebra for A { fn multiply_element_by_basis_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, s_idx: usize, excess: i32, @@ -480,12 +514,12 @@ impl MuAlgebra for A { fn multiply_element_by_element_unstable( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self>, + coeff: Scalar, r_degree: i32, - r: FpSlice, + r: BaseSliceOf<'_, Self>, s_degree: i32, - s: FpSlice, + s: BaseSliceOf<'_, Self>, excess: i32, ) { UnstableAlgebra::multiply_element_by_element_unstable( diff --git a/ext/crates/algebra/src/algebra/base_ring.rs b/ext/crates/algebra/src/algebra/base_ring.rs new file mode 100644 index 0000000000..f97b404483 --- /dev/null +++ b/ext/crates/algebra/src/algebra/base_ring.rs @@ -0,0 +1,138 @@ +//! The base ring an algebra is linear over. +//! +//! An [`Algebra`] declares its coefficient ring as `type BaseRing: Ring`. [`Ring`] is that ring: the +//! scalar type, its arithmetic, and the representation of finite free modules over it (vectors and +//! their slices) — everything needed to define an algebra and act on its modules. The *harder* +//! linear algebra that a free resolution needs — kernels and quasi-inverses, which require the ring +//! to be a graded DVR — is the [`GradedDvr`](crate::linear_algebra::GradedDvr) subtrait, in +//! [`linear_algebra`](crate::linear_algebra). Keeping them separate lets an algebra be defined over a +//! ring whose solving linear algebra is not yet implemented. +//! +//! Every classical algebra has `BaseRing = Field` (the field $\mathbb{F}_p$); the C-motivic Steenrod +//! algebra will use $\mathbb{F}_2[\tau]$. + +use fp::{ + prime::Prime, + vector::{FpSlice, FpSliceMut, FpVector}, +}; + +use crate::{ + algebra::{Algebra, Field}, + linear_algebra::{BaseSlice, BaseSliceMut}, +}; + +/// The base ring of an algebra `A`, i.e. `::BaseRing`. +/// +/// The base ring is a `Copy` handle ([`Ring`] requires `Copy`), so it is threaded *by value*; +/// coefficient code reaches it via [`Module::base_ring`](crate::module::Module::base_ring) rather +/// than through the algebra's `Arc`. +pub type BaseRingOf = ::BaseRing; + +/// The base-ring scalar type of an algebra `A`, i.e. `::Element`. +/// +/// This is the type of coefficients in `A`'s products and its modules' actions. For every classical +/// algebra it is `u32` (the field $\mathbb{F}_p$), so threading it through a signature is a no-op +/// there; the C-motivic algebra will make it $\mathbb{F}_2[\tau]$. +pub type Scalar = as Ring>::Element; + +/// A graded coefficient ring over which algebras and their modules are linear — concretely +/// $\mathbb{F}_p$ (a field, via [`Field`]) or $\mathbb{F}_2[\tau]$. +/// +/// This trait is the coefficient *ring*: the scalar type and its arithmetic, together with a +/// representation of finite free modules over the ring (vectors and their slices). This is enough to +/// *define* an algebra and its modules — to multiply basis elements and act on modules, accumulating +/// ring-coefficient results into a vector. It is deliberately *not* enough to resolve: the linear +/// algebra of solving (kernels, quasi-inverses, minimal generators) requires the ring to be a graded +/// DVR, and lives in the [`GradedDvr`](crate::linear_algebra::GradedDvr) subtrait. Splitting them +/// this way lets us define algebras over rings whose solving linear algebra we have not yet +/// implemented (e.g. $\mathbb{R}$-motivic). +/// +/// The ring is a `Copy` handle (like `fp`'s `Field`): it may carry a small amount of runtime data +/// (e.g. the prime of $\mathbb{F}_p$), so its operations take `self`. +pub trait Ring: Copy + Send + Sync + 'static { + /// A scalar of the ring. + type Element: Copy + PartialEq + Send + Sync; + + fn zero(self) -> Self::Element; + fn one(self) -> Self::Element; + fn add(self, a: Self::Element, b: Self::Element) -> Self::Element; + fn mul(self, a: Self::Element, b: Self::Element) -> Self::Element; + fn is_zero(self, a: Self::Element) -> bool; + + /// Embed a prime-field coefficient — a canonical $\mathbb{F}_p$ representative, e.g. an entry of + /// an $\mathbb{F}_p$-valued vector — into the ring. + /// + /// Every base ring is an (augmented) $\mathbb{F}_p$-algebra, so this is the inclusion + /// $\mathbb{F}_p \hookrightarrow R$. It is used to lift the $\mathbb{F}_p$ multiplicities that + /// appear when expanding a general element into basis elements into ring coefficients. + fn embed_field(self, c: u32) -> Self::Element; + + /// An owned vector of a finite free module over the ring. For [`Field`] this is `fp`'s + /// `FpVector`. + type Vector: Send + Sync; + + /// An immutable slice of a [`Vector`](Ring::Vector). For [`Field`] this is `fp`'s `FpSlice`. + type Slice<'a>: BaseSlice<'a, Self>; + + /// A mutable slice of a [`Vector`](Ring::Vector). For [`Field`] this is `fp`'s `FpSliceMut`. + type SliceMut<'a>: BaseSliceMut<'a, Self>; + + /// Allocate a zero vector of length `len`. + fn new_vector(self, len: usize) -> Self::Vector; + + /// The length of a vector. + fn vector_len(self, vector: &Self::Vector) -> usize; + + /// Borrow a vector immutably. + fn as_slice(self, vector: &Self::Vector) -> Self::Slice<'_>; + + /// Borrow a vector mutably. + fn as_slice_mut(self, vector: &mut Self::Vector) -> Self::SliceMut<'_>; +} + +impl Ring for Field { + type Element = u32; + type Slice<'a> = FpSlice<'a>; + type SliceMut<'a> = FpSliceMut<'a>; + type Vector = FpVector; + + fn zero(self) -> u32 { + 0 + } + + fn one(self) -> u32 { + 1 + } + + fn add(self, a: u32, b: u32) -> u32 { + Algebra::prime(&self).sum(a, b) + } + + fn mul(self, a: u32, b: u32) -> u32 { + Algebra::prime(&self).product(a, b) + } + + fn is_zero(self, a: u32) -> bool { + a.is_multiple_of(Algebra::prime(&self).as_u32()) + } + + fn embed_field(self, c: u32) -> u32 { + c + } + + fn new_vector(self, len: usize) -> FpVector { + FpVector::new(Algebra::prime(&self), len) + } + + fn vector_len(self, vector: &FpVector) -> usize { + vector.len() + } + + fn as_slice(self, vector: &FpVector) -> FpSlice<'_> { + vector.as_slice() + } + + fn as_slice_mut(self, vector: &mut FpVector) -> FpSliceMut<'_> { + vector.as_slice_mut() + } +} diff --git a/ext/crates/algebra/src/algebra/field.rs b/ext/crates/algebra/src/algebra/field.rs index 6f18ebfe4a..4147337476 100644 --- a/ext/crates/algebra/src/algebra/field.rs +++ b/ext/crates/algebra/src/algebra/field.rs @@ -11,7 +11,9 @@ use crate::algebra::{Algebra, Bialgebra}; /// /// As an [`Algebra`], a field is one-dimensional, with basis element `1`. /// It is also trivially a coalgebra via the trivial diagonal comultiplication, -/// and thus a [`Bialgebra`]. +/// and thus a [`Bialgebra`]. It is also the base ring (a [`Ring`](crate::algebra::Ring)) +/// of every classical $\mathbb{F}_p$ algebra. +#[derive(Clone, Copy)] pub struct Field { prime: ValidPrime, } @@ -30,6 +32,12 @@ impl std::fmt::Display for Field { } impl Algebra for Field { + type BaseRing = Self; + + fn base_ring(&self) -> Self { + *self + } + fn prime(&self) -> ValidPrime { self.prime } diff --git a/ext/crates/algebra/src/algebra/milnor_algebra.rs b/ext/crates/algebra/src/algebra/milnor_algebra.rs index 07b3bd35f8..86d7a052d4 100644 --- a/ext/crates/algebra/src/algebra/milnor_algebra.rs +++ b/ext/crates/algebra/src/algebra/milnor_algebra.rs @@ -9,7 +9,7 @@ use once::OnceVec; use rustc_hash::FxHashMap as HashMap; use serde::{Deserialize, Serialize}; -use crate::algebra::{Algebra, Bialgebra, GeneratedAlgebra, UnstableAlgebra, combinatorics}; +use crate::algebra::{Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, combinatorics}; fn q_part_default() -> u32 { !0 @@ -345,6 +345,12 @@ impl MilnorAlgebra { } impl Algebra for MilnorAlgebra { + type BaseRing = Field; + + fn base_ring(&self) -> Field { + Field::new(self.prime()) + } + fn prefix(&self) -> &str { "milnor" } diff --git a/ext/crates/algebra/src/algebra/mod.rs b/ext/crates/algebra/src/algebra/mod.rs index 67baed2b04..eb2de42104 100644 --- a/ext/crates/algebra/src/algebra/mod.rs +++ b/ext/crates/algebra/src/algebra/mod.rs @@ -10,6 +10,9 @@ pub use algebra_trait::{Algebra, GeneratedAlgebra, MuAlgebra, UnstableAlgebra}; mod bialgebra_trait; pub use bialgebra_trait::Bialgebra; +pub mod base_ring; +pub use base_ring::{BaseRingOf, Ring, Scalar}; + pub mod combinatorics; pub mod field; diff --git a/ext/crates/algebra/src/algebra/steenrod_algebra.rs b/ext/crates/algebra/src/algebra/steenrod_algebra.rs index 04c65a7e4e..1b17c186fa 100644 --- a/ext/crates/algebra/src/algebra/steenrod_algebra.rs +++ b/ext/crates/algebra/src/algebra/steenrod_algebra.rs @@ -9,7 +9,11 @@ use serde::Deserialize; use serde_json::Value; use crate::{ - algebra::{AdemAlgebra, Algebra, Bialgebra, GeneratedAlgebra, MilnorAlgebra, UnstableAlgebra}, + algebra::{ + AdemAlgebra, Algebra, Bialgebra, Field, GeneratedAlgebra, MilnorAlgebra, Scalar, + UnstableAlgebra, + }, + linear_algebra::{BaseSliceMutOf, BaseSliceOf}, pair_algebra::PairAlgebra, }; @@ -53,7 +57,11 @@ impl std::str::FromStr for AlgebraType { } #[allow(clippy::large_enum_variant)] -#[enum_dispatch::enum_dispatch(Algebra, Bialgebra, GeneratedAlgebra, UnstableAlgebra)] +// `Algebra` is *not* in this list: it declares an associated `BaseRing` type, which `enum_dispatch` +// cannot handle, so its impl for this enum is hand-rolled below via `dispatch_steenrod!`. The other +// traits declare no associated type and stay dispatched even though their methods consume base-ring +// coefficients. +#[enum_dispatch::enum_dispatch(Bialgebra, GeneratedAlgebra, UnstableAlgebra)] pub enum SteenrodAlgebra { AdemAlgebra(AdemAlgebra), MilnorAlgebra(MilnorAlgebra), @@ -142,6 +150,31 @@ macro_rules! dispatch_steenrod { }; } +// `Algebra` cannot be `enum_dispatch`ed because it declares an associated `BaseRing` type, so we +// forward every method to the active variant by hand. This reproduces exactly what `enum_dispatch` +// generated, keeping the classical behaviour bit-identical. +impl Algebra for SteenrodAlgebra { + type BaseRing = Field; + + dispatch_steenrod! { + fn base_ring(&self) -> Field; + fn prefix(&self) -> &str; + fn magic(&self) -> u32; + fn prime(&self) -> ValidPrime; + fn compute_basis(&self, degree: i32); + fn dimension(&self, degree: i32) -> usize; + fn multiply_basis_elements(&self, result: FpSliceMut, coeff: u32, r_degree: i32, r_idx: usize, s_degree: i32, s_idx: usize); + fn multiply_basis_element_by_element(&self, result: FpSliceMut, coeff: u32, r_degree: i32, r_idx: usize, s_degree: i32, s: FpSlice); + fn multiply_element_by_basis_element(&self, result: FpSliceMut, coeff: u32, r_degree: i32, r: FpSlice, s_degree: i32, s_idx: usize); + fn multiply_element_by_element(&self, result: FpSliceMut, coeff: u32, r_degree: i32, r: FpSlice, s_degree: i32, s: FpSlice); + fn default_filtration_one_products(&self) -> Vec<(String, i32, usize)>; + fn basis_element_to_string(&self, degree: i32, idx: usize) -> String; + fn try_basis_element_to_string(&self, degree: i32, idx: usize) -> Option; + fn basis_element_from_string(&self, elt: &str) -> Option<(i32, usize)>; + fn element_to_string(&self, degree: i32, element: FpSlice) -> String; + } +} + impl PairAlgebra for AdemAlgebra { type Element = crate::pair_algebra::MilnorPairElement; diff --git a/ext/crates/algebra/src/lib.rs b/ext/crates/algebra/src/lib.rs index 3cb1c0cf2f..b78dfab2ec 100644 --- a/ext/crates/algebra/src/lib.rs +++ b/ext/crates/algebra/src/lib.rs @@ -5,13 +5,20 @@ #![deny(clippy::use_self, unsafe_op_in_unsafe_fn)] +pub mod linear_algebra; pub mod module; pub mod steenrod_evaluator; pub(crate) mod steenrod_parser; mod algebra; -pub use crate::algebra::*; +pub use crate::{ + algebra::*, + linear_algebra::{ + BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, GradedDvr, QuasiInverseOf, + SubmoduleOf, VectorOf, + }, +}; pub(crate) fn module_gens_from_json( gens: &serde_json::Value, diff --git a/ext/crates/algebra/src/linear_algebra/mod.rs b/ext/crates/algebra/src/linear_algebra/mod.rs new file mode 100644 index 0000000000..88ddcb740c --- /dev/null +++ b/ext/crates/algebra/src/linear_algebra/mod.rs @@ -0,0 +1,660 @@ +//! The linear algebra of *solving* over a graded DVR. +//! +//! This layer sits beside [`algebra`](crate::algebra) and [`module`](crate::module). The coefficient +//! *ring* — scalars, their arithmetic, and the representation of finite free modules over it +//! (vectors and slices) — is [`Ring`](crate::algebra::Ring); that is enough to define an algebra and +//! act on its modules. This module adds the harder capability a free *resolution* needs: computing +//! images, kernels, and quasi-inverses, which requires the ring to be a graded DVR (so that graded +//! Nakayama holds and minimal generators can be read off mod the maximal ideal). +//! +//! The central trait is [`GradedDvr`]: a [`Ring`](crate::algebra::Ring) over which one can solve +//! linear systems. For [`Field`] the vector types are exactly `fp`'s `FpVector`/`FpSlice`/`FpSliceMut` +//! and every operation forwards to `fp`, so the classical path is unchanged and bit-identical. A +//! future $\mathbb{F}_2[\tau]$ impl provides the graded-local solving (per-weight `fp` blocks plus a +//! $\tau$-divisibility sweep) behind the same interface. + +use fp::{ + matrix::{AugmentedMatrix, QuasiInverse, Subspace}, + vector::{FpSlice, FpSliceMut, FpVector}, +}; +#[allow(unused_imports)] +use maybe_rayon::prelude::*; + +use crate::algebra::{Algebra, BaseRingOf, Field, Ring}; + +/// An owned vector over the base ring of the algebra `A`. +pub type VectorOf = as Ring>::Vector; + +/// An immutable slice of a vector over the base ring of the algebra `A`. For every classical algebra +/// this is `fp`'s `FpSlice`. +pub type BaseSliceOf<'a, A> = as Ring>::Slice<'a>; + +/// A mutable slice of a vector over the base ring of the algebra `A`. For every classical algebra +/// this is `fp`'s `FpSliceMut`. +pub type BaseSliceMutOf<'a, A> = as Ring>::SliceMut<'a>; + +/// A submodule (image or kernel) over the base ring of the algebra `A`. For every classical algebra +/// this is `fp`'s `Subspace`. +pub type SubmoduleOf = as GradedDvr>::Submodule; + +/// A quasi-inverse of a map over the base ring of the algebra `A`. For every classical algebra this +/// is `fp`'s `QuasiInverse`. +pub type QuasiInverseOf = as GradedDvr>::QuasiInverse; + +/// The data — beyond the step matrix itself — needed to construct the next stage of a resolution: +/// how to make the chain map a chain map, and which cycles the new generators must hit. +/// +/// At homological degree 0 there is no previous stage, so both submodule fields are `None` and the +/// target dimension is 0. +pub struct NextStageInput<'a, R: GradedDvr> { + /// The previous chain map's quasi-inverse, used to set `dX(x) = f^{-1}(dC(f(x)))`. `None` at + /// homological degree 0, or when no new augmentation generators are added. + pub previous_chain_map_quasi_inverse: Option<&'a R::QuasiInverse>, + /// The previous kernel, whose cycles the new generators must hit. `None` at homological degree 0. + pub previous_kernel: Option<&'a R::Submodule>, + /// The dimension of `C_{s-1,t}`, the codomain of the target complex's differential (the length of + /// the vector the `apply_complex_differential` callback writes into). + pub complex_differential_target_dim: usize, +} + +/// The generators produced by one resolution step: the new differential and chain-map rows to store, +/// plus the quasi-inverses of the two maps. Returned by +/// [`GradedDvr::construct_next_stage`]. +pub struct NextStage { + /// The number of new generators added (surjecting onto the cokernel plus hitting the old kernel). + pub num_new_gens: usize, + /// The chain map on each new generator (its image in `C_{s,t}`). + pub chain_map_rows: Vec, + /// The differential on each new generator (its image in `X_{s-1,t}`). + pub differential_rows: Vec, + /// A quasi-inverse of the chain map (the augmentation). + pub chain_map_quasi_inverse: R::QuasiInverse, + /// A quasi-inverse of the differential. + pub differential_quasi_inverse: R::QuasiInverse, +} + +/// An immutable slice of a vector over the ring `R`. +/// +/// Slices are cheap `Copy` views (like `fp`'s `FpSlice`). This trait exposes the read-only vector +/// operations the module and algebra code performs. +pub trait BaseSlice<'a, R: Ring>: Copy { + /// The number of entries in the slice. + fn len(self) -> usize; + + /// Whether the slice is empty. + fn is_empty(self) -> bool { + self.len() == 0 + } + + /// The entry at `index`. + fn entry(self, index: usize) -> R::Element; + + /// Whether every entry is zero. + fn is_zero(self) -> bool; + + /// Restrict to the entries in `start..end`. + fn restrict(self, start: usize, end: usize) -> Self; + + /// Iterate over `(index, coefficient)` for each nonzero entry. + fn iter_nonzero(self) -> impl Iterator + 'a; +} + +/// A mutable slice of a vector over the ring `R`. +/// +/// This exposes the in-place vector operations (axpy, setting entries, zeroing) the module and +/// algebra code performs while assembling a result. +pub trait BaseSliceMut<'a, R: Ring> { + /// Reborrow as a shorter-lived mutable slice (so the original stays usable in a loop). This is + /// the base-ring analogue of `FpSliceMut::copy`. + fn reborrow(&mut self) -> R::SliceMut<'_>; + + /// Borrow as an immutable slice. + fn as_slice(&self) -> R::Slice<'_>; + + /// Add `coeff` to the entry at `index`. + fn add_basis_element(&mut self, index: usize, coeff: R::Element); + + /// Add `coeff * other` into this slice. + fn add(&mut self, other: R::Slice<'_>, coeff: R::Element); + + /// Set the entry at `index` to `value`. + fn set_entry(&mut self, index: usize, value: R::Element); + + /// Set every entry to zero. + fn set_to_zero(&mut self); + + /// Restrict to the entries in `start..end`. + fn slice_mut(&mut self, start: usize, end: usize) -> R::SliceMut<'_>; +} + +/// The linear algebra of solving over a graded DVR `R`. +/// +/// Extends [`Ring`](crate::algebra::Ring) — which already provides the ring's scalars and its vector +/// representation — with the operations a free resolution needs: computing the image, kernel, and +/// quasi-inverse of an `R`-linear map. These are meaningful precisely because `R` is a graded DVR +/// (graded-local, so graded Nakayama holds). For [`Field`] everything forwards to `fp`. +pub trait GradedDvr: Ring { + /// A submodule of a finite free module over the ring (the image or kernel of a map). For + /// [`Field`] this is `fp`'s [`Subspace`]. + type Submodule: Send + Sync; + + /// A quasi-inverse of an `R`-linear map: a right inverse when restricted to its image. For + /// [`Field`] this is `fp`'s [`QuasiInverse`]. + type QuasiInverse: Send + Sync; + + /// Compute `(image, kernel, quasi_inverse)` of the `R`-linear map + /// `R^source_dim -> R^target_dim` whose matrix is assembled by calling `fill_row(i, row)` for + /// each source basis index `i`. + /// + /// This is the base-ring-generic form of + /// [`ModuleHomomorphism::auxiliary_data`](crate::module::homomorphism::ModuleHomomorphism::auxiliary_data): + /// the map is presented one row at a time, so the ring owns its matrix representation — a field + /// uses a single dense `fp` matrix, while a graded-local ring can store per-weight blocks. + fn image_kernel_quasi_inverse( + self, + source_dim: usize, + target_dim: usize, + fill_row: impl FnMut(usize, Self::SliceMut<'_>), + ) -> (Self::Submodule, Self::Submodule, Self::QuasiInverse); + + /// The number of generators (rank) of a submodule. + fn submodule_dimension(self, submodule: &Self::Submodule) -> usize; + + /// Iterate over a basis of the submodule. + fn submodule_iter<'a>( + self, + submodule: &'a Self::Submodule, + ) -> impl Iterator> + 'a; + + /// The dimension of the domain of the map the quasi-inverse inverts. + fn quasi_inverse_source_dimension(self, quasi_inverse: &Self::QuasiInverse) -> usize; + + /// The dimension of the codomain of the map the quasi-inverse inverts. + fn quasi_inverse_target_dimension(self, quasi_inverse: &Self::QuasiInverse) -> usize; + + /// Add `coeff * quasi_inverse(input)` to `result`. + /// + /// `input` must lie in the image of the map the quasi-inverse was computed for; the result is a + /// preimage. This is the base-ring-generic form of + /// [`ModuleHomomorphism::apply_quasi_inverse`](crate::module::homomorphism::ModuleHomomorphism::apply_quasi_inverse). + fn apply_quasi_inverse( + self, + quasi_inverse: &Self::QuasiInverse, + result: Self::SliceMut<'_>, + coeff: Self::Element, + input: Self::Slice<'_>, + ); + + /// The augmented matrix `[f | d | I]` of a resolution step, expressing the step's chain map `f` + /// and differential `d` (out of the free module `X_{s,t}`) as a plain vector-space map over the + /// ring, with an identity block for tracking preimages. For `Field` this is `fp`'s + /// `AugmentedMatrix<3>`. + type Matrix; + + /// **Block 2** — realize the differential as a vector-space map. Assemble and row-reduce the + /// augmented matrix `[f | d | I]`: `fill_chain_map(i, row)` / `fill_differential(i, row)` write + /// row `i` of the `f` / `d` blocks (via the module action), for `i in 0..source_dim`. The + /// callbacks are `Sync` so the assembly can be parallelized. + fn differential_matrix( + self, + source_dim: usize, + chain_map_target_dim: usize, + differential_target_dim: usize, + max_new_gens: usize, + fill_chain_map: impl Fn(usize, Self::SliceMut<'_>) + Sync, + fill_differential: impl Fn(usize, Self::SliceMut<'_>) + Sync, + ) -> Self::Matrix; + + /// **Block 3** — the kernel of the reduced step matrix (the cycles in `X_{s,t}`). + fn step_kernel(self, matrix: &Self::Matrix) -> Self::Submodule; + + /// **Block 4** — construct the next stage: add generators to `X_{s,t}` that surject onto the + /// cokernel of `f` and hit any cycles of the previous kernel not already in the image, compute + /// their differential (making `f` a chain map, via `apply_complex_differential` and the previous + /// chain map's quasi-inverse), and produce quasi-inverses of `f` and `d`. + /// + /// The source dimension and chain-map target dimension are read off the matrix; the rest of the + /// data is bundled in [`NextStageInput`]. `apply_complex_differential` writes `dC(f(x))` for the + /// new generator at column `x` into the provided slice. Consumes the matrix (its quasi-inverses + /// take ownership). + fn construct_next_stage( + self, + matrix: Self::Matrix, + input: NextStageInput<'_, Self>, + apply_complex_differential: impl FnMut(usize, Self::SliceMut<'_>), + max_new_gens: usize, + ) -> NextStage; +} + +impl<'a> BaseSlice<'a, Field> for FpSlice<'a> { + fn len(self) -> usize { + FpSlice::len(&self) + } + + fn entry(self, index: usize) -> u32 { + FpSlice::entry(&self, index) + } + + fn is_zero(self) -> bool { + FpSlice::is_zero(&self) + } + + fn restrict(self, start: usize, end: usize) -> Self { + FpSlice::restrict(self, start, end) + } + + fn iter_nonzero(self) -> impl Iterator + 'a { + FpSlice::iter_nonzero(self) + } +} + +impl<'a> BaseSliceMut<'a, Field> for FpSliceMut<'a> { + fn reborrow(&mut self) -> FpSliceMut<'_> { + self.copy() + } + + fn as_slice(&self) -> FpSlice<'_> { + FpSliceMut::as_slice(self) + } + + fn add_basis_element(&mut self, index: usize, coeff: u32) { + FpSliceMut::add_basis_element(self, index, coeff); + } + + fn add(&mut self, other: FpSlice<'_>, coeff: u32) { + FpSliceMut::add(self, other, coeff); + } + + fn set_entry(&mut self, index: usize, value: u32) { + FpSliceMut::set_entry(self, index, value); + } + + fn set_to_zero(&mut self) { + FpSliceMut::set_to_zero(self); + } + + fn slice_mut(&mut self, start: usize, end: usize) -> FpSliceMut<'_> { + FpSliceMut::slice_mut(self, start, end) + } +} + +impl GradedDvr for Field { + type Matrix = AugmentedMatrix<3>; + type QuasiInverse = QuasiInverse; + type Submodule = Subspace; + + fn image_kernel_quasi_inverse( + self, + source_dim: usize, + target_dim: usize, + mut fill_row: impl FnMut(usize, FpSliceMut<'_>), + ) -> (Subspace, Subspace, QuasiInverse) { + let p = Algebra::prime(&self); + // The augmented matrix `[ M | I ]`: segment 0 holds the map, segment 1 tracks preimages so + // that after row reduction we can read off the quasi-inverse. This mirrors + // `ModuleHomomorphism::auxiliary_data` exactly. + let mut matrix = AugmentedMatrix::<2>::new(p, source_dim, [target_dim, source_dim]); + { + let mut segment = matrix.segment(0, 0); + for i in 0..source_dim { + fill_row(i, segment.row_mut(i)); + } + } + matrix.segment(1, 1).add_identity(); + + matrix.row_reduce(); + + ( + matrix.compute_image(), + matrix.compute_kernel(), + matrix.compute_quasi_inverse(), + ) + } + + fn submodule_dimension(self, submodule: &Subspace) -> usize { + submodule.dimension() + } + + fn submodule_iter<'a>(self, submodule: &'a Subspace) -> impl Iterator> + 'a { + submodule.iter() + } + + fn quasi_inverse_source_dimension(self, quasi_inverse: &QuasiInverse) -> usize { + quasi_inverse.source_dimension() + } + + fn quasi_inverse_target_dimension(self, quasi_inverse: &QuasiInverse) -> usize { + quasi_inverse.target_dimension() + } + + fn apply_quasi_inverse( + self, + quasi_inverse: &QuasiInverse, + result: FpSliceMut<'_>, + coeff: u32, + input: FpSlice<'_>, + ) { + quasi_inverse.apply(result, coeff, input); + } + + fn differential_matrix( + self, + source_dim: usize, + chain_map_target_dim: usize, + differential_target_dim: usize, + max_new_gens: usize, + fill_chain_map: impl Fn(usize, FpSliceMut<'_>) + Sync, + fill_differential: impl Fn(usize, FpSliceMut<'_>) + Sync, + ) -> AugmentedMatrix<3> { + let p = Algebra::prime(&self); + let mut matrix = AugmentedMatrix::<3>::new_with_capacity( + p, + source_dim, + &[chain_map_target_dim, differential_target_dim, source_dim], + source_dim + max_new_gens, + max_new_gens, + ); + matrix + .segment(0, 0) + .maybe_par_iter_mut() + .enumerate() + .for_each(|(i, row)| fill_chain_map(i, row)); + matrix + .segment(1, 1) + .maybe_par_iter_mut() + .enumerate() + .for_each(|(i, row)| fill_differential(i, row)); + matrix.segment(2, 2).add_identity(); + matrix.row_reduce(); + matrix + } + + fn step_kernel(self, matrix: &AugmentedMatrix<3>) -> Subspace { + matrix.compute_kernel() + } + + fn construct_next_stage( + self, + mut matrix: AugmentedMatrix<3>, + input: NextStageInput<'_, Self>, + mut apply_complex_differential: impl FnMut(usize, FpSliceMut<'_>), + max_new_gens: usize, + ) -> NextStage { + let NextStageInput { + previous_chain_map_quasi_inverse, + previous_kernel, + complex_differential_target_dim, + } = input; + + let p = Algebra::prime(&self); + // The source dimension and the chain-map (augmentation) target dimension are exactly the row + // count and the first block's width of the freshly built matrix. + let source_dimension = matrix.rows(); + let target_cc_dimension = matrix.end[0] - matrix.start[0]; + let differential_block_dim = matrix.end[1] - matrix.start[1]; + + // Add generators to surject onto C_{s,t} (the cokernel of f). + let cc_new_gens = matrix.extend_to_surjection(0, target_cc_dimension, max_new_gens); + let mut res_new_gens = Vec::new(); + + if let Some(previous_kernel) = previous_kernel { + if !cc_new_gens.is_empty() { + // Make f a chain map: set dX(x) = f^{-1}(dC(f(x))) for each new generator x. + let quasi_inverse = previous_chain_map_quasi_inverse + .expect("chain homotopy requires the previous chain map's quasi-inverse"); + let mut dfx = FpVector::new(p, complex_differential_target_dim); + for (i, &column) in cc_new_gens.iter().enumerate() { + apply_complex_differential(column, dfx.as_slice_mut()); + quasi_inverse.apply( + matrix.row_segment_mut(source_dimension + i, 1, 1), + 1, + dfx.as_slice(), + ); + dfx.set_to_zero(); + } + } + + // Add generators to hit any cycles of the old kernel not already in the image. + res_new_gens = matrix.inner.extend_image( + matrix.start[1], + matrix.end[1], + previous_kernel, + max_new_gens, + ); + } + + let num_new_gens = cc_new_gens.len() + res_new_gens.len(); + let new_rows = source_dimension + num_new_gens; + + // Read off the new generators' chain-map and differential rows before the RREF fix-up below + // mutates them. + let chain_map_rows: Vec = { + let mut segment = matrix.segment(0, 0); + (source_dimension..new_rows) + .map(|r| { + let mut v = FpVector::new(p, target_cc_dimension); + v.as_slice_mut().add(segment.row(r), 1); + v + }) + .collect() + }; + let differential_rows: Vec = { + let mut segment = matrix.segment(1, 1); + (source_dimension..new_rows) + .map(|r| { + let mut v = FpVector::new(p, differential_block_dim); + v.as_slice_mut().add(segment.row(r), 1); + v + }) + .collect() + }; + + if num_new_gens > 0 { + // Fix up the augmentation: the new rows are almost in RREF, so patch them up rather than + // re-running the full reduction. + let columns = matrix.columns(); + matrix.extend_column_dimension(columns + num_new_gens); + + for i in source_dimension..new_rows { + matrix.inner.row_mut(i).set_entry(matrix.start[2] + i, 1); + } + + // Clear the new cc rows using the old rows. + for k in source_dimension..source_dimension + cc_new_gens.len() { + for column in matrix.start[1]..matrix.end[1] { + let row = matrix.pivots()[column]; + if row < 0 { + continue; + } + let row = row as usize; + unsafe { + matrix.row_op(k, row, column, p); + } + } + } + + // Use the new resolution rows to reduce the old rows and the cc rows. + let first_res_row = source_dimension + cc_new_gens.len(); + for (source_row, &pivot_col) in res_new_gens.iter().enumerate() { + for target_row in 0..first_res_row { + unsafe { + matrix.row_op(target_row, source_row + first_res_row, pivot_col, p); + } + } + } + + // Permute the rows into RREF. + let mut new_gens = cc_new_gens.into_iter().chain(res_new_gens).enumerate(); + let (mut next_new_row, mut next_new_col) = new_gens.next().unwrap(); + let mut next_old_row = 0; + + for old_col in 0..matrix.columns() { + if old_col == next_new_col { + matrix.rotate_down(next_old_row..source_dimension + next_new_row + 1, 1); + matrix.pivots_mut()[old_col] = next_old_row as isize; + match new_gens.next() { + Some((x, y)) => { + next_new_row = x; + next_new_col = y; + } + None => { + for entry in &mut matrix.pivots_mut()[old_col + 1..] { + if *entry >= 0 { + *entry += next_new_row as isize + 1; + } + } + break; + } + } + next_old_row += 1; + } else if matrix.pivots()[old_col] >= 0 { + matrix.pivots_mut()[old_col] += next_new_row as isize; + next_old_row += 1; + } + } + } + + let (chain_map_quasi_inverse, differential_quasi_inverse) = matrix.compute_quasi_inverses(); + + NextStage { + num_new_gens, + chain_map_rows, + differential_rows, + chain_map_quasi_inverse, + differential_quasi_inverse, + } + } +} + +#[cfg(test)] +mod tests { + use fp::prime::ValidPrime; + + use super::*; + + #[test] + fn field_vector_ops() { + let field = Field::new(ValidPrime::new(2)); + let mut v = field.new_vector(3); + { + let mut s = field.as_slice_mut(&mut v); + s.add_basis_element(0, 1); + s.add_basis_element(2, 1); + } + let s = field.as_slice(&v); + assert_eq!(s.len(), 3); + assert_eq!(s.entry(0), 1); + assert_eq!(s.entry(1), 0); + assert_eq!(s.entry(2), 1); + assert!(!s.is_zero()); + let nz: Vec<_> = s.iter_nonzero().collect(); + assert_eq!(nz, vec![(0, 1), (2, 1)]); + } + + #[test] + fn field_kernel_quasi_inverse() { + let field = Field::new(ValidPrime::new(2)); + // Rank-deficient map F_2^2 -> F_2^3 with two equal rows (1,1,0): kernel is 1-dimensional. + let rows = [[1u32, 1, 0], [1, 1, 0]]; + let (image, kernel, _qi) = field.image_kernel_quasi_inverse(2, 3, |i, mut row| { + for (j, &val) in rows[i].iter().enumerate() { + if val != 0 { + row.add_basis_element(j, val); + } + } + }); + assert_eq!(image.dimension(), 1); + assert_eq!(kernel.dimension(), 1); + } + + #[test] + fn field_apply_quasi_inverse_is_section() { + let field = Field::new(ValidPrime::new(2)); + // Full-rank map F_2^2 -> F_2^3, rows (1,1,0) and (0,1,1). + let rows = [[1u32, 1, 0], [0, 1, 1]]; + let (_image, kernel, qi) = field.image_kernel_quasi_inverse(2, 3, |i, mut row| { + for (j, &val) in rows[i].iter().enumerate() { + if val != 0 { + row.add_basis_element(j, val); + } + } + }); + + assert_eq!(field.submodule_dimension(&kernel), 0); + assert_eq!(field.quasi_inverse_source_dimension(&qi), 2); + assert_eq!(field.quasi_inverse_target_dimension(&qi), 3); + + // Apply the quasi-inverse to y = row 0 = (1,1,0), then re-apply the map: recover y. + let mut preimage = field.new_vector(2); + let y = FpVector::from_slice(field.prime(), &[1, 1, 0]); + field.apply_quasi_inverse(&qi, field.as_slice_mut(&mut preimage), 1, y.as_slice()); + + let mut reconstructed = field.new_vector(3); + for (i, c) in field.as_slice(&preimage).iter_nonzero() { + for (j, &val) in rows[i].iter().enumerate() { + if val != 0 { + field + .as_slice_mut(&mut reconstructed) + .add_basis_element(j, (c * val) % 2); + } + } + } + for j in 0..3 { + assert_eq!(field.as_slice(&reconstructed).entry(j), y.entry(j)); + } + } + + #[test] + fn differential_matrix_and_kernel() { + let field = Field::new(ValidPrime::new(2)); + // X has dimension 2; the differential d: X -> Y (dim 2) has rows (1,0) and (1,0): rank 1, so + // its kernel is 1-dimensional. There is no chain map (C has dimension 0). + let d_rows = [[1u32, 0], [1, 0]]; + let matrix = field.differential_matrix( + 2, + 0, + 2, + 0, + |_, _| {}, + |i, mut row| { + for (j, &v) in d_rows[i].iter().enumerate() { + if v != 0 { + row.add_basis_element(j, v); + } + } + }, + ); + let kernel = field.step_kernel(&matrix); + assert_eq!(field.submodule_dimension(&kernel), 1); + } + + #[test] + fn resolution_step_augmentation() { + let field = Field::new(ValidPrime::new(2)); + // s = 0 augmentation: X_0 is empty so far, C_0 has dimension 2, X_{-1} is zero. We expect two + // new generators mapping isomorphically onto C_0, with no differential. + let matrix = field.differential_matrix(0, 2, 0, 2, |_, _| {}, |_, _| {}); + let next = field.construct_next_stage( + matrix, + NextStageInput { + previous_chain_map_quasi_inverse: None, + previous_kernel: None, + complex_differential_target_dim: 0, + }, + |_, _| {}, + 2, + ); + + assert_eq!(next.num_new_gens, 2); + assert_eq!(next.chain_map_rows.len(), 2); + // The chain map on the new generators is the identity onto C_0. + assert_eq!(next.chain_map_rows[0].entry(0), 1); + assert_eq!(next.chain_map_rows[0].entry(1), 0); + assert_eq!(next.chain_map_rows[1].entry(0), 0); + assert_eq!(next.chain_map_rows[1].entry(1), 1); + // No differential (X_{-1} is zero). + for row in &next.differential_rows { + assert_eq!(row.len(), 0); + } + } +} diff --git a/ext/crates/algebra/src/module/finite_dimensional_module.rs b/ext/crates/algebra/src/module/finite_dimensional_module.rs index 58fed33b6f..a62733d33c 100644 --- a/ext/crates/algebra/src/module/finite_dimensional_module.rs +++ b/ext/crates/algebra/src/module/finite_dimensional_module.rs @@ -7,11 +7,11 @@ use serde::Deserialize; use serde_json::{json, value::Value}; use crate::{ - algebra::{Algebra, GeneratedAlgebra}, + algebra::{Algebra, Field, GeneratedAlgebra, Ring, Scalar}, module::{Module, ModuleFailedRelationError, ZeroModule}, }; -pub struct FiniteDimensionalModule { +pub struct FiniteDimensionalModule> { algebra: Arc, pub name: String, graded_dimension: BiVec, @@ -20,13 +20,13 @@ pub struct FiniteDimensionalModule { actions: BiVec>>>, } -impl std::fmt::Display for FiniteDimensionalModule { +impl> std::fmt::Display for FiniteDimensionalModule { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.name) } } -impl Clone for FiniteDimensionalModule { +impl> Clone for FiniteDimensionalModule { fn clone(&self) -> Self { Self { algebra: Arc::clone(&self.algebra), @@ -38,15 +38,15 @@ impl Clone for FiniteDimensionalModule { } } -impl PartialEq for FiniteDimensionalModule { +impl> PartialEq for FiniteDimensionalModule { fn eq(&self, other: &Self) -> bool { self.test_equal(other).is_ok() } } -impl Eq for FiniteDimensionalModule {} +impl> Eq for FiniteDimensionalModule {} -impl FiniteDimensionalModule { +impl> FiniteDimensionalModule { pub fn test_equal(&self, other: &Self) -> Result<(), String> { if self.graded_dimension != other.graded_dimension { if self.graded_dimension.min_degree() != other.graded_dimension.min_degree() { @@ -122,7 +122,7 @@ impl FiniteDimensionalModule { } } -impl Module for FiniteDimensionalModule { +impl> Module for FiniteDimensionalModule { type Algebra = A; fn algebra(&self) -> Arc { @@ -156,7 +156,7 @@ impl Module for FiniteDimensionalModule { fn act_on_basis( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -182,13 +182,13 @@ impl Module for FiniteDimensionalModule { } } -impl ZeroModule for FiniteDimensionalModule { +impl> ZeroModule for FiniteDimensionalModule { fn zero_module(algebra: Arc, min_degree: i32) -> Self { Self::new(algebra, "zero".to_string(), BiVec::new(min_degree)) } } -impl FiniteDimensionalModule { +impl> FiniteDimensionalModule { pub fn new(algebra: Arc, name: String, graded_dimension: BiVec) -> Self { let min_degree = graded_dimension.min_degree(); let max_degree = graded_dimension.len(); @@ -397,7 +397,10 @@ impl FiniteDimensionalModule { } } -impl From<&M> for FiniteDimensionalModule { +impl From<&M> for FiniteDimensionalModule +where + M::Algebra: Algebra, +{ /// This should really by try_from but orphan rules prohibit this fn from(module: &M) -> Self { let min_degree = module.min_degree(); @@ -432,7 +435,7 @@ impl From<&M> for FiniteDimensionalModule { result.action_mut(op_degree, op_idx, input_degree, input_idx); module.act_on_basis( output_vec.as_slice_mut(), - 1, + module.base_ring().one(), op_degree, op_idx, input_degree, @@ -446,7 +449,10 @@ impl From<&M> for FiniteDimensionalModule { } } -impl FiniteDimensionalModule { +impl FiniteDimensionalModule +where + A: Algebra, +{ pub fn from_json(algebra: Arc, json: &Value) -> anyhow::Result { let (graded_dimension, gen_names, gen_to_idx) = crate::module_gens_from_json(&json["gens"]); let name = json["name"].as_str().unwrap_or("").to_string(); @@ -560,10 +566,17 @@ impl FiniteDimensionalModule { for &(coef, (deg_1, idx_1), (deg_2, idx_2)) in &relation { let intermediate_dim = self.dimension(input_deg + deg_2); tmp_output.set_scratch_vector_size(intermediate_dim); - self.act_on_basis(tmp_output.as_slice_mut(), 1, deg_2, idx_2, input_deg, idx); + self.act_on_basis( + tmp_output.as_slice_mut(), + self.algebra.base_ring().one(), + deg_2, + idx_2, + input_deg, + idx, + ); self.act( output_vec.as_slice_mut(), - coef, + self.algebra.base_ring().embed_field(coef), deg_1, idx_1, deg_2 + input_deg, @@ -624,7 +637,7 @@ impl FiniteDimensionalModule { } self.act_on_basis( tmp_output.slice_mut(0, intermediate_dim), - 1, + self.algebra.base_ring().one(), deg_2, idx_2, input_deg, @@ -632,7 +645,7 @@ impl FiniteDimensionalModule { ); self.act( output_vec.as_slice_mut(), - coef, + self.algebra.base_ring().embed_field(coef), deg_1, idx_1, deg_2 + input_deg, diff --git a/ext/crates/algebra/src/module/finitely_presented_module.rs b/ext/crates/algebra/src/module/finitely_presented_module.rs index d8af3d11e1..ea9c6c3d5f 100644 --- a/ext/crates/algebra/src/module/finitely_presented_module.rs +++ b/ext/crates/algebra/src/module/finitely_presented_module.rs @@ -6,7 +6,8 @@ use once::OnceBiVec; use serde_json::Value; use crate::{ - algebra::Algebra, + algebra::{Algebra, BaseRingOf, Field, Scalar}, + linear_algebra::GradedDvr, module::{ FreeModule, Module, ZeroModule, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, @@ -18,7 +19,10 @@ struct FPMIndexTable { fp_idx_to_gen_idx: Vec, } -pub struct FinitelyPresentedModule { +pub struct FinitelyPresentedModule +where + BaseRingOf: GradedDvr, +{ name: String, min_degree: i32, generators: Arc>, @@ -27,27 +31,33 @@ pub struct FinitelyPresentedModule { index_table: OnceBiVec, } -impl std::fmt::Display for FinitelyPresentedModule { +impl std::fmt::Display for FinitelyPresentedModule +where + BaseRingOf: GradedDvr, +{ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.name) } } -impl PartialEq for FinitelyPresentedModule { +impl PartialEq for FinitelyPresentedModule +where + BaseRingOf: GradedDvr, +{ fn eq(&self, _other: &Self) -> bool { todo!() } } -impl Eq for FinitelyPresentedModule {} +impl Eq for FinitelyPresentedModule where BaseRingOf: GradedDvr {} -impl ZeroModule for FinitelyPresentedModule { +impl> ZeroModule for FinitelyPresentedModule { fn zero_module(algebra: Arc, min_degree: i32) -> Self { Self::new(algebra, "zero".to_string(), min_degree) } } -impl FinitelyPresentedModule { +impl> FinitelyPresentedModule { pub fn new(algebra: Arc, name: String, min_degree: i32) -> Self { let generators = Arc::new(FreeModule::new( Arc::clone(&algebra), @@ -99,7 +109,7 @@ impl FinitelyPresentedModule { } } -impl FinitelyPresentedModule { +impl> FinitelyPresentedModule { pub fn from_json(algebra: Arc, json: &Value) -> anyhow::Result { use anyhow::anyhow; use nom::{Parser, combinator::opt}; @@ -170,7 +180,7 @@ impl FinitelyPresentedModule { } } -impl Module for FinitelyPresentedModule { +impl> Module for FinitelyPresentedModule { type Algebra = A; fn algebra(&self) -> Arc { @@ -217,7 +227,7 @@ impl Module for FinitelyPresentedModule { fn act_on_basis( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, diff --git a/ext/crates/algebra/src/module/free_module.rs b/ext/crates/algebra/src/module/free_module.rs index 45af27b2c8..f125344a0e 100644 --- a/ext/crates/algebra/src/module/free_module.rs +++ b/ext/crates/algebra/src/module/free_module.rs @@ -1,10 +1,11 @@ use std::sync::Arc; -use fp::vector::{FpSlice, FpSliceMut}; +use fp::vector::FpSlice; use once::{OnceBiVec, OnceVec}; use crate::{ - algebra::MuAlgebra, + algebra::{MuAlgebra, Scalar}, + linear_algebra::{BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf}, module::{Module, ZeroModule}, }; @@ -139,8 +140,8 @@ impl> Module for MuFreeModule { fn act_on_basis( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -179,12 +180,12 @@ impl> Module for MuFreeModule { fn act( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, input_degree: i32, - input: FpSlice, + input: BaseSliceOf<'_, Self::Algebra>, ) { for GeneratorData { gen_deg, 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..c94bac4b45 100644 --- a/ext/crates/algebra/src/module/homomorphism/free_module_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/free_module_homomorphism.rs @@ -1,13 +1,16 @@ use std::sync::Arc; use fp::{ - matrix::{MatrixSliceMut, QuasiInverse, Subspace}, - vector::{FpSlice, FpSliceMut, FpVector}, + matrix::MatrixSliceMut, + vector::{FpSlice, FpVector}, }; use once::OnceBiVec; use crate::{ - algebra::MuAlgebra, + algebra::{Algebra, BaseRingOf, Field, MuAlgebra, Ring, Scalar}, + linear_algebra::{ + BaseSlice, BaseSliceMut, BaseSliceMutOf, GradedDvr, QuasiInverseOf, SubmoduleOf, VectorOf, + }, module::{ Module, MuFreeModule, free_module::OperationGeneratorPair, @@ -21,13 +24,14 @@ pub type UnstableFreeModuleHomomorphism = MuFreeModuleHomomorphism; pub struct MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, + BaseRingOf: GradedDvr, { source: Arc>, target: Arc, - outputs: OnceBiVec>, // degree --> input_idx --> output - pub images: OnceBiVec>, - pub kernels: OnceBiVec>, - pub quasi_inverses: OnceBiVec>, + outputs: OnceBiVec>>, // degree --> input_idx --> output + pub images: OnceBiVec>>, + pub kernels: OnceBiVec>>, + pub quasi_inverses: OnceBiVec>>, min_degree: i32, /// degree shift, such that ouptut_degree = input_degree - degree_shift degree_shift: i32, @@ -36,6 +40,7 @@ where impl ModuleHomomorphism for MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, + BaseRingOf: GradedDvr, { type Source = MuFreeModule; type Target = M; @@ -54,8 +59,8 @@ where fn apply_to_basis_element( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, ::Algebra>, + coeff: Scalar<::Algebra>, input_degree: i32, input_index: usize, ) { @@ -74,6 +79,7 @@ where } = *self.source.index_to_op_gen(input_degree, input_index); if generator_degree >= self.min_degree() { + let ring = self.target.base_ring(); let output_on_generator = self.output(generator_degree, generator_index); self.target.act( result, @@ -81,20 +87,20 @@ where operation_degree, operation_index, generator_degree - self.degree_shift, - output_on_generator.as_slice(), + ring.as_slice(output_on_generator), ); } } - fn quasi_inverse(&self, degree: i32) -> Option<&QuasiInverse> { + fn quasi_inverse(&self, degree: i32) -> Option<&QuasiInverseOf> { self.quasi_inverses.get(degree).and_then(Option::as_ref) } - fn kernel(&self, degree: i32) -> Option<&Subspace> { + fn kernel(&self, degree: i32) -> Option<&SubmoduleOf> { self.kernels.get(degree).and_then(Option::as_ref) } - fn image(&self, degree: i32) -> Option<&Subspace> { + fn image(&self, degree: i32) -> Option<&SubmoduleOf> { self.images.get(degree).and_then(Option::as_ref) } @@ -111,6 +117,7 @@ where impl MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, + BaseRingOf: GradedDvr, { pub fn new( source: Arc>, @@ -146,7 +153,7 @@ where self.outputs.len() } - pub fn output(&self, generator_degree: i32, generator_index: usize) -> &FpVector { + pub fn output(&self, generator_degree: i32, generator_index: usize) -> &VectorOf { assert!( generator_degree >= self.min_degree(), "generator_degree {} less than min degree {}", @@ -162,28 +169,70 @@ where &self.outputs[generator_degree][generator_index] } - pub fn differential_density(&self, degree: i32) -> f32 { - let outputs = &self.outputs[degree]; - if outputs.is_empty() { - f32::NAN - } else { - outputs.iter().map(FpVector::density).sum::() / outputs.len() as f32 - } - } - pub fn extend_by_zero(&self, degree: i32) { - let p = self.prime(); + let ring = self.target.base_ring(); self.outputs.extend(degree, |i| { let num_gens = self.source.number_of_gens_in_degree(i); let dimension = self.target.dimension(i - self.degree_shift); - let mut new_outputs: Vec = Vec::with_capacity(num_gens); + let mut new_outputs: Vec> = Vec::with_capacity(num_gens); for _ in 0..num_gens { - new_outputs.push(FpVector::new(p, dimension)); + new_outputs.push(ring.new_vector(dimension)); } new_outputs }); } + pub fn add_generators_from_rows(&self, degree: i32, rows: Vec>) { + self.outputs.push_checked(rows, degree); + } + + /// Add the image of a bidegree out of order. See + /// [`OnceVec::push_ooo`](once::OnceVec::push_ooo) for details on return value. + pub fn add_generators_from_rows_ooo( + &self, + degree: i32, + rows: Vec>, + ) -> std::ops::Range { + self.outputs.push_ooo(rows, degree) + } + + /// List of outputs that have been added out of order + pub fn ooo_outputs(&self) -> Vec { + self.outputs.ooo_elements() + } + + pub fn set_image(&self, degree: i32, image: Option>) { + self.images.push_checked(image, degree); + } + + pub fn set_kernel(&self, degree: i32, kernel: Option>) { + self.kernels.push_checked(kernel, degree); + } + + pub fn set_quasi_inverse( + &self, + degree: i32, + quasi_inverse: Option>, + ) { + self.quasi_inverses.push_checked(quasi_inverse, degree); + } +} + +/// Helpers that build outputs from `fp`'s concrete matrices/vectors, available only when the base +/// ring is a field. +impl MuFreeModuleHomomorphism +where + M::Algebra: MuAlgebra + Algebra, +{ + pub fn differential_density(&self, degree: i32) -> f32 { + let outputs = &self.outputs[degree]; + if outputs.is_empty() { + f32::NAN + } else { + outputs.iter().map(FpVector::density).sum::() / outputs.len() as f32 + } + } + pub fn add_generators_from_big_vector(&self, degree: i32, outputs_vectors: FpSlice) { let p = self.prime(); let new_generators = self.source.number_of_gens_in_degree(degree); @@ -224,47 +273,17 @@ where self.outputs.push_checked(new_outputs, degree); } - pub fn add_generators_from_rows(&self, degree: i32, rows: Vec) { - self.outputs.push_checked(rows, degree); - } - - /// Add the image of a bidegree out of order. See - /// [`OnceVec::push_ooo`](once::OnceVec::push_ooo) for details on return value. - pub fn add_generators_from_rows_ooo( - &self, - degree: i32, - rows: Vec, - ) -> std::ops::Range { - self.outputs.push_ooo(rows, degree) - } - - /// List of outputs that have been added out of order - pub fn ooo_outputs(&self) -> Vec { - self.outputs.ooo_elements() - } - pub fn apply_to_generator(&self, result: &mut FpVector, coeff: u32, degree: i32, idx: usize) { let output_on_gen = self.output(degree, idx); result.add(output_on_gen, coeff); } - - pub fn set_image(&self, degree: i32, image: Option) { - self.images.push_checked(image, degree); - } - - pub fn set_kernel(&self, degree: i32, kernel: Option) { - self.kernels.push_checked(kernel, degree); - } - - pub fn set_quasi_inverse(&self, degree: i32, quasi_inverse: Option) { - self.quasi_inverses.push_checked(quasi_inverse, degree); - } } impl ZeroHomomorphism, M> for MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, + BaseRingOf: GradedDvr, { fn zero_homomorphism( source: Arc>, @@ -275,7 +294,9 @@ where } } -impl> MuFreeModuleHomomorphism> { +impl + Algebra> + MuFreeModuleHomomorphism> +{ /// Given f: M -> N, compute the dual f*: Hom(N, k) -> Hom(M, k) in source (N) degree t. pub fn hom_k(&self, t: i32) -> Vec> { let source_dim = self.source.number_of_gens_in_degree(t + self.degree_shift); 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..5bddee73e7 100644 --- a/ext/crates/algebra/src/module/homomorphism/full_module_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/full_module_homomorphism.rs @@ -8,7 +8,7 @@ use fp::{ use once::OnceBiVec; use crate::{ - algebra::Algebra, + algebra::{Algebra, Field, Scalar}, module::{ Module, homomorphism::{IdentityHomomorphism, ModuleHomomorphism, ZeroHomomorphism}, @@ -42,8 +42,9 @@ impl> Clone for FullModuleHomomorphis } } -impl> ModuleHomomorphism - for FullModuleHomomorphism +impl> ModuleHomomorphism for FullModuleHomomorphism +where + S::Algebra: Algebra, { type Source = S; type Target = T; @@ -63,7 +64,7 @@ impl> ModuleHomomorphism fn apply_to_basis_element( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar<::Algebra>, input_degree: i32, input_idx: usize, ) { @@ -98,7 +99,7 @@ impl> ModuleHomomorphism impl FullModuleHomomorphism where - A: Algebra, + A: Algebra, S: Module, T: Module, { @@ -204,13 +205,18 @@ where impl> ZeroHomomorphism for FullModuleHomomorphism +where + S::Algebra: Algebra, { fn zero_homomorphism(source: Arc, target: Arc, degree_shift: i32) -> Self { Self::new(source, target, degree_shift) } } -impl IdentityHomomorphism for FullModuleHomomorphism { +impl IdentityHomomorphism for FullModuleHomomorphism +where + S::Algebra: Algebra, +{ fn identity_homomorphism(source: Arc) -> Self { let p = source.prime(); let min_degree = source.min_degree(); diff --git a/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs b/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs index a28e2764b2..9c1ab3ee95 100644 --- a/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs @@ -1,10 +1,12 @@ use std::sync::Arc; -use fp::vector::FpSliceMut; - -use crate::module::{ - Module, - homomorphism::{ModuleHomomorphism, ZeroHomomorphism}, +use crate::{ + algebra::{BaseRingOf, Scalar}, + linear_algebra::{BaseSliceMutOf, GradedDvr}, + module::{ + Module, + homomorphism::{ModuleHomomorphism, ZeroHomomorphism}, + }, }; pub struct GenericZeroHomomorphism> { @@ -25,6 +27,8 @@ impl> GenericZeroHomomorphism { impl> ModuleHomomorphism for GenericZeroHomomorphism +where + BaseRingOf: GradedDvr, { type Source = S; type Target = T; @@ -41,11 +45,20 @@ impl> ModuleHomomorphism self.degree_shift } - fn apply_to_basis_element(&self, _: FpSliceMut, _: u32, _: i32, _: usize) {} + fn apply_to_basis_element( + &self, + _: BaseSliceMutOf<'_, ::Algebra>, + _: Scalar<::Algebra>, + _: i32, + _: usize, + ) { + } } impl> ZeroHomomorphism for GenericZeroHomomorphism +where + BaseRingOf: GradedDvr, { fn zero_homomorphism(source: Arc, target: Arc, degree_shift: i32) -> Self { Self::new(source, target, degree_shift) diff --git a/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs b/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs index 197050fb24..c86ed8ee9e 100644 --- a/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs +++ b/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs @@ -6,14 +6,21 @@ use fp::{ }; use once::OnceBiVec; -use crate::module::{ - FreeModule, HomModule, Module, - block_structure::GeneratorBasisEltPair, - homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, +use crate::{ + algebra::{Algebra, BaseRingOf, Field, Ring, Scalar}, + linear_algebra::GradedDvr, + module::{ + FreeModule, HomModule, Module, + block_structure::GeneratorBasisEltPair, + homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, + }, }; /// Given a map $\mathtt{map}: A \to B$ and hom modules $\mathtt{source} = \Hom(B, X)$, $\mathtt{target} = \Hom(A, X)$, produce the induced pullback map $\Hom(B, X) \to \Hom(A, X)$. -pub struct HomPullback { +pub struct HomPullback +where + BaseRingOf: GradedDvr, +{ source: Arc>, target: Arc>, map: Arc>>, @@ -22,7 +29,10 @@ pub struct HomPullback { quasi_inverses: OnceBiVec, } -impl HomPullback { +impl HomPullback +where + BaseRingOf: GradedDvr, +{ /// Fallible version of [`new`](Self::new). /// /// Returns `Err` unless `source`, `target` and `map` are wired together @@ -66,7 +76,10 @@ impl HomPullback { } } -impl ModuleHomomorphism for HomPullback { +impl ModuleHomomorphism for HomPullback +where + M::Algebra: Algebra, +{ type Source = HomModule; type Target = HomModule; @@ -89,7 +102,7 @@ impl ModuleHomomorphism for HomPullback { fn apply_to_basis_element( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar<::Algebra>, fn_degree: i32, fn_idx: usize, ) { @@ -130,7 +143,7 @@ impl ModuleHomomorphism for HomPullback { } target_module.act_by_element_on_basis( result.slice_mut(target_range.start, target_range.end), - coeff, + target_module.base_ring().embed_field(coeff), target_gen_deg - degree_shift - *generator_degree, slice, *generator_degree - fn_degree, diff --git a/ext/crates/algebra/src/module/homomorphism/mod.rs b/ext/crates/algebra/src/module/homomorphism/mod.rs index 540b34ac4b..185b84706b 100644 --- a/ext/crates/algebra/src/module/homomorphism/mod.rs +++ b/ext/crates/algebra/src/module/homomorphism/mod.rs @@ -1,12 +1,18 @@ use std::sync::Arc; use fp::{ - matrix::{AugmentedMatrix, Matrix, MatrixSliceMut, QuasiInverse, Subspace}, + matrix::{Matrix, MatrixSliceMut}, prime::ValidPrime, - vector::{FpSlice, FpSliceMut}, }; -use crate::module::Module; +use crate::{ + algebra::{Algebra, BaseRingOf, Field, Ring, Scalar}, + linear_algebra::{ + BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, GradedDvr, QuasiInverseOf, + SubmoduleOf, + }, + module::Module, +}; mod free_module_homomorphism; mod full_module_homomorphism; @@ -39,7 +45,10 @@ pub use quotient_homomorphism::{QuotientHomomorphism, QuotientHomomorphismSource /// /// Note that an instance of a `ModuleHomomorphism` need not have the data available, even after /// `compute_auxiliary_data_through_degree` is invoked. -pub trait ModuleHomomorphism: Send + Sync { +pub trait ModuleHomomorphism: Send + Sync +where + BaseRingOf<::Algebra>: GradedDvr, +{ type Source: Module; type Target: Module::Algebra>; @@ -52,34 +61,43 @@ pub trait ModuleHomomorphism: Send + Sync { /// usually the case because of out-of-bounds errors. fn apply_to_basis_element( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, ::Algebra>, + coeff: Scalar<::Algebra>, input_degree: i32, input_idx: usize, ); #[allow(unused_variables)] - fn kernel(&self, degree: i32) -> Option<&Subspace> { + fn kernel(&self, degree: i32) -> Option<&SubmoduleOf<::Algebra>> { None } #[allow(unused_variables)] - fn quasi_inverse(&self, degree: i32) -> Option<&QuasiInverse> { + fn quasi_inverse( + &self, + degree: i32, + ) -> Option<&QuasiInverseOf<::Algebra>> { None } #[allow(unused_variables)] - fn image(&self, degree: i32) -> Option<&Subspace> { + fn image(&self, degree: i32) -> Option<&SubmoduleOf<::Algebra>> { None } #[allow(unused_variables)] fn compute_auxiliary_data_through_degree(&self, degree: i32) {} - fn apply(&self, mut result: FpSliceMut, coeff: u32, input_degree: i32, input: FpSlice) { - let p = self.prime(); + fn apply( + &self, + mut result: BaseSliceMutOf<'_, ::Algebra>, + coeff: Scalar<::Algebra>, + input_degree: i32, + input: BaseSliceOf<'_, ::Algebra>, + ) { + let ring = self.target().base_ring(); for (i, v) in input.iter_nonzero() { - self.apply_to_basis_element(result.copy(), (coeff * v) % p, input_degree, i); + self.apply_to_basis_element(result.reborrow(), ring.mul(coeff, v), input_degree, i); } } @@ -87,39 +105,47 @@ pub trait ModuleHomomorphism: Send + Sync { self.source().prime() } + /// The base ring the homomorphism is linear over (a `Copy` handle, returned by value). + fn base_ring(&self) -> BaseRingOf<::Algebra> { + self.source().base_ring() + } + fn min_degree(&self) -> i32 { self.source().min_degree() } /// Compute the auxiliary data associated to the homomorphism at input degree `degree`. Returns /// it in the order image, kernel, quasi_inverse - fn auxiliary_data(&self, degree: i32) -> (Subspace, Subspace, QuasiInverse) { - let p = self.prime(); + fn auxiliary_data( + &self, + degree: i32, + ) -> ( + SubmoduleOf<::Algebra>, + SubmoduleOf<::Algebra>, + QuasiInverseOf<::Algebra>, + ) { let output_degree = degree - self.degree_shift(); self.source().compute_basis(degree); self.target().compute_basis(output_degree); let source_dimension = self.source().dimension(degree); let target_dimension = self.target().dimension(output_degree); - let mut matrix = - AugmentedMatrix::<2>::new(p, source_dimension, [target_dimension, source_dimension]); - - self.get_matrix(matrix.segment(0, 0), degree); - matrix.segment(1, 1).add_identity(); - - matrix.row_reduce(); - - ( - matrix.compute_image(), - matrix.compute_kernel(), - matrix.compute_quasi_inverse(), - ) + let ring = self.base_ring(); + ring.image_kernel_quasi_inverse(source_dimension, target_dimension, |i, row| { + self.apply_to_basis_element(row, ring.one(), degree, i); + }) } /// Write the matrix of the homomorphism at input degree `degree` to `matrix`. /// /// The (sliced) dimensions of `matrix` must be equal to source_dimension x /// target_dimension - fn get_matrix(&self, mut matrix: MatrixSliceMut, degree: i32) { + /// + /// This works with `fp`'s concrete matrices, so it is available only when the base ring is a + /// field. + fn get_matrix(&self, mut matrix: MatrixSliceMut, degree: i32) + where + ::Algebra: Algebra, + { assert_eq!(self.source().dimension(degree), matrix.rows()); assert_eq!( self.target().dimension(degree - self.degree_shift()), @@ -133,11 +159,18 @@ pub trait ModuleHomomorphism: Send + Sync { matrix .maybe_par_iter_mut() .enumerate() - .for_each(|(i, row)| self.apply_to_basis_element(row, 1, degree, i)); + .for_each(|(i, row)| { + self.apply_to_basis_element(row, self.target().base_ring().one(), degree, i) + }); } /// Get the values of the homomorphism on the specified inputs to `matrix`. - fn get_partial_matrix(&self, degree: i32, inputs: &[usize]) -> Matrix { + /// + /// Available only when the base ring is a field (it produces a concrete `fp` matrix). + fn get_partial_matrix(&self, degree: i32, inputs: &[usize]) -> Matrix + where + ::Algebra: Algebra, + { let mut matrix = Matrix::new(self.prime(), inputs.len(), self.target().dimension(degree)); if matrix.columns() == 0 { @@ -147,7 +180,9 @@ pub trait ModuleHomomorphism: Send + Sync { matrix .maybe_par_iter_mut() .enumerate() - .for_each(|(i, row)| self.apply_to_basis_element(row, 1, degree, inputs[i])); + .for_each(|(i, row)| { + self.apply_to_basis_element(row, self.target().base_ring().one(), degree, inputs[i]) + }); matrix } @@ -155,9 +190,15 @@ pub trait ModuleHomomorphism: Send + Sync { /// Attempt to apply quasi inverse to the input. Returns whether the operation was /// successful. This is required to either always succeed or always fail for each degree. #[must_use] - fn apply_quasi_inverse(&self, result: FpSliceMut, degree: i32, input: FpSlice) -> bool { + fn apply_quasi_inverse( + &self, + result: BaseSliceMutOf<'_, ::Algebra>, + degree: i32, + input: BaseSliceOf<'_, ::Algebra>, + ) -> bool { if let Some(qi) = self.quasi_inverse(degree) { - qi.apply(result, 1, input); + let ring = self.base_ring(); + ring.apply_quasi_inverse(qi, result, ring.one(), input); true } else { false @@ -167,10 +208,15 @@ pub trait ModuleHomomorphism: Send + Sync { pub trait ZeroHomomorphism>: ModuleHomomorphism +where + BaseRingOf: GradedDvr, { fn zero_homomorphism(s: Arc, t: Arc, degree_shift: i32) -> Self; } -pub trait IdentityHomomorphism: ModuleHomomorphism { +pub trait IdentityHomomorphism: ModuleHomomorphism +where + BaseRingOf: GradedDvr, +{ fn identity_homomorphism(s: Arc) -> Self; } diff --git a/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs b/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs index e815b17304..0b2b9956c6 100644 --- a/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/quotient_homomorphism.rs @@ -2,7 +2,10 @@ use std::sync::Arc; use fp::vector::{FpSliceMut, FpVector}; -use crate::module::{Module, QuotientModule, homomorphism::ModuleHomomorphism}; +use crate::{ + algebra::{Algebra, Field, Scalar}, + module::{Module, QuotientModule, homomorphism::ModuleHomomorphism}, +}; pub struct QuotientHomomorphism { f: Arc, @@ -20,7 +23,10 @@ impl QuotientHomomorphism { } } -impl ModuleHomomorphism for QuotientHomomorphism { +impl ModuleHomomorphism for QuotientHomomorphism +where + ::Algebra: Algebra, +{ type Source = QuotientModule; type Target = QuotientModule; @@ -39,7 +45,7 @@ impl ModuleHomomorphism for QuotientHomomorphism { fn apply_to_basis_element( &self, result: FpSliceMut, - coeff: u32, + coeff: Scalar<::Algebra>, input_degree: i32, input_idx: usize, ) { @@ -69,7 +75,10 @@ impl QuotientHomomorphismSource { } } -impl ModuleHomomorphism for QuotientHomomorphismSource { +impl ModuleHomomorphism for QuotientHomomorphismSource +where + ::Algebra: Algebra, +{ type Source = QuotientModule; type Target = F::Target; @@ -88,7 +97,7 @@ impl ModuleHomomorphism for QuotientHomomorphismSource fn apply_to_basis_element( &self, result: FpSliceMut, - coeff: u32, + coeff: Scalar<::Algebra>, input_degree: i32, input_idx: usize, ) { diff --git a/ext/crates/algebra/src/module/module_trait.rs b/ext/crates/algebra/src/module/module_trait.rs index 9ef4ee6400..705ecf343f 100644 --- a/ext/crates/algebra/src/module/module_trait.rs +++ b/ext/crates/algebra/src/module/module_trait.rs @@ -1,13 +1,13 @@ use std::sync::Arc; use auto_impl::auto_impl; -use fp::{ - prime::ValidPrime, - vector::{FpSlice, FpSliceMut}, -}; +use fp::{prime::ValidPrime, vector::FpSlice}; use itertools::Itertools; -use crate::algebra::Algebra; +use crate::{ + algebra::{Algebra, BaseRingOf, Ring, Scalar}, + linear_algebra::{BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf}, +}; /// A bounded below module over an algebra. /// @@ -32,6 +32,16 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { /// The algebra the module is over. fn algebra(&self) -> Arc; + /// The base ring the module is linear over. + /// + /// This is a `Copy` handle, so it is returned by value and coefficient code can hold it without + /// touching the algebra's `Arc`. The default forwards through [`Module::algebra`]; modules that + /// store their algebra as an `Arc` field should override it to borrow that field instead of + /// cloning the `Arc`. + fn base_ring(&self) -> BaseRingOf { + self.algebra().base_ring() + } + /// The minimum degree of the module, which is required to be bounded below fn min_degree(&self) -> i32; @@ -52,8 +62,8 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { fn dimension(&self, degree: i32) -> usize; fn act_on_basis( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -65,8 +75,8 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { /// panicking. On success it delegates to [`Module::act_on_basis`] and returns `Ok(())`. fn try_act_on_basis( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -108,12 +118,12 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { /// panicking. On success it delegates to [`Module::act`] and returns `Ok(())`. fn try_act( &self, - result: FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, input_degree: i32, - input: FpSlice, + input: BaseSliceOf<'_, Self::Algebra>, ) -> Result<(), ActError> { if op_degree < 0 { return Err(ActError::IndexOutOfRange(format!( @@ -188,19 +198,19 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { /// what generators will be added in degree `t` yet. fn act( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, input_degree: i32, - input: FpSlice, + input: BaseSliceOf<'_, Self::Algebra>, ) { assert!(input.len() <= self.dimension(input_degree)); - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in input.iter_nonzero() { self.act_on_basis( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), op_degree, op_index, input_degree, @@ -211,20 +221,20 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { fn act_by_element( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, - op: FpSlice, + op: BaseSliceOf<'_, Self::Algebra>, input_degree: i32, - input: FpSlice, + input: BaseSliceOf<'_, Self::Algebra>, ) { assert_eq!(input.len(), self.dimension(input_degree)); assert_eq!(op.len(), self.algebra().dimension(op_degree)); - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in op.iter_nonzero() { self.act( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), op_degree, i, input_degree, @@ -235,19 +245,19 @@ pub trait Module: std::fmt::Display + std::any::Any + Send + Sync { fn act_by_element_on_basis( &self, - mut result: FpSliceMut, - coeff: u32, + mut result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, - op: FpSlice, + op: BaseSliceOf<'_, Self::Algebra>, input_degree: i32, input_index: usize, ) { assert_eq!(op.len(), self.algebra().dimension(op_degree)); - let p = self.prime(); + let ring = self.base_ring(); for (i, v) in op.iter_nonzero() { self.act_on_basis( - result.copy(), - (coeff * v) % p, + result.reborrow(), + ring.mul(coeff, v), op_degree, i, input_degree, diff --git a/ext/crates/algebra/src/module/quotient_module.rs b/ext/crates/algebra/src/module/quotient_module.rs index ba983bce69..897b9a2ac5 100644 --- a/ext/crates/algebra/src/module/quotient_module.rs +++ b/ext/crates/algebra/src/module/quotient_module.rs @@ -7,7 +7,10 @@ use fp::{ vector::{FpSlice, FpSliceMut, FpVector}, }; -use crate::module::{Module, ZeroModule}; +use crate::{ + algebra::{Algebra, Field, Scalar}, + module::{Module, ZeroModule}, +}; /// A quotient of a module truncated below a fix degree. pub struct QuotientModule { @@ -28,7 +31,10 @@ impl std::fmt::Display for QuotientModule { } } -impl QuotientModule { +impl QuotientModule +where + M::Algebra: Algebra, +{ /// Fallible version of [`new`](Self::new). /// /// Returns `Err` when the allocation span `truncation + 1 - min_degree` is @@ -123,7 +129,7 @@ impl QuotientModule { pub fn act_on_original_basis( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -158,7 +164,10 @@ impl QuotientModule { } } -impl Module for QuotientModule { +impl Module for QuotientModule +where + M::Algebra: Algebra, +{ type Algebra = M::Algebra; fn algebra(&self) -> Arc { @@ -184,7 +193,7 @@ impl Module for QuotientModule { fn act_on_basis( &self, result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -221,7 +230,10 @@ impl Module for QuotientModule { } } -impl ZeroModule for QuotientModule { +impl ZeroModule for QuotientModule +where + M::Algebra: Algebra, +{ fn zero_module(algebra: Arc, min_degree: i32) -> Self { Self::new(Arc::new(M::zero_module(algebra, min_degree)), min_degree) } diff --git a/ext/crates/algebra/src/module/rpn.rs b/ext/crates/algebra/src/module/rpn.rs index 6d42a1f9f8..dfeb500f1d 100644 --- a/ext/crates/algebra/src/module/rpn.rs +++ b/ext/crates/algebra/src/module/rpn.rs @@ -9,7 +9,7 @@ use serde_json::Value; use crate::{ algebra::{ - AdemAlgebra, Algebra, MilnorAlgebra, SteenrodAlgebra, + AdemAlgebra, Algebra, Field, MilnorAlgebra, Ring, Scalar, SteenrodAlgebra, adem_algebra::AdemBasisElement, milnor_algebra::{MilnorBasisElement, PPartEntry}, }, @@ -54,7 +54,7 @@ impl PartialEq for RealProjectiveSpace { impl Eq for RealProjectiveSpace {} -impl Module for RealProjectiveSpace +impl> Module for RealProjectiveSpace where for<'a> &'a A: TryInto<&'a SteenrodAlgebra>, { @@ -102,7 +102,7 @@ where fn act_on_basis( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -113,7 +113,7 @@ where let output_degree = mod_degree + op_degree; - if op_degree == 0 || coeff == 0 || self.dimension(output_degree) == 0 { + if op_degree == 0 || self.base_ring().is_zero(coeff) || self.dimension(output_degree) == 0 { return; } @@ -175,7 +175,7 @@ fn coef_milnor(algebra: &MilnorAlgebra, op_deg: i32, op_idx: usize, mut mod_degr PPartEntry::multinomial2(&list) == 1 } -impl ZeroModule for RealProjectiveSpace +impl> ZeroModule for RealProjectiveSpace where for<'a> &'a A: TryInto<&'a SteenrodAlgebra>, { diff --git a/ext/crates/algebra/src/module/suspension_module.rs b/ext/crates/algebra/src/module/suspension_module.rs index 8b92ac1f5e..bb67ac1c3e 100644 --- a/ext/crates/algebra/src/module/suspension_module.rs +++ b/ext/crates/algebra/src/module/suspension_module.rs @@ -1,6 +1,10 @@ use std::sync::Arc; -use crate::module::{Module, ZeroModule}; +use crate::{ + algebra::Scalar, + linear_algebra::{BaseSliceMutOf, BaseSliceOf}, + module::{Module, ZeroModule}, +}; pub struct SuspensionModule { inner: Arc, @@ -53,12 +57,12 @@ impl Module for SuspensionModule { fn act( &self, - result: fp::vector::FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, input_degree: i32, - input: fp::vector::FpSlice, + input: BaseSliceOf<'_, Self::Algebra>, ) { self.inner.act( result, @@ -72,12 +76,12 @@ impl Module for SuspensionModule { fn act_by_element( &self, - result: fp::vector::FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, - op: fp::vector::FpSlice, + op: BaseSliceOf<'_, Self::Algebra>, input_degree: i32, - input: fp::vector::FpSlice, + input: BaseSliceOf<'_, Self::Algebra>, ) { self.inner.act_by_element( result, @@ -91,10 +95,10 @@ impl Module for SuspensionModule { fn act_by_element_on_basis( &self, - result: fp::vector::FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, - op: fp::vector::FpSlice, + op: BaseSliceOf<'_, Self::Algebra>, input_degree: i32, input_index: usize, ) { @@ -130,8 +134,8 @@ impl Module for SuspensionModule { fn act_on_basis( &self, - result: fp::vector::FpSliceMut, - coeff: u32, + result: BaseSliceMutOf<'_, Self::Algebra>, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, diff --git a/ext/crates/algebra/src/module/tensor_module.rs b/ext/crates/algebra/src/module/tensor_module.rs index 64f4def256..ad28842ddd 100644 --- a/ext/crates/algebra/src/module/tensor_module.rs +++ b/ext/crates/algebra/src/module/tensor_module.rs @@ -8,7 +8,7 @@ use fp::{ use once::OnceBiVec; use crate::{ - algebra::{Algebra, Bialgebra}, + algebra::{Algebra, Bialgebra, Field, Ring, Scalar}, module::{Module, ZeroModule, block_structure::BlockStructure}, }; @@ -29,7 +29,7 @@ impl> std::fmt::Display for TensorMod impl TensorModule where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, M: Module, N: Module, { @@ -56,7 +56,7 @@ where fn act_helper( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -66,6 +66,7 @@ where let p = self.prime(); let coproduct = algebra.coproduct(op_degree, op_index).into_iter(); + let ring = algebra.base_ring(); let output_degree = mod_degree + op_degree; let mut left_result = FpVector::new(p, 0); @@ -120,7 +121,7 @@ where } self.right.act_on_basis( right_result.as_slice_mut(), - entry, + ring.embed_field(entry), op_deg_r, op_idx_r, right_deg, @@ -147,7 +148,7 @@ where } impl Module for TensorModule where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, M: Module, N: Module, { @@ -193,7 +194,7 @@ where fn act_on_basis( &self, result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -215,7 +216,7 @@ where fn act( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -304,7 +305,7 @@ where impl ZeroModule for TensorModule where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, M: Module + ZeroModule, N: Module + ZeroModule, { From 7ec6ebb5cdeea6e185845c7e062bca33933d3ff8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 20:38:11 +0000 Subject: [PATCH 2/7] Wire the resolution engine onto the base ring's linear algebra Lift the resolution step's inline linear algebra -- building the differential matrix, computing its kernel, and constructing the next stage -- onto the `GradedDvr` base-ring trait, and dispatch `step_resolution` through those methods instead of calling `fp` directly. `ChainComplex::Algebra` is bounded `BaseRing = Field`, so the classical engine monomorphizes to the existing `fp` code path (benchmarks are bit-identical); relaxing that single bound is the entry point for a non-field base ring. The base-ring scalar is threaded through the chain-complex, chain-homotopy, Yoneda and secondary layers, the resolution homomorphism, and the examples; the generic secondary-homotopy type carries the `GradedDvr` bound it needs to store a free-module homomorphism. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNkiiZghHggyMwDWfDn1y5 --- ext/examples/sq0.rs | 25 ++- ext/examples/steenrod.rs | 44 ++-- ext/src/chain_complex/chain_homotopy.rs | 26 ++- ext/src/chain_complex/finite_chain_complex.rs | 26 ++- ext/src/chain_complex/mod.rs | 10 +- ext/src/resolution.rs | 204 +++++------------- ext/src/resolution_homomorphism.rs | 9 +- ext/src/secondary.rs | 30 ++- ext/src/yoneda.rs | 33 ++- 9 files changed, 196 insertions(+), 211 deletions(-) diff --git a/ext/examples/sq0.rs b/ext/examples/sq0.rs index dc671c159b..61c8ca7c9f 100644 --- a/ext/examples/sq0.rs +++ b/ext/examples/sq0.rs @@ -77,11 +77,11 @@ mod double { mod double_algebra { use algebra::{ - AdemAlgebra, Algebra, MilnorAlgebra, SteenrodAlgebra, adem_algebra::AdemBasisElement, - milnor_algebra::MilnorBasisElement, + AdemAlgebra, Algebra, Field, MilnorAlgebra, SteenrodAlgebra, + adem_algebra::AdemBasisElement, milnor_algebra::MilnorBasisElement, }; - pub trait DoubleAlgebra: Algebra { + pub trait DoubleAlgebra: Algebra { /// `degree` is guaranteed to be even fn halve(&self, degree: i32, idx: usize) -> Option; } @@ -144,7 +144,10 @@ mod double { pub mod double_module { use std::sync::Arc; - use algebra::module::{Module, homomorphism::ModuleHomomorphism}; + use algebra::{ + Scalar, + module::{Module, homomorphism::ModuleHomomorphism}, + }; use fp::{ matrix::{Matrix, MatrixSliceMut, QuasiInverse, Subspace}, vector::{FpSlice, FpSliceMut}, @@ -200,7 +203,7 @@ mod double { fn act_on_basis( &self, result: fp::vector::FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -254,7 +257,7 @@ mod double { fn act( &self, result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, input_degree: i32, @@ -331,7 +334,7 @@ mod double { fn apply_to_basis_element( &self, result: FpSliceMut, - coeff: u32, + coeff: Scalar<::Algebra>, input_degree: i32, input_idx: usize, ) { @@ -339,7 +342,13 @@ mod double { .apply_to_basis_element(result, coeff, input_degree / 2, input_idx) } - fn apply(&self, result: FpSliceMut, coeff: u32, input_degree: i32, input: FpSlice) { + fn apply( + &self, + result: FpSliceMut, + coeff: Scalar<::Algebra>, + input_degree: i32, + input: FpSlice, + ) { if input_degree % 2 == 0 { self.inner.apply(result, coeff, input_degree / 2, input) } diff --git a/ext/examples/steenrod.rs b/ext/examples/steenrod.rs index e94b301d6e..a9d06612be 100644 --- a/ext/examples/steenrod.rs +++ b/ext/examples/steenrod.rs @@ -189,9 +189,12 @@ fn main() -> anyhow::Result<()> { mod sum_module { use std::sync::Arc; - use algebra::module::{ - Module, ZeroModule, - block_structure::{BlockStructure, GeneratorBasisEltPair}, + use algebra::{ + Algebra, Field, Scalar, + module::{ + Module, ZeroModule, + block_structure::{BlockStructure, GeneratorBasisEltPair}, + }, }; use bivec::BiVec; use fp::vector::FpSliceMut; @@ -242,7 +245,10 @@ mod sum_module { } } - impl Module for SumModule { + impl Module for SumModule + where + M::Algebra: Algebra, + { type Algebra = M::Algebra; fn algebra(&self) -> Arc { @@ -279,7 +285,7 @@ mod sum_module { fn act_on_basis( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar, op_degree: i32, op_index: usize, mod_degree: i32, @@ -323,7 +329,10 @@ mod sum_module { } } - impl ZeroModule for SumModule { + impl ZeroModule for SumModule + where + M::Algebra: Algebra, + { fn zero_module(algebra: Arc, min_degree: i32) -> Self { Self::new(algebra, vec![], min_degree) } @@ -378,7 +387,7 @@ mod tensor_product_chain_complex { use std::sync::Arc; use algebra::{ - Algebra, Bialgebra, + Algebra, Bialgebra, Field, Ring, Scalar, module::{Module, TensorModule, ZeroModule, homomorphism::ModuleHomomorphism}, }; use ext::chain_complex::ChainComplex; @@ -395,7 +404,7 @@ mod tensor_product_chain_complex { pub struct TensorChainComplex where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, CC1: ChainComplex, CC2: ChainComplex, { @@ -408,7 +417,7 @@ mod tensor_product_chain_complex { impl TensorChainComplex where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, CC1: ChainComplex, CC2: ChainComplex, { @@ -444,7 +453,7 @@ mod tensor_product_chain_complex { impl TensorChainComplex where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, CC: ChainComplex, { /// This function sends a (x) b to b (x) a. This makes sense only if left_cc and right_cc are @@ -491,7 +500,7 @@ mod tensor_product_chain_complex { impl ChainComplex for TensorChainComplex where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, CC1: ChainComplex, CC2: ChainComplex, { @@ -584,7 +593,7 @@ mod tensor_product_chain_complex { pub struct TensorChainMap where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, CC1: ChainComplex, CC2: ChainComplex, { @@ -598,7 +607,7 @@ mod tensor_product_chain_complex { impl ModuleHomomorphism for TensorChainMap where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, CC1: ChainComplex, CC2: ChainComplex, { @@ -621,7 +630,7 @@ mod tensor_product_chain_complex { fn apply_to_basis_element( &self, mut result: FpSliceMut, - coeff: u32, + coeff: Scalar<::Algebra>, degree: i32, input_idx: usize, ) { @@ -706,7 +715,7 @@ mod tensor_product_chain_complex { impl TensorChainMap where - A: Algebra + Bialgebra, + A: Algebra + Bialgebra, CC1: ChainComplex, CC2: ChainComplex, { @@ -716,6 +725,7 @@ mod tensor_product_chain_complex { degree: i32, ) -> Vec>> { let p = self.prime(); + let ring = self.left_cc.base_ring(); // start, end, preimage let mut quasi_inverse_list: Vec>> = vec![None; self.target.dimension(degree)]; @@ -759,7 +769,7 @@ mod tensor_product_chain_complex { for ri in 0..source_right_dim { self.right_cc .differential(self.source_s - s) - .apply_to_basis_element(result.as_slice_mut(), 1, right_t, ri); + .apply_to_basis_element(result.as_slice_mut(), ring.one(), right_t, ri); for li in 0..source_left_dim { let mut row = matrix.row_mut(row_count + li * source_right_dim + ri); row.slice_mut( @@ -794,7 +804,7 @@ mod tensor_product_chain_complex { for li in 0..source_left_dim { self.left_cc.differential(s).apply_to_basis_element( result.as_slice_mut(), - 1, + ring.one(), left_t, li, ); diff --git a/ext/src/chain_complex/chain_homotopy.rs b/ext/src/chain_complex/chain_homotopy.rs index c0ec0fd0d2..4222c55d59 100644 --- a/ext/src/chain_complex/chain_homotopy.rs +++ b/ext/src/chain_complex/chain_homotopy.rs @@ -1,8 +1,11 @@ use std::sync::{Arc, Mutex}; -use algebra::module::{ - Module, - homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, +use algebra::{ + Ring, + module::{ + Module, + homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, + }, }; use fp::{prime::ValidPrime, vector::FpVector}; use maybe_rayon::prelude::*; @@ -154,6 +157,7 @@ impl< fn extend_step(&self, source: Bidegree) -> std::ops::Range { let p = self.prime(); + let ring = self.left.source.base_ring(); let shift = self.shift(); let target = source + Bidegree::s_t(1, 0) - shift; @@ -205,7 +209,7 @@ impl< let left_shifted_b = source - self.left.shift; self.right.get_map(left_shifted_b.s()).apply( scratch.as_slice_mut(), - 1, + ring.one(), left_shifted_b.t(), self.left .get_map(source.s()) @@ -215,7 +219,7 @@ impl< self.homotopies[source.s() - 1].apply( scratch.as_slice_mut(), - p - 1, + ring.embed_field(p - 1), source.t(), self.left .source @@ -240,7 +244,7 @@ impl< ); self.right.target.differential(target.s() - 1).apply( r.as_slice_mut(), - 1, + ring.one(), target.t(), scratch.as_slice(), ); @@ -294,6 +298,7 @@ pub(crate) mod secondary { use std::sync::Arc; use algebra::{ + Algebra, Ring, module::{Module, homomorphism::ModuleHomomorphism}, pair_algebra::PairAlgebra, }; @@ -408,6 +413,7 @@ pub(crate) mod secondary { fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { let p = self.prime(); + let ring = self.algebra().base_ring(); let neg_1 = p - 1; let shifted_b = g.degree() - self.shift(); @@ -439,7 +445,7 @@ pub(crate) mod secondary { self.underlying.homotopy(g.s() - 2).apply( result.as_slice_mut(), - neg_1, + ring.embed_field(neg_1), g.t() - 1, self.left.secondary_source().homotopies()[g.s()] .homotopies @@ -465,7 +471,7 @@ pub(crate) mod secondary { if let Some(right_lambda) = &self.right_lambda { right_lambda.get_map(left_shifted_b.s()).apply( result.as_slice_mut(), - neg_1, + ring.embed_field(neg_1), left_shifted_b.t(), self.left .underlying() @@ -480,7 +486,7 @@ pub(crate) mod secondary { .get_map(left_shifted_b.s() - 1) .apply( result.as_slice_mut(), - neg_1, + ring.embed_field(neg_1), left_shifted_b.t() - 1, self.left.homotopies()[g.s()] .homotopies @@ -494,7 +500,7 @@ pub(crate) mod secondary { .get_map(left_shifted_b.s() - 1) .apply( result.as_slice_mut(), - neg_1, + ring.embed_field(neg_1), left_shifted_b.t() - 1, left_lambda.get_map(g.s()).output(g.t(), g.idx()).as_slice(), ); diff --git a/ext/src/chain_complex/finite_chain_complex.rs b/ext/src/chain_complex/finite_chain_complex.rs index e5a068ffd0..3d71f6c5e1 100644 --- a/ext/src/chain_complex/finite_chain_complex.rs +++ b/ext/src/chain_complex/finite_chain_complex.rs @@ -1,8 +1,11 @@ use std::sync::Arc; -use algebra::module::{ - Module, ZeroModule, - homomorphism::{FullModuleHomomorphism, ModuleHomomorphism, ZeroHomomorphism}, +use algebra::{ + Algebra, Field, + module::{ + Module, ZeroModule, + homomorphism::{FullModuleHomomorphism, ModuleHomomorphism, ZeroHomomorphism}, + }, }; use sseq::coordinates::Bidegree; @@ -11,6 +14,7 @@ use crate::chain_complex::{AugmentedChainComplex, BoundedChainComplex, ChainComp pub struct FiniteChainComplex> where M: Module, + M::Algebra: Algebra, F: ModuleHomomorphism, { modules: Vec>, @@ -21,6 +25,7 @@ where impl FiniteChainComplex where M: Module + ZeroModule, + M::Algebra: Algebra, F: ModuleHomomorphism + ZeroHomomorphism, { pub fn new(modules: Vec>, differentials: Vec>) -> Self { @@ -57,6 +62,7 @@ where impl FiniteChainComplex where M: Module, + M::Algebra: Algebra, F: ModuleHomomorphism + ZeroHomomorphism, { pub fn pop(&mut self) { @@ -83,7 +89,10 @@ where } } -impl FiniteChainComplex> { +impl FiniteChainComplex> +where + M::Algebra: Algebra, +{ pub fn map>( &self, mut f: impl FnMut(&M) -> N, @@ -123,6 +132,7 @@ impl FiniteChainComplex> { impl ChainComplex for FiniteChainComplex where M: Module, + M::Algebra: Algebra, F: ModuleHomomorphism, { type Algebra = M::Algebra; @@ -174,6 +184,7 @@ where impl BoundedChainComplex for FiniteChainComplex where M: Module, + M::Algebra: Algebra, F: ModuleHomomorphism, { fn max_s(&self) -> i32 { @@ -184,6 +195,7 @@ where pub struct FiniteAugmentedChainComplex where M: Module, + M::Algebra: Algebra, CC: ChainComplex, F1: ModuleHomomorphism, F2: ModuleHomomorphism, @@ -196,6 +208,7 @@ where impl ChainComplex for FiniteAugmentedChainComplex where M: Module, + M::Algebra: Algebra, CC: ChainComplex, F1: ModuleHomomorphism, F2: ModuleHomomorphism, @@ -246,6 +259,7 @@ impl > where M: Module, + M::Algebra: Algebra, CC: ChainComplex, { pub fn map>( @@ -272,6 +286,7 @@ where impl AugmentedChainComplex for FiniteAugmentedChainComplex where M: Module, + M::Algebra: Algebra, CC: ChainComplex, F1: ModuleHomomorphism, F2: ModuleHomomorphism, @@ -292,6 +307,7 @@ where impl From> for FiniteChainComplex where M: Module, + M::Algebra: Algebra, CC: ChainComplex, F1: ModuleHomomorphism, F2: ModuleHomomorphism, @@ -304,6 +320,7 @@ where impl BoundedChainComplex for FiniteAugmentedChainComplex where M: Module, + M::Algebra: Algebra, CC: ChainComplex, F1: ModuleHomomorphism, F2: ModuleHomomorphism, @@ -316,6 +333,7 @@ where impl FiniteChainComplex where M: Module, + M::Algebra: Algebra, F1: ModuleHomomorphism, { pub fn augment< diff --git a/ext/src/chain_complex/mod.rs b/ext/src/chain_complex/mod.rs index cf9b275801..569e2f3abc 100644 --- a/ext/src/chain_complex/mod.rs +++ b/ext/src/chain_complex/mod.rs @@ -4,7 +4,7 @@ mod finite_chain_complex; use std::sync::Arc; use algebra::{ - Algebra, MuAlgebra, + Algebra, BaseRingOf, Field, MuAlgebra, module::{ Module, MuFreeModule, homomorphism::{ModuleHomomorphism, MuFreeModuleHomomorphism}, @@ -192,7 +192,7 @@ where /// A chain complex is defined to start in degree 0. The min_degree is the min_degree of the /// modules in the chain complex, all of which must be the same. pub trait ChainComplex: Send + Sync { - type Algebra: Algebra; + type Algebra: Algebra; type Module: Module; type Homomorphism: ModuleHomomorphism; @@ -201,6 +201,12 @@ pub trait ChainComplex: Send + Sync { } fn algebra(&self) -> Arc; + + /// The base ring the chain complex is linear over (a `Copy` handle, returned by value). + fn base_ring(&self) -> BaseRingOf { + self.algebra().base_ring() + } + fn min_degree(&self) -> i32; fn zero_module(&self) -> Arc; fn module(&self, homological_degree: i32) -> Arc; diff --git a/ext/src/resolution.rs b/ext/src/resolution.rs index d37edd20ed..0893e9c46f 100644 --- a/ext/src/resolution.rs +++ b/ext/src/resolution.rs @@ -3,7 +3,8 @@ use std::sync::{Arc, Mutex, mpsc}; use algebra::{ - Algebra, MuAlgebra, + Algebra, Field, GradedDvr, MuAlgebra, Ring, + linear_algebra::NextStageInput, module::{ Module, MuFreeModule, homomorphism::{ModuleHomomorphism, MuFreeModuleHomomorphism}, @@ -353,6 +354,7 @@ where } let p = self.prime(); + let ring = self.base_ring(); // current_chain_map // X_{s, t} --------------------> C_{s, t} @@ -478,26 +480,25 @@ where return; } - let mut matrix = AugmentedMatrix::<3>::new_with_capacity( - p, - source_dimension, - &[target_cc_dimension, target_res_dimension, source_dimension], - source_dimension + MAX_NEW_GENS, - MAX_NEW_GENS, - ); - // Get the map (d, f) : X_{s, t} -> X_{s-1, t} (+) C_{s, t} into matrix + let field = Field::new(p); - { + // Realize the step's chain map `f` and differential `d` as a vector-space map over the base + // ring (block 2). + let matrix = { let _guard = ParallelGuard::new(); - current_chain_map.get_matrix(matrix.segment(0, 0), b.t()); - current_differential.get_matrix(matrix.segment(1, 1), b.t()); - } - matrix.segment(2, 2).add_identity(); - - matrix.row_reduce(); + field.differential_matrix( + source_dimension, + target_cc_dimension, + target_res_dimension, + MAX_NEW_GENS, + |i, row| current_chain_map.apply_to_basis_element(row, ring.one(), b.t(), i), + |i, row| current_differential.apply_to_basis_element(row, ring.one(), b.t(), i), + ) + }; + // Compute the kernel (block 3), caching it for the next homological degree. if !self.has_computed_bidegree(b + Bidegree::s_t(1, 0)) { - let kernel = matrix.compute_kernel(); + let kernel = field.step_kernel(&matrix); if self.should_save && let Some(dir) = self.save_dir.write() { @@ -514,142 +515,43 @@ where self.kernels.insert(b, kernel); } - // Now add generators to surject onto C_{s, t}. - // (For now we are just adding the eventual images of the new generators into matrix, we will update - // X_{s,t} and f later). - // We record which pivots exactly we added so that we can walk over the added genrators in a moment and - // work out what dX should to to each of them. - let cc_new_gens = matrix.extend_to_surjection(0, target_cc_dimension, MAX_NEW_GENS); - - let mut res_new_gens = Vec::new(); - - if b.s() > 0 { - if !cc_new_gens.is_empty() { - // Now we need to make sure that we have a chain homomorphism. Each generator x we just added to - // X_{s,t} has a nontrivial image f(x) \in C_{s,t}. We need to set d(x) so that f(dX(x)) = dC(f(x)). - // So we set dX(x) = f^{-1}(dC(f(x))) - let prev_chain_map = self.chain_map(b.s() - 1); - let quasi_inverse = prev_chain_map.quasi_inverse(b.t()).unwrap(); - - let dfx_dim = complex_cur_differential.target().dimension(b.t()); - let mut dfx = FpVector::new(self.prime(), dfx_dim); - - for (i, &column) in cc_new_gens.iter().enumerate() { - complex_cur_differential.apply_to_basis_element( - dfx.as_slice_mut(), - 1, - b.t(), - column, - ); - quasi_inverse.apply( - matrix.row_segment_mut(source_dimension + i, 1, 1), - 1, - dfx.as_slice(), - ); - dfx.set_to_zero(); - } - } - - // Now we add new generators to hit any cycles in old_kernel that we don't want in our homology. - // - // At this point the matrix is not quite row reduced and the pivots are not correct. - // However, extend_image only needs the sign of the pivots within the column range, - // which are still correct. The point is that the rows we added all have pivot columns - // in the first segment. - res_new_gens = matrix.inner.extend_image( - matrix.start[1], - matrix.end[1], - &self.get_kernel(b - Bidegree::s_t(1, 0)), + // Construct the next stage (block 4): new generators surjecting onto C_{s,t} and hitting the + // old kernel, their differential, and the quasi-inverses of `f` and `d`. + let complex_differential_target_dim = complex_cur_differential.target().dimension(b.t()); + let next = if b.s() > 0 { + let prev_chain_map = self.chain_map(b.s() - 1); + let previous_kernel = self.get_kernel(b - Bidegree::s_t(1, 0)); + field.construct_next_stage( + matrix, + NextStageInput { + previous_chain_map_quasi_inverse: prev_chain_map.quasi_inverse(b.t()), + previous_kernel: Some(&previous_kernel), + complex_differential_target_dim, + }, + |column, dfx| { + complex_cur_differential.apply_to_basis_element(dfx, ring.one(), b.t(), column); + }, MAX_NEW_GENS, - ); - } - let num_new_gens = cc_new_gens.len() + res_new_gens.len(); - self.add_generators(b, num_new_gens); - - let new_rows = source_dimension + num_new_gens; - - current_chain_map.add_generators_from_matrix_rows( - b.t(), - matrix.segment(0, 0).row_slice(source_dimension, new_rows), - ); - current_differential.add_generators_from_matrix_rows( - b.t(), - matrix.segment(1, 1).row_slice(source_dimension, new_rows), - ); - - if num_new_gens > 0 { - // Fix up the augmentation - let columns = matrix.columns(); - matrix.extend_column_dimension(columns + num_new_gens); - - for i in source_dimension..new_rows { - matrix.inner.row_mut(i).set_entry(matrix.start[2] + i, 1); - } - - // We are now supposed to row reduce the matrix. However, running the full row - // reduction algorithm is wasteful, since we have only added a few rows and the rest is - // intact. - // - // The new resolution rows are all zero in the existing pivot columns. Indeed, - // the resolution generators are mapped to generators of the kernel, which are zero in - // pivot columns of the kernel matrix. But the old image is a subspace of the kernel, - // so its pivot columns are a subset of the pivot columns of the kernel matrix. - // - // So we clear the new cc rows using the old rows. - for k in source_dimension..source_dimension + cc_new_gens.len() { - for column in matrix.start[1]..matrix.end[1] { - let row = matrix.pivots()[column]; - if row < 0 { - continue; - } - let row = row as usize; - unsafe { - matrix.row_op(k, row, column, p); - } - } - } - - // Now use the new resolution rows to reduce the old rows and the cc rows. - let first_res_row = source_dimension + cc_new_gens.len(); - for (source_row, &pivot_col) in res_new_gens.iter().enumerate() { - for target_row in 0..first_res_row { - unsafe { - matrix.row_op(target_row, source_row + first_res_row, pivot_col, p); - } - } - } + ) + } else { + field.construct_next_stage( + matrix, + NextStageInput { + previous_chain_map_quasi_inverse: None, + previous_kernel: None, + complex_differential_target_dim, + }, + |_, _| {}, + MAX_NEW_GENS, + ) + }; - // We are now almost in RREF, except we need to permute the rows. - let mut new_gens = cc_new_gens.into_iter().chain(res_new_gens).enumerate(); - let (mut next_new_row, mut next_new_col) = new_gens.next().unwrap(); - let mut next_old_row = 0; - - for old_col in 0..matrix.columns() { - if old_col == next_new_col { - matrix.rotate_down(next_old_row..source_dimension + next_new_row + 1, 1); - matrix.pivots_mut()[old_col] = next_old_row as isize; - match new_gens.next() { - Some((x, y)) => { - next_new_row = x; - next_new_col = y; - } - None => { - for entry in &mut matrix.pivots_mut()[old_col + 1..] { - if *entry >= 0 { - *entry += next_new_row as isize + 1; - } - } - break; - } - } - next_old_row += 1; - } else if matrix.pivots()[old_col] >= 0 { - matrix.pivots_mut()[old_col] += next_new_row as isize; - next_old_row += 1; - } - } - } - let (cm_qi, res_qi) = matrix.compute_quasi_inverses(); + let num_new_gens = next.num_new_gens; + self.add_generators(b, num_new_gens); + current_chain_map.add_generators_from_rows(b.t(), next.chain_map_rows); + current_differential.add_generators_from_rows(b.t(), next.differential_rows); + let cm_qi = next.chain_map_quasi_inverse; + let res_qi = next.differential_quasi_inverse; tracing::Span::current().record("num_new_gens", num_new_gens); tracing::Span::current().record( diff --git a/ext/src/resolution_homomorphism.rs b/ext/src/resolution_homomorphism.rs index 271c7318d3..bffa564d99 100644 --- a/ext/src/resolution_homomorphism.rs +++ b/ext/src/resolution_homomorphism.rs @@ -3,7 +3,7 @@ use std::{ops::Range, sync::Arc}; use algebra::{ - MuAlgebra, + MuAlgebra, Ring, module::{ Module, homomorphism::{ModuleHomomorphism, MuFreeModuleHomomorphism}, @@ -220,6 +220,7 @@ where } let p = self.source.prime(); + let ring = self.source.base_ring(); let num_gens = f_cur.source().number_of_gens_in_degree(input.t()); let fx_dimension = f_cur.target().dimension(output.t()); @@ -295,7 +296,7 @@ where let mut fdx_vector = FpVector::new(p, fdx_dimension); f_prev.apply( fdx_vector.as_slice_mut(), - 1, + ring.one(), input.t(), dx_vector.as_slice(), ); @@ -501,6 +502,7 @@ pub(crate) mod secondary { use std::sync::Arc; use algebra::{ + Algebra, Ring, module::{Module, homomorphism::ModuleHomomorphism}, pair_algebra::PairAlgebra, }; @@ -625,6 +627,7 @@ pub(crate) mod secondary { fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { let p = self.prime(); + let ring = self.algebra().base_ring(); let neg_1 = p - 1; let shifted_b = g.degree() - self.shift(); let target = self.target().module(shifted_b.s() - 1); @@ -651,7 +654,7 @@ pub(crate) mod secondary { ); self.underlying.get_map(g.s() - 2).apply( result.as_slice_mut(), - 1, + ring.one(), g.t() - 1, self.source .homotopy(g.s()) diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index e04b852a36..55bd255cb8 100644 --- a/ext/src/secondary.rs +++ b/ext/src/secondary.rs @@ -1,7 +1,7 @@ use std::{io, sync::Arc}; use algebra::{ - Algebra, + Algebra, BaseRingOf, Field, GradedDvr, Ring, module::{ FreeModule, Module, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, @@ -54,7 +54,7 @@ pub struct SecondaryComposite { composite: BiVec>, } -impl SecondaryComposite { +impl> SecondaryComposite { pub fn algebra(&self) -> Arc { self.target.algebra() } @@ -201,7 +201,10 @@ impl SecondaryComposite { } } -pub struct SecondaryHomotopy { +pub struct SecondaryHomotopy +where + BaseRingOf: GradedDvr, +{ pub source: Arc>, pub target: Arc>, /// output_t = input_t - shift_t @@ -216,7 +219,7 @@ pub struct SecondaryHomotopy { hit_generator: bool, } -impl SecondaryHomotopy { +impl> SecondaryHomotopy { pub fn new( source: Arc>, target: Arc>, @@ -327,7 +330,12 @@ impl SecondaryHomotopy { } if full { - self.homotopies.apply(result, coeff, elt_degree, elt); + self.homotopies.apply( + result, + self.homotopies.base_ring().embed_field(coeff), + elt_degree, + elt, + ); } } @@ -359,7 +367,7 @@ impl SecondaryHomotopy { /// The λ part of $hd + \mathrm{stuff}$ is known as the intermediate data, and is what /// [`SecondaryLift::compute_intermediate`] returns. pub trait SecondaryLift: Sync + Sized { - type Algebra: PairAlgebra; + type Algebra: PairAlgebra + Algebra; type Source: FreeChainComplex; type Target: FreeChainComplex; type Underlying; @@ -545,6 +553,7 @@ pub trait SecondaryLift: Sync + Sized { let target_b = b - shift - Bidegree::s_t(0, 1); let d = self.source().differential(b.s()); + let ring = d.base_ring(); let source = self.source().module(b.s()); let target = self.target(); let num_gens = source.number_of_gens_in_degree(b.t()); @@ -578,7 +587,7 @@ pub trait SecondaryLift: Sync + Sized { if g.s() > shift.s() + 1 { self.homotopies()[g.s() - 1].homotopies.apply( v.as_slice_mut(), - 1, + ring.one(), g.t(), d.output(g.t(), g.idx()).as_slice(), ); @@ -602,7 +611,12 @@ pub trait SecondaryLift: Sync + Sized { // Check that we indeed had a lift let d = target.differential(target_b.s()); for (src, tgt) in std::iter::zip(&results, &mut intermediates) { - d.apply(tgt.as_slice_mut(), p - 1, target_b.t(), src.as_slice()); + d.apply( + tgt.as_slice_mut(), + ring.embed_field(p - 1), + target_b.t(), + src.as_slice(), + ); anyhow::ensure!( tgt.is_zero(), "secondary: Failed to lift at {b}. This likely indicates an invalid input." diff --git a/ext/src/yoneda.rs b/ext/src/yoneda.rs index 2fffe713c9..2c70ff2990 100644 --- a/ext/src/yoneda.rs +++ b/ext/src/yoneda.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use algebra::{ - AdemAlgebra, Algebra, GeneratedAlgebra, MilnorAlgebra, SteenrodAlgebra, + AdemAlgebra, Algebra, Field, GeneratedAlgebra, MilnorAlgebra, Ring, SteenrodAlgebra, module::{ FDModule, FreeModule, Module, QuotientModule as QM, homomorphism::{ @@ -241,6 +241,7 @@ where let p = cc.prime(); let target_cc = cc.target(); let algebra = cc.algebra(); + let ring = algebra.base_ring(); let t_shift: i32 = map.chain_maps[0].degree_shift(); let s_shift: i32 = map.s_shift; @@ -304,7 +305,7 @@ where for (i, mut row) in differentials.iter_mut().enumerate() { let j = prev.basis_list[t][i]; - d.apply_to_basis_element(row.copy(), 1, t, j); + d.apply_to_basis_element(row.copy(), ring.one(), t, j); curr.reduce(t, row); } @@ -440,7 +441,7 @@ where let mut indices = start..end; target.quotient_vectors(t, |row| { - d.apply_to_basis_element(row, 1, t, indices.next()?); + d.apply_to_basis_element(row, ring.one(), t, indices.next()?); Some(()) }); check!(t); @@ -525,7 +526,7 @@ where }); target.quotient_vectors(t, |row| { - d.apply(row, 1, t, source_iter2.next()?); + d.apply(row, ring.one(), t, source_iter2.next()?); Some(()) }); @@ -576,10 +577,13 @@ fn compute_kernel_image t: i32, ) -> (Matrix, Matrix) where - M::Algebra: GeneratedAlgebra, + M::Algebra: GeneratedAlgebra + Algebra, + ::Algebra: Algebra, + ::Algebra: Algebra, { let algebra = source.algebra(); let p = algebra.prime(); + let ring = algebra.base_ring(); let mut generators: Vec<(i32, usize)> = Vec::new(); let mut target_dims = Vec::new(); @@ -613,6 +617,9 @@ where ], ); + let aug_ring = augmentation_map.map(|m| m.target().base_ring()); + let preserve_ring = preserve_map.map(|m| m.target().base_ring()); + for (row_idx, &i) in source.basis_list[t].iter().enumerate() { let mut offset = 0; let mut row = matrix.row_segment_mut(row_idx, 0, 0); @@ -622,7 +629,7 @@ where let len = cols.next().unwrap(); source.act_on_original_basis( row.slice_mut(offset, offset + len), - 1, + ring.one(), *op_deg, *op_idx, t, @@ -633,13 +640,23 @@ where if let Some(m) = &augmentation_map { let len = cols.next().unwrap(); - m.apply_to_basis_element(row.slice_mut(offset, offset + len), 1, t, i); + m.apply_to_basis_element( + row.slice_mut(offset, offset + len), + aug_ring.unwrap().one(), + t, + i, + ); offset += len; } if let Some(m) = &preserve_map { let len = cols.next().unwrap(); - m.apply_to_basis_element(row.slice_mut(offset, offset + len), 1, t, i); + m.apply_to_basis_element( + row.slice_mut(offset, offset + len), + preserve_ring.unwrap().one(), + t, + i, + ); offset += len; } From 0edc46c36aac34dd1bd15a69d6d69d5e5d69df24 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 00:50:34 +0000 Subject: [PATCH 3/7] Make Ring a sub-trait of Algebra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A coefficient ring is itself an algebra over itself, so fold Ring into the Algebra hierarchy: Ring: Algebra. This makes Algebra::BaseRing: Algebra hold transitively (since BaseRing: Ring), so each graded piece of an algebra can be viewed as a module over its base ring — the basis for exposing motivic weight as the internal grading of the coefficient module. The coefficient capacity (scalar, vector, arithmetic) stays confined to the Ring sub-trait, keeping Algebra lean for the Steenrod, Milnor, Adem, and module algebras that are never coefficient rings. Pure supertrait addition: Field already implements Algebra, so this is a bound, not new boilerplate; all call sites and the monomorphized Field path are unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNkiiZghHggyMwDWfDn1y5 --- .../algebra/src/algebra/algebra_trait.rs | 4 +- ext/crates/algebra/src/algebra/base_ring.rs | 39 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/ext/crates/algebra/src/algebra/algebra_trait.rs b/ext/crates/algebra/src/algebra/algebra_trait.rs index fe1e60a255..90e4292c08 100644 --- a/ext/crates/algebra/src/algebra/algebra_trait.rs +++ b/ext/crates/algebra/src/algebra/algebra_trait.rs @@ -43,7 +43,9 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// The coefficient ring this algebra (and its modules) are linear over. /// /// This is bounded only by [`Ring`] — enough to represent and multiply elements, and to act on - /// modules. Resolving *over* the algebra additionally needs the base ring to be a + /// modules. Since [`Ring`] is a sub-trait of [`Algebra`], the base ring is itself an [`Algebra`] + /// (over itself), so each graded piece of this algebra may be viewed as a module over it. + /// Resolving *over* the algebra additionally needs the base ring to be a /// [`GradedDvr`](crate::linear_algebra::GradedDvr) (for kernels and quasi-inverses); that /// stronger bound is imposed where resolution happens (e.g. [`ModuleHomomorphism`] and the chain /// complex), not here, so that an algebra can be defined over a ring whose solving linear algebra diff --git a/ext/crates/algebra/src/algebra/base_ring.rs b/ext/crates/algebra/src/algebra/base_ring.rs index f97b404483..4122094fc4 100644 --- a/ext/crates/algebra/src/algebra/base_ring.rs +++ b/ext/crates/algebra/src/algebra/base_ring.rs @@ -2,11 +2,16 @@ //! //! An [`Algebra`] declares its coefficient ring as `type BaseRing: Ring`. [`Ring`] is that ring: the //! scalar type, its arithmetic, and the representation of finite free modules over it (vectors and -//! their slices) — everything needed to define an algebra and act on its modules. The *harder* -//! linear algebra that a free resolution needs — kernels and quasi-inverses, which require the ring -//! to be a graded DVR — is the [`GradedDvr`](crate::linear_algebra::GradedDvr) subtrait, in -//! [`linear_algebra`](crate::linear_algebra). Keeping them separate lets an algebra be defined over a -//! ring whose solving linear algebra is not yet implemented. +//! their slices) — everything needed to define an algebra and act on its modules. A coefficient ring +//! is itself an [`Algebra`] (over itself), so [`Ring`] is a *sub-trait* of [`Algebra`]: `Field` is +//! the field $\mathbb{F}_p$ as a one-dimensional algebra over itself, and $\mathbb{F}_2[\tau]$ is the +//! polynomial algebra on $\tau$ over itself. This fold is what makes `Algebra::BaseRing: Algebra` +//! hold, so that each graded piece of an algebra can be viewed as a module over the base ring. +//! +//! The *harder* linear algebra that a free resolution needs — kernels and quasi-inverses, which +//! require the ring to be a graded DVR — is the [`GradedDvr`](crate::linear_algebra::GradedDvr) +//! subtrait, in [`linear_algebra`](crate::linear_algebra). Keeping that separate from [`Ring`] lets +//! an algebra be defined over a ring whose solving linear algebra is not yet implemented. //! //! Every classical algebra has `BaseRing = Field` (the field $\mathbb{F}_p$); the C-motivic Steenrod //! algebra will use $\mathbb{F}_2[\tau]$. @@ -38,18 +43,24 @@ pub type Scalar = as Ring>::Element; /// A graded coefficient ring over which algebras and their modules are linear — concretely /// $\mathbb{F}_p$ (a field, via [`Field`]) or $\mathbb{F}_2[\tau]$. /// -/// This trait is the coefficient *ring*: the scalar type and its arithmetic, together with a -/// representation of finite free modules over the ring (vectors and their slices). This is enough to -/// *define* an algebra and its modules — to multiply basis elements and act on modules, accumulating -/// ring-coefficient results into a vector. It is deliberately *not* enough to resolve: the linear -/// algebra of solving (kernels, quasi-inverses, minimal generators) requires the ring to be a graded -/// DVR, and lives in the [`GradedDvr`](crate::linear_algebra::GradedDvr) subtrait. Splitting them -/// this way lets us define algebras over rings whose solving linear algebra we have not yet -/// implemented (e.g. $\mathbb{R}$-motivic). +/// A coefficient ring is itself an [`Algebra`] (over itself, i.e. `BaseRing = Self`), so this trait +/// is a *sub-trait* of [`Algebra`]. On top of the algebra structure it adds what is needed to serve +/// as coefficients: the scalar type and its arithmetic, together with a representation of finite free +/// modules over the ring (vectors and their slices). This is enough to *define* an algebra and its +/// modules — to multiply basis elements and act on modules, accumulating ring-coefficient results +/// into a vector. It is deliberately *not* enough to resolve: the linear algebra of solving (kernels, +/// quasi-inverses, minimal generators) requires the ring to be a graded DVR, and lives in the +/// [`GradedDvr`](crate::linear_algebra::GradedDvr) subtrait. Splitting them this way lets us define +/// algebras over rings whose solving linear algebra we have not yet implemented (e.g. +/// $\mathbb{R}$-motivic). +/// +/// Confining the coefficient capacity (scalar, vector, arithmetic) to this sub-trait — rather than +/// putting it on [`Algebra`] itself — keeps [`Algebra`] lean: the Steenrod, Milnor, Adem, and +/// module-algebras, which are never coefficient rings, carry no dead scalar/vector members. /// /// The ring is a `Copy` handle (like `fp`'s `Field`): it may carry a small amount of runtime data /// (e.g. the prime of $\mathbb{F}_p$), so its operations take `self`. -pub trait Ring: Copy + Send + Sync + 'static { +pub trait Ring: Algebra + Copy + Send + Sync + 'static { /// A scalar of the ring. type Element: Copy + PartialEq + Send + Sync; From fd80ef350929b8c62c945470d46bc3ed1c704eaa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 01:24:13 +0000 Subject: [PATCH 4/7] Rename the solving trait GradedDvr to Solvable The trait bounding a ring on which a free resolution can solve (compute images, kernels, quasi-inverses) is renamed GradedDvr -> Solvable, moving the emphasis from the sufficient condition (being a graded DVR) to the capability it grants. Pure identifier rename across the algebra and ext crates; no behavior change, all tests pass. The doc prose still explains that solvability requires a graded DVR. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNkiiZghHggyMwDWfDn1y5 --- ext/crates/algebra/src/algebra/algebra_trait.rs | 4 ++-- ext/crates/algebra/src/algebra/base_ring.rs | 4 ++-- ext/crates/algebra/src/lib.rs | 2 +- ext/crates/algebra/src/linear_algebra/mod.rs | 16 ++++++++-------- .../src/module/finitely_presented_module.rs | 10 +++++----- .../homomorphism/free_module_homomorphism.rs | 10 +++++----- .../homomorphism/generic_zero_homomorphism.rs | 6 +++--- .../src/module/homomorphism/hom_pullback.rs | 6 +++--- .../algebra/src/module/homomorphism/mod.rs | 8 ++++---- ext/src/resolution.rs | 2 +- ext/src/secondary.rs | 4 ++-- 11 files changed, 36 insertions(+), 36 deletions(-) diff --git a/ext/crates/algebra/src/algebra/algebra_trait.rs b/ext/crates/algebra/src/algebra/algebra_trait.rs index 90e4292c08..b8f2caf800 100644 --- a/ext/crates/algebra/src/algebra/algebra_trait.rs +++ b/ext/crates/algebra/src/algebra/algebra_trait.rs @@ -45,8 +45,8 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// This is bounded only by [`Ring`] — enough to represent and multiply elements, and to act on /// modules. Since [`Ring`] is a sub-trait of [`Algebra`], the base ring is itself an [`Algebra`] /// (over itself), so each graded piece of this algebra may be viewed as a module over it. - /// Resolving *over* the algebra additionally needs the base ring to be a - /// [`GradedDvr`](crate::linear_algebra::GradedDvr) (for kernels and quasi-inverses); that + /// Resolving *over* the algebra additionally needs the base ring to be + /// [`Solvable`](crate::linear_algebra::Solvable) (for kernels and quasi-inverses); that /// stronger bound is imposed where resolution happens (e.g. [`ModuleHomomorphism`] and the chain /// complex), not here, so that an algebra can be defined over a ring whose solving linear algebra /// is not yet implemented. diff --git a/ext/crates/algebra/src/algebra/base_ring.rs b/ext/crates/algebra/src/algebra/base_ring.rs index 4122094fc4..37b4bcee19 100644 --- a/ext/crates/algebra/src/algebra/base_ring.rs +++ b/ext/crates/algebra/src/algebra/base_ring.rs @@ -9,7 +9,7 @@ //! hold, so that each graded piece of an algebra can be viewed as a module over the base ring. //! //! The *harder* linear algebra that a free resolution needs — kernels and quasi-inverses, which -//! require the ring to be a graded DVR — is the [`GradedDvr`](crate::linear_algebra::GradedDvr) +//! require the ring to be a graded DVR — is the [`Solvable`](crate::linear_algebra::Solvable) //! subtrait, in [`linear_algebra`](crate::linear_algebra). Keeping that separate from [`Ring`] lets //! an algebra be defined over a ring whose solving linear algebra is not yet implemented. //! @@ -50,7 +50,7 @@ pub type Scalar = as Ring>::Element; /// modules — to multiply basis elements and act on modules, accumulating ring-coefficient results /// into a vector. It is deliberately *not* enough to resolve: the linear algebra of solving (kernels, /// quasi-inverses, minimal generators) requires the ring to be a graded DVR, and lives in the -/// [`GradedDvr`](crate::linear_algebra::GradedDvr) subtrait. Splitting them this way lets us define +/// [`Solvable`](crate::linear_algebra::Solvable) subtrait. Splitting them this way lets us define /// algebras over rings whose solving linear algebra we have not yet implemented (e.g. /// $\mathbb{R}$-motivic). /// diff --git a/ext/crates/algebra/src/lib.rs b/ext/crates/algebra/src/lib.rs index b78dfab2ec..47a54b9d00 100644 --- a/ext/crates/algebra/src/lib.rs +++ b/ext/crates/algebra/src/lib.rs @@ -15,7 +15,7 @@ mod algebra; pub use crate::{ algebra::*, linear_algebra::{ - BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, GradedDvr, QuasiInverseOf, + BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, Solvable, QuasiInverseOf, SubmoduleOf, VectorOf, }, }; diff --git a/ext/crates/algebra/src/linear_algebra/mod.rs b/ext/crates/algebra/src/linear_algebra/mod.rs index 88ddcb740c..929a7dec25 100644 --- a/ext/crates/algebra/src/linear_algebra/mod.rs +++ b/ext/crates/algebra/src/linear_algebra/mod.rs @@ -7,7 +7,7 @@ //! images, kernels, and quasi-inverses, which requires the ring to be a graded DVR (so that graded //! Nakayama holds and minimal generators can be read off mod the maximal ideal). //! -//! The central trait is [`GradedDvr`]: a [`Ring`](crate::algebra::Ring) over which one can solve +//! The central trait is [`Solvable`]: a [`Ring`](crate::algebra::Ring) over which one can solve //! linear systems. For [`Field`] the vector types are exactly `fp`'s `FpVector`/`FpSlice`/`FpSliceMut` //! and every operation forwards to `fp`, so the classical path is unchanged and bit-identical. A //! future $\mathbb{F}_2[\tau]$ impl provides the graded-local solving (per-weight `fp` blocks plus a @@ -35,18 +35,18 @@ pub type BaseSliceMutOf<'a, A> = as Ring>::SliceMut<'a>; /// A submodule (image or kernel) over the base ring of the algebra `A`. For every classical algebra /// this is `fp`'s `Subspace`. -pub type SubmoduleOf = as GradedDvr>::Submodule; +pub type SubmoduleOf = as Solvable>::Submodule; /// A quasi-inverse of a map over the base ring of the algebra `A`. For every classical algebra this /// is `fp`'s `QuasiInverse`. -pub type QuasiInverseOf = as GradedDvr>::QuasiInverse; +pub type QuasiInverseOf = as Solvable>::QuasiInverse; /// The data — beyond the step matrix itself — needed to construct the next stage of a resolution: /// how to make the chain map a chain map, and which cycles the new generators must hit. /// /// At homological degree 0 there is no previous stage, so both submodule fields are `None` and the /// target dimension is 0. -pub struct NextStageInput<'a, R: GradedDvr> { +pub struct NextStageInput<'a, R: Solvable> { /// The previous chain map's quasi-inverse, used to set `dX(x) = f^{-1}(dC(f(x)))`. `None` at /// homological degree 0, or when no new augmentation generators are added. pub previous_chain_map_quasi_inverse: Option<&'a R::QuasiInverse>, @@ -59,8 +59,8 @@ pub struct NextStageInput<'a, R: GradedDvr> { /// The generators produced by one resolution step: the new differential and chain-map rows to store, /// plus the quasi-inverses of the two maps. Returned by -/// [`GradedDvr::construct_next_stage`]. -pub struct NextStage { +/// [`Solvable::construct_next_stage`]. +pub struct NextStage { /// The number of new generators added (surjecting onto the cokernel plus hitting the old kernel). pub num_new_gens: usize, /// The chain map on each new generator (its image in `C_{s,t}`). @@ -133,7 +133,7 @@ pub trait BaseSliceMut<'a, R: Ring> { /// representation — with the operations a free resolution needs: computing the image, kernel, and /// quasi-inverse of an `R`-linear map. These are meaningful precisely because `R` is a graded DVR /// (graded-local, so graded Nakayama holds). For [`Field`] everything forwards to `fp`. -pub trait GradedDvr: Ring { +pub trait Solvable: Ring { /// A submodule of a finite free module over the ring (the image or kernel of a map). For /// [`Field`] this is `fp`'s [`Subspace`]. type Submodule: Send + Sync; @@ -278,7 +278,7 @@ impl<'a> BaseSliceMut<'a, Field> for FpSliceMut<'a> { } } -impl GradedDvr for Field { +impl Solvable for Field { type Matrix = AugmentedMatrix<3>; type QuasiInverse = QuasiInverse; type Submodule = Subspace; diff --git a/ext/crates/algebra/src/module/finitely_presented_module.rs b/ext/crates/algebra/src/module/finitely_presented_module.rs index ea9c6c3d5f..1d49b3b465 100644 --- a/ext/crates/algebra/src/module/finitely_presented_module.rs +++ b/ext/crates/algebra/src/module/finitely_presented_module.rs @@ -7,7 +7,7 @@ use serde_json::Value; use crate::{ algebra::{Algebra, BaseRingOf, Field, Scalar}, - linear_algebra::GradedDvr, + linear_algebra::Solvable, module::{ FreeModule, Module, ZeroModule, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, @@ -21,7 +21,7 @@ struct FPMIndexTable { pub struct FinitelyPresentedModule where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { name: String, min_degree: i32, @@ -33,7 +33,7 @@ where impl std::fmt::Display for FinitelyPresentedModule where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.name) @@ -42,14 +42,14 @@ where impl PartialEq for FinitelyPresentedModule where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { fn eq(&self, _other: &Self) -> bool { todo!() } } -impl Eq for FinitelyPresentedModule where BaseRingOf: GradedDvr {} +impl Eq for FinitelyPresentedModule where BaseRingOf: Solvable {} impl> ZeroModule for FinitelyPresentedModule { fn zero_module(algebra: Arc, min_degree: i32) -> Self { 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 c94bac4b45..57ea7ae9b2 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::{Algebra, BaseRingOf, Field, MuAlgebra, Ring, Scalar}, linear_algebra::{ - BaseSlice, BaseSliceMut, BaseSliceMutOf, GradedDvr, QuasiInverseOf, SubmoduleOf, VectorOf, + BaseSlice, BaseSliceMut, BaseSliceMutOf, Solvable, QuasiInverseOf, SubmoduleOf, VectorOf, }, module::{ Module, MuFreeModule, @@ -24,7 +24,7 @@ pub type UnstableFreeModuleHomomorphism = MuFreeModuleHomomorphism; pub struct MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { source: Arc>, target: Arc, @@ -40,7 +40,7 @@ where impl ModuleHomomorphism for MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { type Source = MuFreeModule; type Target = M; @@ -117,7 +117,7 @@ where impl MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { pub fn new( source: Arc>, @@ -283,7 +283,7 @@ impl ZeroHomomorphism, M> for MuFreeModuleHomomorphism where M::Algebra: MuAlgebra, - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { fn zero_homomorphism( source: Arc>, diff --git a/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs b/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs index 9c1ab3ee95..1258ca4983 100644 --- a/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs +++ b/ext/crates/algebra/src/module/homomorphism/generic_zero_homomorphism.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::{ algebra::{BaseRingOf, Scalar}, - linear_algebra::{BaseSliceMutOf, GradedDvr}, + linear_algebra::{BaseSliceMutOf, Solvable}, module::{ Module, homomorphism::{ModuleHomomorphism, ZeroHomomorphism}, @@ -28,7 +28,7 @@ impl> GenericZeroHomomorphism { impl> ModuleHomomorphism for GenericZeroHomomorphism where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { type Source = S; type Target = T; @@ -58,7 +58,7 @@ where impl> ZeroHomomorphism for GenericZeroHomomorphism where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { fn zero_homomorphism(source: Arc, target: Arc, degree_shift: i32) -> Self { Self::new(source, target, degree_shift) diff --git a/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs b/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs index c86ed8ee9e..b7f5fc8241 100644 --- a/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs +++ b/ext/crates/algebra/src/module/homomorphism/hom_pullback.rs @@ -8,7 +8,7 @@ use once::OnceBiVec; use crate::{ algebra::{Algebra, BaseRingOf, Field, Ring, Scalar}, - linear_algebra::GradedDvr, + linear_algebra::Solvable, module::{ FreeModule, HomModule, Module, block_structure::GeneratorBasisEltPair, @@ -19,7 +19,7 @@ use crate::{ /// Given a map $\mathtt{map}: A \to B$ and hom modules $\mathtt{source} = \Hom(B, X)$, $\mathtt{target} = \Hom(A, X)$, produce the induced pullback map $\Hom(B, X) \to \Hom(A, X)$. pub struct HomPullback where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { source: Arc>, target: Arc>, @@ -31,7 +31,7 @@ where impl HomPullback where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { /// Fallible version of [`new`](Self::new). /// diff --git a/ext/crates/algebra/src/module/homomorphism/mod.rs b/ext/crates/algebra/src/module/homomorphism/mod.rs index 185b84706b..c9aeb1c52d 100644 --- a/ext/crates/algebra/src/module/homomorphism/mod.rs +++ b/ext/crates/algebra/src/module/homomorphism/mod.rs @@ -8,7 +8,7 @@ use fp::{ use crate::{ algebra::{Algebra, BaseRingOf, Field, Ring, Scalar}, linear_algebra::{ - BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, GradedDvr, QuasiInverseOf, + BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, Solvable, QuasiInverseOf, SubmoduleOf, }, module::Module, @@ -47,7 +47,7 @@ pub use quotient_homomorphism::{QuotientHomomorphism, QuotientHomomorphismSource /// `compute_auxiliary_data_through_degree` is invoked. pub trait ModuleHomomorphism: Send + Sync where - BaseRingOf<::Algebra>: GradedDvr, + BaseRingOf<::Algebra>: Solvable, { type Source: Module; type Target: Module::Algebra>; @@ -209,14 +209,14 @@ where pub trait ZeroHomomorphism>: ModuleHomomorphism where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { fn zero_homomorphism(s: Arc, t: Arc, degree_shift: i32) -> Self; } pub trait IdentityHomomorphism: ModuleHomomorphism where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { fn identity_homomorphism(s: Arc) -> Self; } diff --git a/ext/src/resolution.rs b/ext/src/resolution.rs index 0893e9c46f..fe2cb4bfbd 100644 --- a/ext/src/resolution.rs +++ b/ext/src/resolution.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex, mpsc}; use algebra::{ - Algebra, Field, GradedDvr, MuAlgebra, Ring, + Algebra, Field, Solvable, MuAlgebra, Ring, linear_algebra::NextStageInput, module::{ Module, MuFreeModule, diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index 55bd255cb8..eec0c148c1 100644 --- a/ext/src/secondary.rs +++ b/ext/src/secondary.rs @@ -1,7 +1,7 @@ use std::{io, sync::Arc}; use algebra::{ - Algebra, BaseRingOf, Field, GradedDvr, Ring, + Algebra, BaseRingOf, Field, Solvable, Ring, module::{ FreeModule, Module, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, @@ -203,7 +203,7 @@ impl> SecondaryComposite { pub struct SecondaryHomotopy where - BaseRingOf: GradedDvr, + BaseRingOf: Solvable, { pub source: Arc>, pub target: Arc>, From 4bdd9eaca22e1ee445907dd3291367603d386b59 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 01:32:21 +0000 Subject: [PATCH 5/7] Add Algebra::module_at accessor exposing graded pieces as R-modules Give Algebra a concrete associated type GradedPiece: Module and an accessor fn module_at(&self, t) -> Self::GradedPiece, so each graded piece of an algebra can be viewed as a module over its coefficient ring. Since BaseRing is itself an Algebra (the Ring: Algebra fold), "an R-module" is just Module, reusing the Module trait. The type is concrete (not -> impl Module) so the hand-rolled SteenrodAlgebra dispatch forwards it. For every classical algebra BaseRing = Field, and since every module over a field is free, the piece is simply a FreeModule with dimension(t) generators in degree 0 (weight-0 internal grading) -- no bespoke type needed, and it matches the plan's framing that free grades are free modules and non-free grades (motivic) are presentations. This is additive, engine-unused structural access; dimension(t) stays load-bearing. Behavior-preserving: all 69 resolution benchmarks are bit-identical, all algebra tests pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNkiiZghHggyMwDWfDn1y5 --- .../algebra/src/algebra/adem_algebra.rs | 18 ++++++++++--- .../algebra/src/algebra/algebra_trait.rs | 26 +++++++++++++++++++ ext/crates/algebra/src/algebra/field.rs | 16 +++++++++++- .../algebra/src/algebra/milnor_algebra.rs | 26 ++++++++++++++++++- .../algebra/src/algebra/steenrod_algebra.rs | 2 ++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/ext/crates/algebra/src/algebra/adem_algebra.rs b/ext/crates/algebra/src/algebra/adem_algebra.rs index 882a5b29f3..d290443ffe 100644 --- a/ext/crates/algebra/src/algebra/adem_algebra.rs +++ b/ext/crates/algebra/src/algebra/adem_algebra.rs @@ -15,9 +15,12 @@ use rustc_hash::FxHashMap as HashMap; #[cfg(doc)] use crate::algebra::SteenrodAlgebra; -use crate::algebra::{ - Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, - combinatorics::{self, MAX_XI_TAU}, +use crate::{ + algebra::{ + Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, + combinatorics::{self, MAX_XI_TAU}, + }, + module::{FreeModule, Module as _}, }; /// An Adem basis element for the Steenrod algebra. @@ -166,6 +169,15 @@ impl Algebra for AdemAlgebra { Field::new(self.prime()) } + type GradedPiece = FreeModule; + + fn module_at(&self, t: i32) -> FreeModule { + let piece = FreeModule::new(std::sync::Arc::new(self.base_ring()), String::new(), 0); + piece.add_generators(0, self.dimension(t), None); + piece.compute_basis(0); + piece + } + fn prefix(&self) -> &str { "adem" } diff --git a/ext/crates/algebra/src/algebra/algebra_trait.rs b/ext/crates/algebra/src/algebra/algebra_trait.rs index b8f2caf800..efe2d05652 100644 --- a/ext/crates/algebra/src/algebra/algebra_trait.rs +++ b/ext/crates/algebra/src/algebra/algebra_trait.rs @@ -58,6 +58,32 @@ pub trait Algebra: std::fmt::Display + Send + Sync + 'static { /// arithmetic and the linear algebra of resolving over this algebra. fn base_ring(&self) -> Self::BaseRing; + /// The type of graded piece returned by [`module_at`](Algebra::module_at). + /// + /// This is a *concrete* associated type (rather than `-> impl Module`) so that the hand-rolled + /// [`SteenrodAlgebra`] dispatch can forward it. For every classical algebra `BaseRing = Field`, + /// and since every module over a field is free, this is simply a + /// [`FreeModule`](crate::module::FreeModule)``; the C-motivic algebra makes it a + /// weight-graded $\mathbb{F}_2[\tau]$-module (a presentation, since those pieces may carry + /// $\tau$-torsion). + /// + /// [`SteenrodAlgebra`]: crate::algebra::SteenrodAlgebra + type GradedPiece: crate::module::Module; + + /// The degree-`t` component of the algebra, viewed as a module over the base ring. + /// + /// Since [`BaseRing`](Algebra::BaseRing) is itself an [`Algebra`], "an R-module" is just + /// `Module`, and each graded piece of the algebra is such a module. Its own internal + /// grading is the *weight*: classically the base field sits in degree 0, so the piece is a + /// $\mathbb{F}_p$-vector space of dimension [`dimension(t)`](Algebra::dimension) concentrated in + /// degree 0 — represented as a free `Field`-module with that many generators in degree 0; the + /// C-motivic algebra's pieces are weight-graded. + /// + /// This is additive structural access — a view of the algebra as a module over its coefficients. + /// The load-bearing rank is still [`dimension`](Algebra::dimension); `module_at` is derived from + /// it classically. + fn module_at(&self, t: i32) -> Self::GradedPiece; + /// 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 { diff --git a/ext/crates/algebra/src/algebra/field.rs b/ext/crates/algebra/src/algebra/field.rs index 4147337476..cd715ad8a7 100644 --- a/ext/crates/algebra/src/algebra/field.rs +++ b/ext/crates/algebra/src/algebra/field.rs @@ -1,11 +1,16 @@ //! Finite fields over a prime. +use std::sync::Arc; + use fp::{ prime::ValidPrime, vector::{FpSlice, FpSliceMut}, }; -use crate::algebra::{Algebra, Bialgebra}; +use crate::{ + algebra::{Algebra, Bialgebra}, + module::{FreeModule, Module}, +}; /// $\mathbb{F}_p$, viewed as an [`Algebra`] over itself. /// @@ -38,6 +43,15 @@ impl Algebra for Field { *self } + type GradedPiece = FreeModule; + + fn module_at(&self, t: i32) -> FreeModule { + let piece = FreeModule::new(Arc::new(self.base_ring()), String::new(), 0); + piece.add_generators(0, self.dimension(t), None); + piece.compute_basis(0); + piece + } + fn prime(&self) -> ValidPrime { self.prime } diff --git a/ext/crates/algebra/src/algebra/milnor_algebra.rs b/ext/crates/algebra/src/algebra/milnor_algebra.rs index 86d7a052d4..1e0e830e41 100644 --- a/ext/crates/algebra/src/algebra/milnor_algebra.rs +++ b/ext/crates/algebra/src/algebra/milnor_algebra.rs @@ -9,7 +9,10 @@ use once::OnceVec; use rustc_hash::FxHashMap as HashMap; use serde::{Deserialize, Serialize}; -use crate::algebra::{Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, combinatorics}; +use crate::{ + algebra::{Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, combinatorics}, + module::{FreeModule, Module as _}, +}; fn q_part_default() -> u32 { !0 @@ -351,6 +354,15 @@ impl Algebra for MilnorAlgebra { Field::new(self.prime()) } + type GradedPiece = FreeModule; + + fn module_at(&self, t: i32) -> FreeModule { + let piece = FreeModule::new(std::sync::Arc::new(self.base_ring()), String::new(), 0); + piece.add_generators(0, self.dimension(t), None); + piece.compute_basis(0); + piece + } + fn prefix(&self) -> &str { "milnor" } @@ -1799,6 +1811,18 @@ mod tests { use super::*; + #[test] + fn module_at_is_the_graded_piece() { + let algebra = MilnorAlgebra::new(ValidPrime::new(2), false); + algebra.compute_basis(20); + for t in 0..=20 { + let piece = algebra.module_at(t); + // The graded piece is a free Field-module concentrated in degree 0, whose dimension + // there equals the algebra's dimension in degree t. + assert_eq!(piece.dimension(0), algebra.dimension(t), "t = {t}"); + } + } + #[rstest] #[trace] #[case(2, 32, None)] diff --git a/ext/crates/algebra/src/algebra/steenrod_algebra.rs b/ext/crates/algebra/src/algebra/steenrod_algebra.rs index 1b17c186fa..c9c5d6987a 100644 --- a/ext/crates/algebra/src/algebra/steenrod_algebra.rs +++ b/ext/crates/algebra/src/algebra/steenrod_algebra.rs @@ -155,9 +155,11 @@ macro_rules! dispatch_steenrod { // generated, keeping the classical behaviour bit-identical. impl Algebra for SteenrodAlgebra { type BaseRing = Field; + type GradedPiece = crate::module::FreeModule; dispatch_steenrod! { fn base_ring(&self) -> Field; + fn module_at(&self, t: i32) -> crate::module::FreeModule; fn prefix(&self) -> &str; fn magic(&self) -> u32; fn prime(&self) -> ValidPrime; From 2e53345accc1880dd116253b4df7d0fac1ac9308 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 02:05:58 +0000 Subject: [PATCH 6/7] Satisfy rustfmt and rustdoc lints - rustfmt (nightly): order associated type GradedPiece before methods in the Algebra impls, and restore alphabetical import ordering disturbed by the GradedDvr -> Solvable rename. - rustdoc (-D warnings): the linear_algebra module doc linked the private `algebra` module and used redundant explicit targets on [`Ring`] links; make the module reference plain and drop the redundant targets. just lint and just docs now pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNkiiZghHggyMwDWfDn1y5 --- ext/crates/algebra/src/algebra/adem_algebra.rs | 3 +-- ext/crates/algebra/src/algebra/field.rs | 3 +-- ext/crates/algebra/src/algebra/milnor_algebra.rs | 3 +-- ext/crates/algebra/src/lib.rs | 2 +- ext/crates/algebra/src/linear_algebra/mod.rs | 8 ++++---- .../src/module/homomorphism/free_module_homomorphism.rs | 2 +- ext/crates/algebra/src/module/homomorphism/mod.rs | 3 +-- ext/src/resolution.rs | 2 +- ext/src/secondary.rs | 2 +- 9 files changed, 12 insertions(+), 16 deletions(-) diff --git a/ext/crates/algebra/src/algebra/adem_algebra.rs b/ext/crates/algebra/src/algebra/adem_algebra.rs index d290443ffe..e2c99d0a4c 100644 --- a/ext/crates/algebra/src/algebra/adem_algebra.rs +++ b/ext/crates/algebra/src/algebra/adem_algebra.rs @@ -164,13 +164,12 @@ impl fmt::Display for AdemAlgebra { impl Algebra for AdemAlgebra { type BaseRing = Field; + type GradedPiece = FreeModule; fn base_ring(&self) -> Field { Field::new(self.prime()) } - type GradedPiece = FreeModule; - fn module_at(&self, t: i32) -> FreeModule { let piece = FreeModule::new(std::sync::Arc::new(self.base_ring()), String::new(), 0); piece.add_generators(0, self.dimension(t), None); diff --git a/ext/crates/algebra/src/algebra/field.rs b/ext/crates/algebra/src/algebra/field.rs index cd715ad8a7..9f4d7a7697 100644 --- a/ext/crates/algebra/src/algebra/field.rs +++ b/ext/crates/algebra/src/algebra/field.rs @@ -38,13 +38,12 @@ impl std::fmt::Display for Field { impl Algebra for Field { type BaseRing = Self; + type GradedPiece = FreeModule; fn base_ring(&self) -> Self { *self } - type GradedPiece = FreeModule; - fn module_at(&self, t: i32) -> FreeModule { let piece = FreeModule::new(Arc::new(self.base_ring()), String::new(), 0); piece.add_generators(0, self.dimension(t), None); diff --git a/ext/crates/algebra/src/algebra/milnor_algebra.rs b/ext/crates/algebra/src/algebra/milnor_algebra.rs index 1e0e830e41..fd28f00e5c 100644 --- a/ext/crates/algebra/src/algebra/milnor_algebra.rs +++ b/ext/crates/algebra/src/algebra/milnor_algebra.rs @@ -349,13 +349,12 @@ impl MilnorAlgebra { impl Algebra for MilnorAlgebra { type BaseRing = Field; + type GradedPiece = FreeModule; fn base_ring(&self) -> Field { Field::new(self.prime()) } - type GradedPiece = FreeModule; - fn module_at(&self, t: i32) -> FreeModule { let piece = FreeModule::new(std::sync::Arc::new(self.base_ring()), String::new(), 0); piece.add_generators(0, self.dimension(t), None); diff --git a/ext/crates/algebra/src/lib.rs b/ext/crates/algebra/src/lib.rs index 47a54b9d00..c1984ca8bf 100644 --- a/ext/crates/algebra/src/lib.rs +++ b/ext/crates/algebra/src/lib.rs @@ -15,7 +15,7 @@ mod algebra; pub use crate::{ algebra::*, linear_algebra::{ - BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, Solvable, QuasiInverseOf, + BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, QuasiInverseOf, Solvable, SubmoduleOf, VectorOf, }, }; diff --git a/ext/crates/algebra/src/linear_algebra/mod.rs b/ext/crates/algebra/src/linear_algebra/mod.rs index 929a7dec25..83e562c2a6 100644 --- a/ext/crates/algebra/src/linear_algebra/mod.rs +++ b/ext/crates/algebra/src/linear_algebra/mod.rs @@ -1,13 +1,13 @@ //! The linear algebra of *solving* over a graded DVR. //! -//! This layer sits beside [`algebra`](crate::algebra) and [`module`](crate::module). The coefficient +//! This layer sits beside the `algebra` and [`module`](crate::module) modules. The coefficient //! *ring* — scalars, their arithmetic, and the representation of finite free modules over it -//! (vectors and slices) — is [`Ring`](crate::algebra::Ring); that is enough to define an algebra and +//! (vectors and slices) — is [`Ring`]; that is enough to define an algebra and //! act on its modules. This module adds the harder capability a free *resolution* needs: computing //! images, kernels, and quasi-inverses, which requires the ring to be a graded DVR (so that graded //! Nakayama holds and minimal generators can be read off mod the maximal ideal). //! -//! The central trait is [`Solvable`]: a [`Ring`](crate::algebra::Ring) over which one can solve +//! The central trait is [`Solvable`]: a [`Ring`] over which one can solve //! linear systems. For [`Field`] the vector types are exactly `fp`'s `FpVector`/`FpSlice`/`FpSliceMut` //! and every operation forwards to `fp`, so the classical path is unchanged and bit-identical. A //! future $\mathbb{F}_2[\tau]$ impl provides the graded-local solving (per-weight `fp` blocks plus a @@ -129,7 +129,7 @@ pub trait BaseSliceMut<'a, R: Ring> { /// The linear algebra of solving over a graded DVR `R`. /// -/// Extends [`Ring`](crate::algebra::Ring) — which already provides the ring's scalars and its vector +/// Extends [`Ring`] — which already provides the ring's scalars and its vector /// representation — with the operations a free resolution needs: computing the image, kernel, and /// quasi-inverse of an `R`-linear map. These are meaningful precisely because `R` is a graded DVR /// (graded-local, so graded Nakayama holds). For [`Field`] everything forwards to `fp`. 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 57ea7ae9b2..065864b610 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::{Algebra, BaseRingOf, Field, MuAlgebra, Ring, Scalar}, linear_algebra::{ - BaseSlice, BaseSliceMut, BaseSliceMutOf, Solvable, QuasiInverseOf, SubmoduleOf, VectorOf, + BaseSlice, BaseSliceMut, BaseSliceMutOf, QuasiInverseOf, Solvable, SubmoduleOf, VectorOf, }, module::{ Module, MuFreeModule, diff --git a/ext/crates/algebra/src/module/homomorphism/mod.rs b/ext/crates/algebra/src/module/homomorphism/mod.rs index c9aeb1c52d..4cb4a81c62 100644 --- a/ext/crates/algebra/src/module/homomorphism/mod.rs +++ b/ext/crates/algebra/src/module/homomorphism/mod.rs @@ -8,8 +8,7 @@ use fp::{ use crate::{ algebra::{Algebra, BaseRingOf, Field, Ring, Scalar}, linear_algebra::{ - BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, Solvable, QuasiInverseOf, - SubmoduleOf, + BaseSlice, BaseSliceMut, BaseSliceMutOf, BaseSliceOf, QuasiInverseOf, Solvable, SubmoduleOf, }, module::Module, }; diff --git a/ext/src/resolution.rs b/ext/src/resolution.rs index fe2cb4bfbd..7501b38359 100644 --- a/ext/src/resolution.rs +++ b/ext/src/resolution.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex, mpsc}; use algebra::{ - Algebra, Field, Solvable, MuAlgebra, Ring, + Algebra, Field, MuAlgebra, Ring, Solvable, linear_algebra::NextStageInput, module::{ Module, MuFreeModule, diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index eec0c148c1..4f7a794b92 100644 --- a/ext/src/secondary.rs +++ b/ext/src/secondary.rs @@ -1,7 +1,7 @@ use std::{io, sync::Arc}; use algebra::{ - Algebra, BaseRingOf, Field, Solvable, Ring, + Algebra, BaseRingOf, Field, Ring, Solvable, module::{ FreeModule, Module, homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, From 70667e98ece8b54a43fb361bf019f44293da66d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 19:03:23 +0000 Subject: [PATCH 7/7] Extract the classical module_at body into a shared helper Field, AdemAlgebra, and MilnorAlgebra all built their module_at graded piece with the same three lines (FreeModule::new + add_generators + compute_basis). Factor that into a pub(crate) classical_graded_piece helper in field.rs so the three classical impls share one source of truth. Behavior-identical (module_at is engine-unused); addresses the CodeRabbit review on #263. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JNkiiZghHggyMwDWfDn1y5 --- ext/crates/algebra/src/algebra/adem_algebra.rs | 8 +++----- ext/crates/algebra/src/algebra/field.rs | 16 ++++++++++++---- ext/crates/algebra/src/algebra/milnor_algebra.rs | 13 +++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ext/crates/algebra/src/algebra/adem_algebra.rs b/ext/crates/algebra/src/algebra/adem_algebra.rs index e2c99d0a4c..10e87fb644 100644 --- a/ext/crates/algebra/src/algebra/adem_algebra.rs +++ b/ext/crates/algebra/src/algebra/adem_algebra.rs @@ -19,8 +19,9 @@ use crate::{ algebra::{ Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, combinatorics::{self, MAX_XI_TAU}, + field::classical_graded_piece, }, - module::{FreeModule, Module as _}, + module::FreeModule, }; /// An Adem basis element for the Steenrod algebra. @@ -171,10 +172,7 @@ impl Algebra for AdemAlgebra { } fn module_at(&self, t: i32) -> FreeModule { - let piece = FreeModule::new(std::sync::Arc::new(self.base_ring()), String::new(), 0); - piece.add_generators(0, self.dimension(t), None); - piece.compute_basis(0); - piece + classical_graded_piece(self.base_ring(), self.dimension(t)) } fn prefix(&self) -> &str { diff --git a/ext/crates/algebra/src/algebra/field.rs b/ext/crates/algebra/src/algebra/field.rs index 9f4d7a7697..c960c3efbc 100644 --- a/ext/crates/algebra/src/algebra/field.rs +++ b/ext/crates/algebra/src/algebra/field.rs @@ -36,6 +36,17 @@ impl std::fmt::Display for Field { } } +/// Build the graded piece of a classical (`BaseRing = Field`) algebra: a free `Field`-module with +/// `num_gens` generators concentrated in weight 0. This is the shared body of the +/// [`module_at`](Algebra::module_at) accessor for every classical algebra ([`Field`], +/// [`AdemAlgebra`](crate::algebra::AdemAlgebra), [`MilnorAlgebra`](crate::algebra::MilnorAlgebra)). +pub(crate) fn classical_graded_piece(base_ring: Field, num_gens: usize) -> FreeModule { + let piece = FreeModule::new(Arc::new(base_ring), String::new(), 0); + piece.add_generators(0, num_gens, None); + piece.compute_basis(0); + piece +} + impl Algebra for Field { type BaseRing = Self; type GradedPiece = FreeModule; @@ -45,10 +56,7 @@ impl Algebra for Field { } fn module_at(&self, t: i32) -> FreeModule { - let piece = FreeModule::new(Arc::new(self.base_ring()), String::new(), 0); - piece.add_generators(0, self.dimension(t), None); - piece.compute_basis(0); - piece + classical_graded_piece(self.base_ring(), self.dimension(t)) } fn prime(&self) -> ValidPrime { diff --git a/ext/crates/algebra/src/algebra/milnor_algebra.rs b/ext/crates/algebra/src/algebra/milnor_algebra.rs index fd28f00e5c..1bce406100 100644 --- a/ext/crates/algebra/src/algebra/milnor_algebra.rs +++ b/ext/crates/algebra/src/algebra/milnor_algebra.rs @@ -10,8 +10,11 @@ use rustc_hash::FxHashMap as HashMap; use serde::{Deserialize, Serialize}; use crate::{ - algebra::{Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, combinatorics}, - module::{FreeModule, Module as _}, + algebra::{ + Algebra, Bialgebra, Field, GeneratedAlgebra, UnstableAlgebra, combinatorics, + field::classical_graded_piece, + }, + module::FreeModule, }; fn q_part_default() -> u32 { @@ -356,10 +359,7 @@ impl Algebra for MilnorAlgebra { } fn module_at(&self, t: i32) -> FreeModule { - let piece = FreeModule::new(std::sync::Arc::new(self.base_ring()), String::new(), 0); - piece.add_generators(0, self.dimension(t), None); - piece.compute_basis(0); - piece + classical_graded_piece(self.base_ring(), self.dimension(t)) } fn prefix(&self) -> &str { @@ -1809,6 +1809,7 @@ mod tests { use rstest::rstest; use super::*; + use crate::module::Module; #[test] fn module_at_is_the_graded_piece() {