From 6c78b264240185ef1a067ef4701604d7cd1c5cf7 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer Date: Tue, 23 Jun 2026 17:16:56 -0400 Subject: [PATCH 1/3] Forbid Matrix::trim keep_pivots with nonzero col_start --- ext/crates/fp/src/matrix/matrix_inner.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/crates/fp/src/matrix/matrix_inner.rs b/ext/crates/fp/src/matrix/matrix_inner.rs index 392625cb6b..8c6dd93b38 100644 --- a/ext/crates/fp/src/matrix/matrix_inner.rs +++ b/ext/crates/fp/src/matrix/matrix_inner.rs @@ -1081,6 +1081,10 @@ impl Matrix { } pub fn trim(&mut self, row_start: usize, row_end: usize, col_start: usize, keep_pivots: bool) { + assert!( + !keep_pivots || col_start == 0, + "trim cannot keep pivots when col_start != 0: column indices would shift" + ); let mut new = Self::new(self.prime(), row_end - row_start, self.columns - col_start); for (i, mut row) in new.iter_mut().enumerate() { row.assign(self.row(row_start + i).restrict(col_start, self.columns)); From 4d73e6ccf1ffc0b69836c680da6f90bac92b7a90 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer Date: Fri, 3 Apr 2026 04:36:58 -0400 Subject: [PATCH 2/3] First pass at unification --- .github/workflows/ext.yaml | 3 - ext/Cargo.toml | 1 - ext/Makefile | 15 +- .../algebra/src/module/homomorphism/mod.rs | 2 +- .../benchmarks/differentials-S_2-nassau | 22 - .../benchmarks/filtration_one-S_2-nassau | 33 -- ext/examples/benchmarks/lift_hom-g-S_2-nassau | 12 - ext/examples/benchmarks/resolve-S_2-nassau | 14 - .../resolve_through_stem-S_2-nassau | 14 - ext/examples/benchmarks/secondary-C2-nassau | 12 - .../secondary_product-h_0-S_2-nassau | 38 -- .../benchmarks/steenrod-c_0-S_2-nassau | 6 - ext/examples/benchmarks/yoneda-g-S_2-nassau | 6 - ext/examples/bruner.rs | 13 - ext/examples/ext_m_n.rs | 4 - ext/examples/lift_hom.rs | 45 +- ext/flake.nix | 1 - ext/src/chain_complex/mod.rs | 2 +- ext/src/lib.rs | 3 - ext/src/nassau.rs | 557 ++++++------------ ext/src/resolution.rs | 87 ++- ext/src/utils.rs | 93 +-- ext/tests/milnor_vs_nassau.rs | 25 +- ext/tests/save_load_resolution.rs | 22 +- 24 files changed, 295 insertions(+), 735 deletions(-) delete mode 100644 ext/examples/benchmarks/differentials-S_2-nassau delete mode 100644 ext/examples/benchmarks/filtration_one-S_2-nassau delete mode 100644 ext/examples/benchmarks/lift_hom-g-S_2-nassau delete mode 100644 ext/examples/benchmarks/resolve-S_2-nassau delete mode 100644 ext/examples/benchmarks/resolve_through_stem-S_2-nassau delete mode 100644 ext/examples/benchmarks/secondary-C2-nassau delete mode 100644 ext/examples/benchmarks/secondary_product-h_0-S_2-nassau delete mode 100644 ext/examples/benchmarks/steenrod-c_0-S_2-nassau delete mode 100644 ext/examples/benchmarks/yoneda-g-S_2-nassau diff --git a/.github/workflows/ext.yaml b/.github/workflows/ext.yaml index 569743feeb..8c7edc5039 100644 --- a/.github/workflows/ext.yaml +++ b/.github/workflows/ext.yaml @@ -45,9 +45,6 @@ jobs: - name: Run ext examples run: make -C ext benchmarks - - name: Run ext examples (nassau) - run: make -C ext benchmarks-nassau - - name: Run ext examples (concurrent) run: make -C ext benchmarks-concurrent diff --git a/ext/Cargo.toml b/ext/Cargo.toml index 8b3919aba8..109175d413 100644 --- a/ext/Cargo.toml +++ b/ext/Cargo.toml @@ -58,7 +58,6 @@ concurrent = [ ] odd-primes = ["fp/odd-primes", "algebra/odd-primes", "sseq/odd-primes"] logging = [] -nassau = [] [workspace] members = [ diff --git a/ext/Makefile b/ext/Makefile index 963bc530a6..0af9af111a 100644 --- a/ext/Makefile +++ b/ext/Makefile @@ -25,13 +25,11 @@ docs: echo "window.ALL_CRATES = [$$(ls crates/ | sed "s/.*/'&',/; s/-/_/g")'ext'];" > target/doc/crates.js -BENCHMARKS = $(filter-out examples/benchmarks/%-nassau, $(wildcard examples/benchmarks/*)) +BENCHMARKS = $(wildcard examples/benchmarks/*) benchmarks: $(BENCHMARKS) -benchmarks-nassau: $(wildcard examples/benchmarks/*-nassau) fix-benchmarks: $(patsubst examples/benchmarks/%, examples/benchmarks/%-fixed, BENCHMARKS) -fix-benchmarks-nassau: $(patsubst examples/benchmarks/%, examples/benchmarks/%-fixed, $(wildcard examples/benchmarks/*-nassau)) benchmarks-concurrent: $(patsubst examples/benchmarks/%, examples/benchmarks/%-concurrent, $(BENCHMARKS)) @@ -43,21 +41,10 @@ examples/benchmarks/%-fixed: dummy else \ mv $@ $(FILE); \ fi -examples/benchmarks/%-nassau-fixed: dummy - (head -n 1 $(FILE)-nassau && bash -c "echo '' | cargo run --features nassau --example $$(head -n 1 $(FILE)-nassau)") > $@ - if diff --color $(FILE)-nassau $@; then \ - rm $@; \ - else \ - mv $@ $(FILE)-nassau; \ - fi - examples/benchmarks/%: dummy (head -n 1 $@ && bash -c "echo '' | cargo run --example $$(head -n 1 $@)") | diff --color $@ - -examples/benchmarks/%-nassau: dummy - (head -n 1 $@ && bash -c "echo '' | cargo run --features nassau --example $$(head -n 1 $@)") | diff --color $@ - - examples/benchmarks/%-concurrent: FILE = examples/benchmarks/$* examples/benchmarks/%-concurrent: dummy @{ \ diff --git a/ext/crates/algebra/src/module/homomorphism/mod.rs b/ext/crates/algebra/src/module/homomorphism/mod.rs index 540b34ac4b..e83ffa5aad 100644 --- a/ext/crates/algebra/src/module/homomorphism/mod.rs +++ b/ext/crates/algebra/src/module/homomorphism/mod.rs @@ -39,7 +39,7 @@ 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 + 'static { type Source: Module; type Target: Module::Algebra>; diff --git a/ext/examples/benchmarks/differentials-S_2-nassau b/ext/examples/benchmarks/differentials-S_2-nassau deleted file mode 100644 index a25c426760..0000000000 --- a/ext/examples/benchmarks/differentials-S_2-nassau +++ /dev/null @@ -1,22 +0,0 @@ -differentials -- S_2 "" 10 5 -d x_(0,0,0) = 0 -d x_(0,1,0) = P(1) x_(0,0,0) -d x_(0,2,0) = P(1) x_(0,1,0) -d x_(0,3,0) = P(1) x_(0,2,0) -d x_(0,4,0) = P(1) x_(0,3,0) -d x_(0,5,0) = P(1) x_(0,4,0) -d x_(1,1,0) = P(2) x_(0,0,0) -d x_(2,2,0) = P(3) x_(0,1,0) + P(2) x_(1,1,0) -d x_(3,1,0) = P(4) x_(0,0,0) -d x_(3,2,0) = P(4) x_(0,1,0) + P(0, 1) x_(1,1,0) + P(1) x_(3,1,0) -d x_(3,3,0) = P(4) x_(0,2,0) + P(2) x_(2,2,0) + P(1) x_(3,2,0) -d x_(6,2,0) = P(1, 2) x_(0,1,0) + P(0, 2) x_(1,1,0) + P(4) x_(3,1,0) -d x_(7,1,0) = P(8) x_(0,0,0) -d x_(7,2,0) = P(8) x_(0,1,0) + P(2, 1) x_(3,1,0) + P(1) x_(7,1,0) -d x_(7,3,0) = P(8) x_(0,2,0) + P(2, 1) x_(3,2,0) + P(1) x_(7,2,0) -d x_(7,4,0) = P(8) x_(0,3,0) + P(2, 1) x_(3,3,0) + P(1) x_(7,3,0) -d x_(8,2,0) = P(8) x_(1,1,0) + P(1, 0, 1) x_(1,1,0) + P(0, 2) x_(3,1,0) + P(2) x_(7,1,0) -d x_(8,3,0) = P(4, 1) x_(2,2,0) + P(1, 2) x_(2,2,0) + P(0, 0, 1) x_(2,2,0) + P(6) x_(3,2,0) + P(3) x_(6,2,0) + P(0, 1) x_(6,2,0) -d x_(9,3,0) = P(3, 0, 1) x_(0,2,0) + P(8) x_(2,2,0) + P(1, 0, 1) x_(2,2,0) + P(4) x_(6,2,0) + P(3) x_(7,2,0) + P(2) x_(8,2,0) -d x_(9,4,0) = P(4, 2) x_(0,3,0) + P(4, 1) x_(3,3,0) + P(1, 2) x_(3,3,0) + P(0, 0, 1) x_(3,3,0) + P(2) x_(8,3,0) -d x_(9,5,0) = P(10) x_(0,4,0) + P(3) x_(7,4,0) + P(0, 1) x_(7,4,0) diff --git a/ext/examples/benchmarks/filtration_one-S_2-nassau b/ext/examples/benchmarks/filtration_one-S_2-nassau deleted file mode 100644 index 20d8843a4d..0000000000 --- a/ext/examples/benchmarks/filtration_one-S_2-nassau +++ /dev/null @@ -1,33 +0,0 @@ -filtration_one -- S_2 "" 10 5 -h_0 x_(0, 0, 0) = [1] -h_1 x_(0, 0, 0) = [1] -h_2 x_(0, 0, 0) = [1] -h_3 x_(0, 0, 0) = [1] -h_0 x_(0, 1, 0) = [1] -h_2 x_(0, 1, 0) = [1] -h_3 x_(0, 1, 0) = [1] -h_0 x_(0, 2, 0) = [1] -h_2 x_(0, 2, 0) = [1] -h_3 x_(0, 2, 0) = [1] -h_0 x_(0, 3, 0) = [1] -h_3 x_(0, 3, 0) = [1] -h_0 x_(0, 4, 0) = [1] -h_1 x_(1, 1, 0) = [1] -h_3 x_(1, 1, 0) = [1] -h_1 x_(2, 2, 0) = [1] -h_3 x_(2, 2, 0) = [1] -h_0 x_(3, 1, 0) = [1] -h_2 x_(3, 1, 0) = [1] -h_0 x_(3, 2, 0) = [1] -h_1 x_(6, 2, 0) = [0] -h_2 x_(6, 2, 0) = [1] -h_0 x_(7, 1, 0) = [1] -h_1 x_(7, 1, 0) = [1] -h_0 x_(7, 2, 0) = [1] -h_1 x_(7, 2, 0) = [0] -h_0 x_(7, 3, 0) = [1] -h_0 x_(8, 2, 0) = [0] -h_1 x_(8, 2, 0) = [1] -h_1 x_(8, 3, 0) = [1] -h_0 x_(9, 3, 0) = [0] -h_0 x_(9, 4, 0) = [0] diff --git a/ext/examples/benchmarks/lift_hom-g-S_2-nassau b/ext/examples/benchmarks/lift_hom-g-S_2-nassau deleted file mode 100644 index 3176d0cee7..0000000000 --- a/ext/examples/benchmarks/lift_hom-g-S_2-nassau +++ /dev/null @@ -1,12 +0,0 @@ -lift_hom -- S_2 /tmp/test_nassau_s_2 25 9 S_2 g 20 4 [1] -g x_(0, 0, 0) = [1] -g x_(0, 1, 0) = [1] -g x_(0, 2, 0) = [1] -g x_(0, 3, 0) = [] -g x_(0, 4, 0) = [] -g x_(0, 5, 0) = [] -g x_(1, 1, 0) = [1] -g x_(2, 2, 0) = [] -g x_(3, 1, 0) = [1] -g x_(3, 2, 0) = [1] -g x_(3, 3, 0) = [0] diff --git a/ext/examples/benchmarks/resolve-S_2-nassau b/ext/examples/benchmarks/resolve-S_2-nassau deleted file mode 100644 index 0898911373..0000000000 --- a/ext/examples/benchmarks/resolve-S_2-nassau +++ /dev/null @@ -1,14 +0,0 @@ -resolve -- S_2 "" 30 11 "" -· · -· · · -· · · -· · · · -· · · · · · -· · · · · · · · · -· · · · : · · · · · · -· · · · · · : · · · -· · · · · · · · · · · -· · · · · · · · · · -· · · · · -· - diff --git a/ext/examples/benchmarks/resolve_through_stem-S_2-nassau b/ext/examples/benchmarks/resolve_through_stem-S_2-nassau deleted file mode 100644 index 9f84a24d24..0000000000 --- a/ext/examples/benchmarks/resolve_through_stem-S_2-nassau +++ /dev/null @@ -1,14 +0,0 @@ -resolve_through_stem -- S_2 "" 30 11 "" -· · · · · · -· · · · · · · · · -· · · · : · · · · · -· · · · · · · · · · -· · · · · · · · · -· · · · · · · · · · · -· · · · : · · · · · · · -· · · · · · : · · · · -· · · · · · · · · · · · -· · · · · · · · · · · -· · · · · -· - diff --git a/ext/examples/benchmarks/secondary-C2-nassau b/ext/examples/benchmarks/secondary-C2-nassau deleted file mode 100644 index 0dc2bb66f5..0000000000 --- a/ext/examples/benchmarks/secondary-C2-nassau +++ /dev/null @@ -1,12 +0,0 @@ -secondary -- C2 /tmp/test_nassau_c2 30 7 "" -d_2 x_(9, 2, 0) = [0] -d_2 x_(10, 3, 0) = [0] -d_2 x_(17, 3, 0) = [0] -d_2 x_(17, 4, 0) = [1] -d_2 x_(18, 2, 0) = [0] -d_2 x_(18, 4, 0) = [0] -d_2 x_(19, 5, 0) = [1] -d_2 x_(20, 3, 0) = [0] -d_2 x_(22, 3, 0) = [0] -d_2 x_(22, 4, 0) = [0] -d_2 x_(24, 5, 0) = [0] diff --git a/ext/examples/benchmarks/secondary_product-h_0-S_2-nassau b/ext/examples/benchmarks/secondary_product-h_0-S_2-nassau deleted file mode 100644 index 75dbb090de..0000000000 --- a/ext/examples/benchmarks/secondary_product-h_0-S_2-nassau +++ /dev/null @@ -1,38 +0,0 @@ -secondary_product -- S_2 /tmp/test_nassau_s_2 30 7 h_0 0 1 [1] -[h_0] [x_(0, 0, 0)] = [1] + λ [0] -[h_0] [x_(0, 1, 0)] = [1] + λ [1] -[h_0] [x_(0, 2, 0)] = [1] + λ [0] -[h_0] [x_(0, 3, 0)] = [1] + λ [1] -[h_0] [x_(0, 4, 0)] = [1] + λ [0] -[h_0] [x_(0, 5, 0)] = [1] + λ [1] -[h_0] [x_(3, 1, 0)] = [1] + λ [1] -[h_0] [x_(3, 2, 0)] = [1] + λ [] -[h_0] [x_(7, 1, 0)] = [1] + λ [0] -[h_0] [x_(7, 2, 0)] = [1] + λ [1] -[h_0] [x_(7, 3, 0)] = [1] + λ [] -[h_0] [x_(8, 2, 0)] = [0] + λ [] -[h_0] [x_(9, 3, 0)] = [0] + λ [0] -[h_0] [x_(9, 4, 0)] = [0] + λ [] -[h_0] [x_(11, 5, 0)] = [1] + λ [1] -[h_0] [x_(14, 2, 0)] = [1] + λ [0] -[h_0] [x_(14, 3, 0)] = [0] + λ [1] -[h_0] [x_(14, 4, 0)] = [1] + λ [0] -[h_0] [x_(14, 5, 0)] = [1] + λ [] -[h_0] λ x_(15, 1, 0) = λ [1] -[h_0] [x_(15, 2, 0)] = [1] + λ [1] -[h_0] [x_(15, 3, 0)] = [1] + λ [0, 0] -[h_0] [x_(15, 4, 0)] = [1, 0] + λ [1] -[h_0] [x_(15, 5, 0)] = [1] + λ [0] -[h_0] [x_(15, 5, 1)] = [0] + λ [0] -[h_0] [x_(17, 3, 0)] = [0] + λ [0] -[h_0] λ x_(17, 4, 0) = λ [1] -[h_0] [x_(17, 5, 0)] = [1] + λ [0] -[h_0] [x_(18, 2, 0)] = [1] + λ [1, 0] -[h_0] [x_(18, 3, 0)] = [1, 0] + λ [0] -[h_0] λ x_(18, 4, 1) = λ [1] -[h_0] [x_(18, 4, 0)] = [0] + λ [] -[h_0] [x_(20, 4, 0)] = [1] + λ [1] -[h_0] [x_(20, 5, 0)] = [1] + λ [] -[h_0] [x_(21, 3, 0)] = [] + λ [0] -[h_0] [x_(23, 4, 0)] = [0] + λ [0] -[h_0] [x_(23, 5, 0)] = [1] + λ [0] diff --git a/ext/examples/benchmarks/steenrod-c_0-S_2-nassau b/ext/examples/benchmarks/steenrod-c_0-S_2-nassau deleted file mode 100644 index d0512568b5..0000000000 --- a/ext/examples/benchmarks/steenrod-c_0-S_2-nassau +++ /dev/null @@ -1,6 +0,0 @@ -steenrod -- S_2 /tmp/save_nassau_s_2 8 3 [1] -Dimensions of Yoneda representative: 1 5 7 4 1 -Sq^3 x_(8, 3, 0) = [1] -Sq^2 x_(8, 3, 0) = [1] -Sq^1 x_(8, 3, 0) = [0, 1] -Sq^0 x_(8, 3, 0) = [1] diff --git a/ext/examples/benchmarks/yoneda-g-S_2-nassau b/ext/examples/benchmarks/yoneda-g-S_2-nassau deleted file mode 100644 index f25dafd924..0000000000 --- a/ext/examples/benchmarks/yoneda-g-S_2-nassau +++ /dev/null @@ -1,6 +0,0 @@ -yoneda -- S_2 "" 20 4 [1] -Dimension of 0th module is 15 -Dimension of 1th module is 41 -Dimension of 2th module is 32 -Dimension of 3th module is 6 -Dimension of 4th module is 1 diff --git a/ext/examples/bruner.rs b/ext/examples/bruner.rs index 71c5c0e22a..eb353d9ded 100644 --- a/ext/examples/bruner.rs +++ b/ext/examples/bruner.rs @@ -36,9 +36,6 @@ use ext::{ use fp::{matrix::Matrix, prime::TWO, vector::FpVector}; use sseq::coordinates::{Bidegree, BidegreeGenerator}; -#[cfg(feature = "nassau")] -type FreeModule = FM; -#[cfg(not(feature = "nassau"))] type FreeModule = FM; type FreeModuleHomomorphism = FMH; @@ -134,10 +131,6 @@ fn get_element( /// Create a new `FiniteChainComplex` with `num_s` many non-zero modules. fn create_chain_complex(num_s: usize) -> FiniteChainComplex { - #[cfg(feature = "nassau")] - let algebra: Arc = Arc::new(MilnorAlgebra::new(TWO, false)); - - #[cfg(not(feature = "nassau"))] let algebra: Arc = Arc::new(algebra::SteenrodAlgebra::MilnorAlgebra( MilnorAlgebra::new(TWO, false), )); @@ -235,12 +228,6 @@ fn main() -> anyhow::Result<()> { core::result::Result::::Ok(PathBuf::from(x)) }); - #[cfg(feature = "nassau")] - assert!( - save_dir.is_some(), - "A save directory is required for comparison between Bruner and Nassau resolutions." - ); - let resolution = ext::utils::construct("S_2@milnor", save_dir).unwrap(); resolution.compute_through_stem(max); diff --git a/ext/examples/ext_m_n.rs b/ext/examples/ext_m_n.rs index 0796354f3d..05895e3054 100644 --- a/ext/examples/ext_m_n.rs +++ b/ext/examples/ext_m_n.rs @@ -11,12 +11,8 @@ fn main() -> anyhow::Result<()> { eprintln!("This script computes Ext(M, N)"); let res = ext::utils::query_module_only("Module M", None, false)?; let module_spec = query::raw("Module N", ext::utils::parse_module_name); - #[cfg(not(feature = "nassau"))] let module = algebra::module::steenrod_module::from_json(res.algebra(), &module_spec)?; - #[cfg(feature = "nassau")] - let module = algebra::module::FDModule::from_json(res.algebra(), &module_spec)?; - let max = Bidegree::n_s( query::raw("Max n", str::parse), query::raw("Max s", str::parse), diff --git a/ext/examples/lift_hom.rs b/ext/examples/lift_hom.rs index 3958a6069c..9e13ad39a1 100644 --- a/ext/examples/lift_hom.rs +++ b/ext/examples/lift_hom.rs @@ -42,7 +42,7 @@ use std::{path::PathBuf, sync::Arc}; use algebra::module::Module; -use anyhow::{Context, anyhow}; +use anyhow::Context; use ext::{ chain_complex::{AugmentedChainComplex, ChainComplex, FreeChainComplex}, resolution_homomorphism::ResolutionHomomorphism, @@ -61,30 +61,25 @@ fn main() -> anyhow::Result<()> { ); let source_name = source.name(); - let target = query::with_default("Target module", source_name, |s| { - if s == source_name { - Ok(Arc::clone(&source)) - } else if cfg!(feature = "nassau") { - Err(anyhow!("Can only resolve S_2 with nassau")) - } else { - let config: utils::Config = s.try_into()?; - let save_dir = query::optional("Target save directory", |x| { - Result::::Ok(PathBuf::from(x)) - }); - - let mut target = utils::construct(config, save_dir) - .context("Failed to load module from save file") - .unwrap(); - - target.set_name(s.to_owned()); - - #[cfg(feature = "nassau")] - unreachable!(); - - #[cfg(not(feature = "nassau"))] - Ok(Arc::new(target)) - } - }); + let target: Arc = + query::with_default("Target module", source_name, |s| -> anyhow::Result<_> { + if s == source_name { + Ok(Arc::clone(&source)) + } else { + let config: utils::Config = s.try_into()?; + let save_dir = query::optional("Target save directory", |x| { + Result::::Ok(PathBuf::from(x)) + }); + + let mut target = utils::construct(config, save_dir) + .context("Failed to load module from save file") + .unwrap(); + + target.set_name(s.to_owned()); + + Ok(Arc::new(target)) + } + }); assert_eq!(source.prime(), target.prime()); let p = source.prime(); diff --git a/ext/flake.nix b/ext/flake.nix index cfc98ca094..36ff098e83 100644 --- a/ext/flake.nix +++ b/ext/flake.nix @@ -47,7 +47,6 @@ make lint make test make benchmarks - make benchmarks-nassau make benchmarks-concurrent make miri ''); diff --git a/ext/src/chain_complex/mod.rs b/ext/src/chain_complex/mod.rs index dd19d9a70f..07a2686d89 100644 --- a/ext/src/chain_complex/mod.rs +++ b/ext/src/chain_complex/mod.rs @@ -160,7 +160,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 { +pub trait ChainComplex: Send + Sync + 'static { type Algebra: Algebra; type Module: Module; type Homomorphism: ModuleHomomorphism; diff --git a/ext/src/lib.rs b/ext/src/lib.rs index ad5a6afb26..bef4a91310 100644 --- a/ext/src/lib.rs +++ b/ext/src/lib.rs @@ -160,9 +160,6 @@ //! $\mathrm{tmf}$ modules. //! - `logging`: Print timing information of the computations to stderr. Note that this has no //! effect unless the `RUST_LOG` environment variable is set appropriately. -//! - `nassau`: Use Nassau's algorithm to compute the minimal resolution instead of the usual -//! minimal resolution algorithm. When this feature is enabled, only finite dimensional modules -//! at the prime 2 can be resolved. #![allow(clippy::upper_case_acronyms)] #![deny(clippy::use_self, unsafe_op_in_unsafe_fn)] diff --git a/ext/src/nassau.rs b/ext/src/nassau.rs index 8d1919136a..858c037c7d 100644 --- a/ext/src/nassau.rs +++ b/ext/src/nassau.rs @@ -1,78 +1,36 @@ //! This module implements [Nassau's algorithm](https://arxiv.org/abs/1910.04063). //! -//! The main export is the [`Resolution`] object, which is a resolution of the sphere at the prime 2 -//! using Nassau's algorithm. It aims to provide an API similar to -//! [`resolution::Resolution`](crate::resolution::Resolution). From an API point of view, the main -//! difference between the two is that our `Resolution` is a chain complex over [`MilnorAlgebra`] -//! over [`SteenrodAlgebra`](algebra::SteenrodAlgebra). -//! -//! To make use of this resolution in the example scripts, enable the `nassau` feature. This will -//! cause [`utils::query_module`](crate::utils::query_module) to return the `Resolution` from this -//! module instead of [`resolution`](crate::resolution). There is no formal polymorphism involved; -//! the feature changes the return type of the function. While this is an incorrect use of features, -//! we find that this the easiest way to make all scripts support both types of resolutions. - -use std::{ - fmt::Display, - io, - sync::{Arc, Mutex, mpsc}, -}; +//! Nassau's algorithm is a specialized algorithm for computing minimal resolutions at the prime 2 +//! using the Milnor basis. It is implemented as methods on +//! [`Resolution`](crate::resolution::Resolution) that are dispatched to at runtime when +//! the conditions are met (prime 2, Milnor algebra, bounded module, save directory present). + +use std::{fmt::Display, io}; use algebra::{ - Algebra, combinatorics, + combinatorics, milnor_algebra::{MilnorAlgebra, PPartEntry}, module::{ - FreeModule, GeneratorData, Module, ZeroModule, - homomorphism::{FreeModuleHomomorphism, FullModuleHomomorphism, ModuleHomomorphism}, + FreeModule, GeneratorData, Module, + homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, }, }; -use anyhow::anyhow; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use fp::{ matrix::{AugmentedMatrix, Matrix}, - prime::{TWO, ValidPrime}, + prime::TWO, vector::{FpSlice, FpSliceMut, FpVector}, }; use itertools::Itertools; -use once::OnceBiVec; use sseq::coordinates::{Bidegree, BidegreeGenerator}; use crate::{ - chain_complex::{AugmentedChainComplex, ChainComplex, FiniteChainComplex, FreeChainComplex}, - save::{SaveDirectory, SaveKind}, + chain_complex::{AugmentedChainComplex, ChainComplex, FreeChainComplex}, + resolution::Resolution, + save::SaveKind, utils::{LogWriter, parallel::ParallelGuard}, }; -/// See [`resolution::SenderData`](../resolution/struct.SenderData.html). This differs by not having the `new` field. -struct SenderData { - b: Bidegree, - retry: bool, - sender: mpsc::Sender, -} - -impl SenderData { - pub(crate) fn send(b: Bidegree, sender: mpsc::Sender) { - sender - .send(Self { - b, - retry: false, - sender: sender.clone(), - }) - .unwrap() - } - - pub(crate) fn send_retry(b: Bidegree, sender: mpsc::Sender) { - tracing::info!(%b, "retrying"); - sender - .send(Self { - b, - retry: true, - sender: sender.clone(), - }) - .unwrap() - } -} - const MAX_NEW_GENS: usize = 10; /// A Milnor subalgebra to be used in [Nassau's algorithm](https://arxiv.org/abs/1910.04063). This @@ -117,11 +75,13 @@ impl MilnorSubalgebra { /// Give a list of basis elements in degree `degree` that has signature `signature`. /// - /// This requires passing the algebra for borrow checker reasons. - fn signature_mask<'a>( + /// The `milnor` parameter provides access to the P-part table. The `module` can be over any + /// algebra type as long as it provides generator offset data (the offsets are identical when + /// the algebra is actually Milnor). + fn signature_mask<'a, A: algebra::Algebra>( &'a self, - algebra: &'a MilnorAlgebra, - module: &'a FreeModule, + milnor: &'a MilnorAlgebra, + module: &'a FreeModule, degree: i32, signature: &'a [PPartEntry], ) -> impl Iterator + 'a { @@ -131,7 +91,7 @@ impl MilnorSubalgebra { start: [offset], end: _, }| { - algebra + milnor .ppart_table(degree - gen_deg) .iter() .enumerate() @@ -148,24 +108,26 @@ impl MilnorSubalgebra { /// Get the matrix of a free module homomorphism when restricted to the subquotient given by /// the signature. - fn signature_matrix( + /// + /// The `milnor` parameter provides access to the P-part table. + fn signature_matrix( &self, - hom: &FreeModuleHomomorphism>, + milnor: &MilnorAlgebra, + hom: &FreeModuleHomomorphism>, degree: i32, signature: &[PPartEntry], ) -> Matrix { let p = hom.prime(); let source = hom.source(); let target = hom.target(); - let algebra = target.algebra(); let target_degree = degree - hom.degree_shift(); let target_mask: Vec = self - .signature_mask(&algebra, &target, degree - hom.degree_shift(), signature) + .signature_mask(milnor, &target, degree - hom.degree_shift(), signature) .collect(); let source_mask: Vec = self - .signature_mask(&algebra, &source, degree, signature) + .signature_mask(milnor, &source, degree, signature) .collect(); let mut scratch = FpVector::new(p, target.dimension(target_degree)); @@ -388,67 +350,47 @@ enum Magic { Fix = -3, } -/// A resolution of `S_2` using Nassau's algorithm. +/// Context for Nassau's algorithm, extracted once per step. /// -/// This aims to have an API similar to that of -/// [`resolution::Resolution`](crate::resolution::Resolution). From an API point of view, the main -/// difference between the two is that this is a chain complex over [`MilnorAlgebra`] over -/// [`SteenrodAlgebra`](algebra::SteenrodAlgebra). -pub struct Resolution> { - lock: Mutex<()>, - name: String, - max_degree: i32, - modules: OnceBiVec>>, - zero_module: Arc>, - differentials: OnceBiVec>>>, - target: Arc>, - chain_maps: OnceBiVec>>, - save_dir: SaveDirectory, +/// Holds onto the `Arc` so that the `&MilnorAlgebra` reference remains valid. +pub(crate) struct NassauContext { + algebra: std::sync::Arc, } -impl> Resolution { - pub fn name(&self) -> &str { - &self.name - } - - pub fn set_name(&mut self, name: String) { - self.name = name; - } - - pub fn new(module: Arc) -> Self { - Self::new_with_save(module, None).unwrap() +impl NassauContext { + /// Get the inner [`MilnorAlgebra`]. + pub fn milnor(&self) -> &MilnorAlgebra { + match &*self.algebra { + algebra::SteenrodAlgebra::MilnorAlgebra(m) => m, + _ => unreachable!("NassauContext is only created when algebra is MilnorAlgebra"), + } } +} - pub fn new_with_save( - module: Arc, - save_dir: impl Into, - ) -> anyhow::Result { - let save_dir = save_dir.into(); - let max_degree = module - .max_degree() - .ok_or_else(|| anyhow!("Nassau's algorithm requires bounded module"))?; - let target = Arc::new(FiniteChainComplex::ccdz(module)); - - if let Some(p) = save_dir.write() { - for subdir in SaveKind::nassau_data() { - subdir.create_dir(p)?; - } +impl Resolution { + /// Check all Nassau preconditions. Returns context if eligible. + /// + /// Nassau's algorithm requires: + /// - Prime 2 + /// - Milnor basis (the algebra is `SteenrodAlgebra::MilnorAlgebra`) + /// - Bounded target module (finite-dimensional) + /// - A save directory (Nassau stores qi data on disk) + pub(crate) fn nassau_context(&self) -> Option { + if self.prime() != TWO { + return None; } - - Ok(Self { - lock: Mutex::new(()), - zero_module: Arc::new(FreeModule::new(target.algebra(), "F_{-1}".to_string(), 0)), - name: String::new(), - modules: OnceBiVec::new(0), - differentials: OnceBiVec::new(0), - chain_maps: OnceBiVec::new(0), - target, - max_degree, - save_dir, - }) + let algebra = self.algebra(); + if !matches!(&*algebra, algebra::SteenrodAlgebra::MilnorAlgebra(_)) { + return None; + } + self.target().module(0).max_degree()?; + if self.save_dir.is_none() { + return None; + } + Some(NassauContext { algebra }) } - fn add_generators(&self, b: Bidegree, num_new_gens: usize) { + fn nassau_add_generators(&self, b: Bidegree, num_new_gens: usize) { let gen_names = (0..num_new_gens) .map(|idx| format!("x_{:#}", BidegreeGenerator::new(b, idx))) .collect(); @@ -456,47 +398,8 @@ impl> Resolution { .add_generators(b.t(), num_new_gens, Some(gen_names)); } - /// This function prepares the Resolution object to perform computations up to the - /// specified s degree. It does *not* perform any computations by itself. It simply lengthens - /// the `OnceVec`s `modules`, `chain_maps`, etc. to the right length. - fn extend_through_degree(&self, max_s: i32) { - let min_degree = self.min_degree(); - - self.modules.extend(max_s, |i| { - Arc::new(FreeModule::new( - Arc::clone(&self.algebra()), - format!("F{i}"), - min_degree, - )) - }); - - self.differentials.extend(0, |_| { - Arc::new(FreeModuleHomomorphism::new( - Arc::clone(&self.modules[0]), - Arc::clone(&self.zero_module), - 0, - )) - }); - - self.differentials.extend(max_s, |i| { - Arc::new(FreeModuleHomomorphism::new( - Arc::clone(&self.modules[i]), - Arc::clone(&self.modules[i - 1]), - 0, - )) - }); - - self.chain_maps.extend(max_s, |i| { - Arc::new(FreeModuleHomomorphism::new( - Arc::clone(&self.modules[i]), - self.target.module(i), - 0, - )) - }); - } - - #[tracing::instrument(skip_all, fields(throughput))] - fn write_qi( + #[tracing::instrument(skip_all, fields(signature = ?signature, throughput))] + fn nassau_write_qi( f: &mut Option, scratch: &mut FpVector, signature: &[PPartEntry], @@ -548,13 +451,15 @@ impl> Resolution { Ok(()) } - fn write_differential( + fn nassau_write_differential( &self, b: Bidegree, num_new_gens: usize, target_dim: usize, ) -> anyhow::Result<()> { - if let Some(dir) = self.save_dir.write() { + if self.should_save + && let Some(dir) = self.save_dir.write() + { let mut f = self .save_file(SaveKind::NassauDifferential, b) .create_file(dir.clone(), false); @@ -568,37 +473,39 @@ impl> Resolution { Ok(()) } - #[tracing::instrument(skip(self), fields(%b, %subalgebra, num_new_gens, density))] - fn step_resolution_with_subalgebra( + #[tracing::instrument(skip(self, milnor), fields(%b, %subalgebra, num_new_gens, density))] + fn nassau_step_with_subalgebra( &self, b: Bidegree, subalgebra: MilnorSubalgebra, + milnor: &MilnorAlgebra, ) -> anyhow::Result<()> { let end = || { tracing::Span::current().record("num_new_gens", self.number_of_gens_in_bidegree(b)); tracing::Span::current().record( "density", - self.differentials[b.s()].differential_density(b.t()) * 100.0, + self.differential(b.s()).differential_density(b.t()) * 100.0, ); }; let p = self.prime(); let mut scratch = FpVector::new(p, 0); - let target = &*self.modules[b.s() - 1]; - let algebra = target.algebra(); + let target = self.module(b.s() - 1); let zero_sig = subalgebra.zero_signature(); let target_dim = target.dimension(b.t()); let target_mask: Vec = subalgebra - .signature_mask(&algebra, target, b.t(), &zero_sig) + .signature_mask(milnor, &target, b.t(), &zero_sig) .collect(); let target_masked_dim = target_mask.len(); - let next = &self.modules[b.s() - 2]; + let next = self.module(b.s() - 2); next.compute_basis(b.t()); - let mut f = if let Some(dir) = self.save_dir().write() { + let mut f = if self.should_save + && let Some(dir) = self.save_dir().write() + { let mut f = self .save_file(SaveKind::NassauQi, b - Bidegree::s_t(1, 0)) .create_file(dir.to_owned(), true); @@ -612,13 +519,14 @@ impl> Resolution { let guard = tracing::info_span!("step", signature = ?zero_sig).entered(); let next_mask: Vec = subalgebra - .signature_mask(&algebra, &self.modules[b.s() - 2], b.t(), &zero_sig) + .signature_mask(milnor, &next, b.t(), &zero_sig) .collect(); let next_masked_dim = next_mask.len(); let full_matrix = { let _guard = ParallelGuard::new(); - self.differentials[b.s() - 1].get_partial_matrix(b.t(), &target_mask) + self.differential(b.s() - 1) + .get_partial_matrix(b.t(), &target_mask) }; let mut masked_matrix = AugmentedMatrix::new(p, target_masked_dim, [next_masked_dim, target_masked_dim]); @@ -630,7 +538,7 @@ impl> Resolution { masked_matrix.row_reduce(); let kernel = masked_matrix.compute_kernel(); - Self::write_qi( + Self::nassau_write_qi( &mut f, &mut scratch, &zero_sig, @@ -646,7 +554,8 @@ impl> Resolution { } // Compute image - let mut n = subalgebra.signature_matrix(&self.differentials[b.s()], b.t(), &zero_sig); + let d_s = self.differential(b.s()); + let mut n = subalgebra.signature_matrix(milnor, &d_s, b.t(), &zero_sig); n.row_reduce(); let next_row = n.rows(); @@ -656,7 +565,7 @@ impl> Resolution { assert_eq!(num_new_gens, 0, "Adding generators at {b}"); } - self.add_generators(b, num_new_gens); + self.nassau_add_generators(b, num_new_gens); let mut xs = vec![FpVector::new(p, target_dim); num_new_gens]; let mut dxs = vec![FpVector::new(p, next.dimension(b.t())); num_new_gens]; @@ -682,8 +591,8 @@ impl> Resolution { let _guard = tracing::info_span!("step", ?signature).entered(); target_mask.clear(); next_mask.clear(); - target_mask.extend(subalgebra.signature_mask(&algebra, target, b.t(), &signature)); - next_mask.extend(subalgebra.signature_mask(&algebra, next, b.t(), &signature)); + target_mask.extend(subalgebra.signature_mask(milnor, &target, b.t(), &signature)); + next_mask.extend(subalgebra.signature_mask(milnor, &next, b.t(), &signature)); let full_matrix = { let _guard = ParallelGuard::new(); @@ -720,7 +629,7 @@ impl> Resolution { dx.as_slice_mut().add(full_matrix.row(i), 1); } } - Self::write_qi( + Self::nassau_write_qi( &mut f, &mut scratch, &signature, @@ -740,20 +649,20 @@ impl> Resolution { f.write_u64::(Magic::End as u64)?; } - self.write_differential(b, num_new_gens, target_dim)?; + self.nassau_write_differential(b, num_new_gens, target_dim)?; Ok(()) } /// Step resolution for s = 0 #[tracing::instrument(skip(self))] - fn step0(&self, t: i32) { + fn nassau_step0(&self, t: i32) { self.zero_module.extend_by_zero(t); - let source_module = &self.modules[0]; - let target_module = self.target.module(0); + let source_module = self.module(0); + let target_module = self.target().module(0); - let chain_map = &self.chain_maps[0]; - let d = &self.differentials[0]; + let chain_map = self.chain_map(0); + let d = self.differential(0); let source_dim = source_module.dimension(t); let target_dim = target_module.dimension(t); @@ -782,7 +691,7 @@ impl> Resolution { let num_new_gens = matrix.extend_to_surjection(0, target_dim, 0).len(); - self.add_generators(Bidegree::s_t(0, t), num_new_gens); + self.nassau_add_generators(Bidegree::s_t(0, t), num_new_gens); chain_map.add_generators_from_matrix_rows( t, @@ -801,12 +710,12 @@ impl> Resolution { /// Step resolution for s = 1 #[tracing::instrument(skip(self))] - fn step1(&self, t: i32) -> anyhow::Result<()> { + fn nassau_step1(&self, t: i32) -> anyhow::Result<()> { let p = self.prime(); - let source_module = &self.modules[1]; - let target_module = &self.modules[0]; - let cc_module = self.target.module(0); + let source_module = self.module(1); + let target_module = self.module(0); + let cc_module = self.target().module(0); let source_dim = source_module.dimension(t); let target_dim = target_module.dimension(t); @@ -815,12 +724,14 @@ impl> Resolution { AugmentedMatrix::<2>::new(p, target_dim, [cc_module.dimension(t), target_dim]); { let _guard = ParallelGuard::new(); - self.chain_maps[0].get_matrix(matrix.segment(0, 0), t); + self.chain_map(0).get_matrix(matrix.segment(0, 0), t); } matrix.segment(1, 1).add_identity(); matrix.row_reduce(); let desired_image = matrix.compute_kernel(); + let d1 = self.differential(1); + let mut matrix = AugmentedMatrix::<2>::new_with_capacity( p, source_dim, @@ -830,31 +741,30 @@ impl> Resolution { ); { let _guard = ParallelGuard::new(); - self.differentials[1].get_matrix(matrix.segment(0, 0), t); + d1.get_matrix(matrix.segment(0, 0), t); } matrix.segment(1, 1).add_identity(); matrix.row_reduce(); let num_new_gens = matrix.extend_image(0, target_dim, &desired_image, 0).len(); - self.add_generators(Bidegree::s_t(1, t), num_new_gens); + self.nassau_add_generators(Bidegree::s_t(1, t), num_new_gens); - self.differentials[1].add_generators_from_matrix_rows( + d1.add_generators_from_matrix_rows( t, matrix .segment(0, 0) .row_slice(source_dim, source_dim + num_new_gens), ); - self.write_differential(Bidegree::s_t(1, t), num_new_gens, target_dim)?; + self.nassau_write_differential(Bidegree::s_t(1, t), num_new_gens, target_dim)?; Ok(()) } - fn step_resolution_with_result(&self, b: Bidegree) -> anyhow::Result<()> { - let p = self.prime(); + fn nassau_step_with_result(&self, b: Bidegree, milnor: &MilnorAlgebra) -> anyhow::Result<()> { let set_data = || { - let d = &self.differentials[b.s()]; - let c = &self.chain_maps[b.s()]; + let d = self.differential(b.s()); + let c = self.chain_map(b.s()); d.set_kernel(b.t(), None); d.set_image(b.t(), None); @@ -864,13 +774,14 @@ impl> Resolution { c.set_image(b.t(), None); c.set_quasi_inverse(b.t(), None); }; - self.modules[b.s()].compute_basis(b.t()); + + self.module(b.s()).compute_basis(b.t()); if b.s() > 0 { - self.modules[b.s() - 1].compute_basis(b.t()); + self.module(b.s() - 1).compute_basis(b.t()); } if b.s() == 0 { - self.step0(b.t()); + self.nassau_step0(b.t()); return Ok(()); } @@ -888,15 +799,20 @@ impl> Resolution { // want to resolve further, it will be bigger. let saved_target_res_dimension = f.read_u64::()? as usize; - self.add_generators(b, num_new_gens); + self.nassau_add_generators(b, num_new_gens); let mut d_targets = Vec::with_capacity(num_new_gens); for _ in 0..num_new_gens { - d_targets.push(FpVector::from_bytes(p, saved_target_res_dimension, &mut f)?); + d_targets.push(FpVector::from_bytes( + self.prime(), + saved_target_res_dimension, + &mut f, + )?); } - self.differentials[b.s()].add_generators_from_rows(b.t(), d_targets); + self.differential(b.s()) + .add_generators_from_rows(b.t(), d_targets); set_data(); @@ -904,167 +820,71 @@ impl> Resolution { } if b.s() == 1 { - self.step1(b.t())?; + self.nassau_step1(b.t())?; set_data(); return Ok(()); } - self.step_resolution_with_subalgebra( + self.nassau_step_with_subalgebra( b, - MilnorSubalgebra::optimal_for(b - Bidegree::s_t(0, self.max_degree)), + MilnorSubalgebra::optimal_for( + b - Bidegree::s_t( + 0, + self.target() + .module(0) + .max_degree() + .expect("checked before resolving"), + ), + ), + milnor, )?; - self.chain_maps[b.s()].extend_by_zero(b.t()); + self.chain_map(b.s()).extend_by_zero(b.t()); set_data(); Ok(()) } - fn step_resolution(&self, b: Bidegree) { - self.step_resolution_with_result(b) - .unwrap_or_else(|e| panic!("Error computing bidegree {b}: {e}")); - } - - /// This function resolves up till a fixed stem instead of a fixed t. - #[tracing::instrument(skip(self), fields(self = self.name, %max))] - pub fn compute_through_stem(&self, max: Bidegree) { - let _lock = self.lock.lock(); - - self.extend_through_degree(max.s()); - self.algebra().compute_basis(max.t()); - - let tracing_span = tracing::Span::current(); - maybe_rayon::in_place_scope(|scope| { - let _tracing_guard = tracing_span.enter(); - - // This algorithm is not optimal, as we compute (s, t) only after computing (s - 1, t) - // and (s, t - 1). In theory, it suffices to wait for (s, t - 1) and (s - 1, t - 1), - // but having the dimensions of the modules change halfway through the computation is - // annoying to do correctly. It seems more prudent to improve parallelism elsewhere. - - // Things that we have finished computing. - let mut progress: Vec = vec![-1; max.s() as usize + 1]; - // We will kickstart the process by pretending we have computed (0, - 1). So - // we must pretend we have only computed up to (0, - 2); - progress[0] = -2; - - let (sender, receiver) = mpsc::channel(); - SenderData::send(Bidegree::s_t(0, -1), sender); - - let f = |b, sender| { - if self.has_computed_bidegree(b) { - SenderData::send(b, sender); - } else { - let tracing_span = tracing_span.clone(); - scope.spawn(move |_| { - let _tracing_guard = tracing_span.enter(); - if crate::utils::parallel::is_in_parallel() { - SenderData::send_retry(b, sender); - return; - } - self.step_resolution(b); - SenderData::send(b, sender); - }); - } - }; - - while let Ok(SenderData { b, retry, sender }) = receiver.recv() { - if retry { - f(b, sender); - continue; - } - assert!(progress[b.s() as usize] == b.t() - 1); - progress[b.s() as usize] = b.t(); - - // How far we are from the last one for this s. - let distance = max.n() - b.n() + 1; - - if b.s() < max.s() && progress[b.s() as usize + 1] == b.t() - 1 { - f(b + Bidegree::s_t(1, 0), sender.clone()); - } - - if distance > 1 && (b.s() == 0 || progress[b.s() as usize - 1] > b.t()) { - // We are computing a normal step - f(b + Bidegree::s_t(0, 1), sender); - } else if distance == 1 && b.s() < max.s() { - SenderData::send(b + Bidegree::s_t(0, 1), sender); - } - } - }); - } -} - -impl> ChainComplex for Resolution { - type Algebra = MilnorAlgebra; - type Homomorphism = FreeModuleHomomorphism>; - type Module = FreeModule; - - fn prime(&self) -> ValidPrime { - TWO - } - - fn algebra(&self) -> Arc { - self.zero_module.algebra() - } - - fn module(&self, s: i32) -> Arc { - Arc::clone(&self.modules[s]) - } + /// Try to compute a Nassau step at the given bidegree. Returns `Ok(())` if Nassau was used, + /// `Err(())` if the conditions weren't met and classical should be used instead. + pub(crate) fn try_nassau_step(&self, b: Bidegree) -> Result<(), ()> { + let ctx = self.nassau_context().ok_or(())?; - fn zero_module(&self) -> Arc { - Arc::clone(&self.zero_module) - } - - fn min_degree(&self) -> i32 { - 0 - } - - fn has_computed_bidegree(&self, b: Bidegree) -> bool { - self.differentials.len() > b.s() && self.differential(b.s()).next_degree() > b.t() - } - - fn differential(&self, s: i32) -> Arc { - Arc::clone(&self.differentials[s]) - } - - #[tracing::instrument(skip(self), fields(self = self.name, %max))] - fn compute_through_bidegree(&self, max: Bidegree) { - let _lock = self.lock.lock(); - - self.extend_through_degree(max.s()); - self.algebra().compute_basis(max.t()); - - for t in 0..=max.t() { - for s in 0..=max.s() { - let b = Bidegree::s_t(s, t); - if self.has_computed_bidegree(b) { - continue; - } - self.step_resolution(b); + // Ensure nassau save dirs exist + if self.should_save + && let Some(p) = self.save_dir.write() + { + for subdir in SaveKind::nassau_data() { + subdir.create_dir(p).unwrap(); } } - } - - fn next_homological_degree(&self) -> i32 { - self.modules.len() - } - fn save_dir(&self) -> &SaveDirectory { - &self.save_dir + self.nassau_step_with_result(b, ctx.milnor()) + .unwrap_or_else(|e| panic!("Error computing bidegree {b} with Nassau: {e}")); + Ok(()) } - fn apply_quasi_inverse(&self, results: &mut [T], b: Bidegree, inputs: &[S]) -> bool + /// Try to apply quasi-inverse using Nassau's saved data. + pub(crate) fn nassau_apply_quasi_inverse( + &self, + results: &mut [T], + b: Bidegree, + inputs: &[S], + ) -> Option where for<'a> &'a mut T: Into>, for<'a> &'a S: Into>, { - let mut f = if let Some(dir) = self.save_dir.read() { - if let Some(f) = self.save_file(SaveKind::NassauQi, b).open_file(dir.clone()) { - f - } else { - return false; - } + let ctx = self.nassau_context()?; + let milnor = ctx.milnor(); + + let mut f = if let Some(dir) = self.save_dir.read() + && let Some(f) = self.save_file(SaveKind::NassauQi, b).open_file(dir.clone()) + { + f } else { - return false; + // No NassauQi file at this bidegree (e.g. at the boundary of the computed range). + // Return None to fall through to the classical qi path. + return None; }; let p = self.prime(); @@ -1072,15 +892,15 @@ impl> ChainComplex for Resolution { let target_dim = f.read_u64::().unwrap() as usize; let zero_mask_dim = f.read_u64::().unwrap() as usize; let subalgebra = MilnorSubalgebra::from_bytes(&mut f).unwrap(); - let source = &self.modules[b.s()]; - let target = &self.modules[b.s() - 1]; - let algebra = target.algebra(); + let source = self.module(b.s()); + let target = self.module(b.s() - 1); + let diff = self.differential(b.s()); let mut inputs: Vec = inputs.iter().map(|x| x.into().to_owned()).collect(); let mut mask: Vec = Vec::with_capacity(zero_mask_dim + 8); mask.extend(subalgebra.signature_mask( - &algebra, - source, + milnor, + &source, b.t(), &subalgebra.zero_signature(), )); @@ -1111,7 +931,7 @@ impl> ChainComplex for Resolution { assert_eq!(mask.len(), zero_mask_dim + num_new_gens); let target_zero_mask: Vec = subalgebra - .signature_mask(&algebra, target, b.t(), &subalgebra.zero_signature()) + .signature_mask(milnor, &target, b.t(), &subalgebra.zero_signature()) .collect(); let mut matrix = AugmentedMatrix::<3>::new( p, @@ -1120,7 +940,7 @@ impl> ChainComplex for Resolution { ); for i in 0..num_new_gens { - let dx = self.differentials[b.s()].output(b.t(), i); + let dx = diff.output(b.t(), i); matrix .row_segment_mut(i, 1, 1) .slice_mut(0, dx.len()) @@ -1143,7 +963,7 @@ impl> ChainComplex for Resolution { let signature = subalgebra.signature_from_bytes(&mut f).unwrap(); mask.clear(); - mask.extend(subalgebra.signature_mask(&algebra, source, b.t(), &signature)); + mask.extend(subalgebra.signature_mask(milnor, &source, b.t(), &signature)); scratch0.set_scratch_vector_size(mask.len()); } else if col == Magic::Fix as usize { // We need to fix the differential problem @@ -1220,49 +1040,14 @@ impl> ChainComplex for Resolution { target.element_to_string(b.t(), dx.as_slice()) ); } - true - } -} - -impl> AugmentedChainComplex for Resolution { - type ChainMap = FreeModuleHomomorphism; - type TargetComplex = FiniteChainComplex>; - - fn target(&self) -> Arc { - Arc::clone(&self.target) - } - - fn chain_map(&self, s: i32) -> Arc { - Arc::clone(&self.chain_maps[s]) + Some(true) } } #[cfg(test)] mod tests { - use expect_test::expect; - use super::*; - #[test] - fn test_restart_stem() { - let res = crate::utils::construct_nassau("S_2", None).unwrap(); - res.compute_through_stem(Bidegree::n_s(14, 8)); - res.compute_through_bidegree(Bidegree::s_t(5, 19)); - - expect![[r#" - · - · · - · · · · - · · · · - · · · · · - · · · · · · · - · · · · · · · · · - · · · · · - · - "#]] - .assert_eq(&res.graded_dimension_string()); - } - #[test] fn test_signature_iterator() { let subalgebra = MilnorSubalgebra::new(vec![2, 1]); diff --git a/ext/src/resolution.rs b/ext/src/resolution.rs index 1d138cafd4..925b6451b5 100644 --- a/ext/src/resolution.rs +++ b/ext/src/resolution.rs @@ -1,6 +1,9 @@ //! This module exports the [`Resolution`] object, which is a chain complex resolving a module. In //! particular, this contains the core logic that compute minimal resolutions. -use std::sync::{Arc, Mutex, mpsc}; +use std::{ + any::Any, + sync::{Arc, Mutex, mpsc}, +}; use algebra::{ Algebra, MuAlgebra, @@ -81,19 +84,20 @@ pub struct MuResolution where CC::Algebra: MuAlgebra, { - name: String, - lock: Mutex<()>, - complex: Arc, - modules: OnceBiVec>>, - zero_module: Arc>, - chain_maps: OnceBiVec>>, - differentials: OnceVec>>>, + pub(crate) name: String, + pub(crate) lock: Mutex<()>, + pub(crate) complex: Arc, + pub(crate) modules: OnceBiVec>>, + pub(crate) zero_module: Arc>, + pub(crate) chain_maps: OnceBiVec>>, + pub(crate) differentials: + OnceVec>>>, /// For each *internal* degree, store the kernel of the most recently calculated chain map as /// returned by `generate_old_kernel_and_compute_new_kernel`, to be used if we run /// compute_through_degree again. - kernels: DashMap, - save_dir: SaveDirectory, + pub(crate) kernels: DashMap, + pub(crate) save_dir: SaveDirectory, /// Whether we should save newly computed data to the disk. This has no effect if there is no /// save file. Defaults to `self.save_dir.is_some()`. @@ -346,8 +350,19 @@ where /// To run `step_resolution(s, t)`, we must have already had run `step_resolution(s, t - 1)` /// and `step_resolution(s - 1, t - 1)`. It is more efficient if we have in fact run /// `step_resolution(s - 1, t)`, so try your best to arrange calls to be run in this order. - #[tracing::instrument(skip(self), fields(%b, num_new_gens, density))] fn step_resolution(&self, b: Bidegree) { + use crate::CCC; + if let Some(res) = (self as &dyn Any).downcast_ref::>() + && res.try_nassau_step(b).is_ok() + { + return; + } + + self.step_resolution_classical(b); + } + + #[tracing::instrument(skip(self), fields(%b, num_new_gens, density))] + fn step_resolution_classical(&self, b: Bidegree) { if b.s() == 0 { self.zero_module.extend_by_zero(b.t()); } @@ -813,6 +828,12 @@ where self.extend_through_degree(max.s()); self.algebra().compute_basis(max.t() - min_degree); + // Check once whether Nassau is active. If so, skip kernel precomputation at the stem + // boundary, since Nassau doesn't use kernels. + let is_nassau = (self as &dyn Any) + .downcast_ref::>() + .is_some_and(|r| r.nassau_context().is_some()); + let tracing_span = tracing::Span::current(); maybe_rayon::in_place_scope(|scope| { let _tracing_guard = tracing_span.enter(); @@ -868,20 +889,25 @@ where // We are computing a normal step f(b + Bidegree::s_t(0, 1), sender); } else if distance == 1 && b.s() < max.s() { - // We compute the kernel at the edge if necessary - let next_b = b + Bidegree::s_t(0, 1); - if !self.has_computed_bidegree(b + Bidegree::s_t(1, 1)) - && (self.save_dir.is_none() - || !self - .save_file(SaveKind::Differential, b + Bidegree::s_t(1, 1)) - .exists(self.save_dir.read().cloned().unwrap())) - { - scope.spawn(move |_| { - self.kernels.insert(next_b, self.get_kernel(next_b)); - SenderData::send(next_b, false, sender); - }); + if is_nassau { + // Nassau doesn't use kernels; just mark this bidegree as done. + SenderData::send(b + Bidegree::s_t(0, 1), false, sender); } else { - SenderData::send(next_b, false, sender); + // We compute the kernel at the edge if necessary + let next_b = b + Bidegree::s_t(0, 1); + if !self.has_computed_bidegree(b + Bidegree::s_t(1, 1)) + && (self.save_dir.is_none() + || !self + .save_file(SaveKind::Differential, b + Bidegree::s_t(1, 1)) + .exists(self.save_dir.read().cloned().unwrap())) + { + scope.spawn(move |_| { + self.kernels.insert(next_b, self.get_kernel(next_b)); + SenderData::send(next_b, false, sender); + }); + } else { + SenderData::send(next_b, false, sender); + } } } if new { @@ -940,6 +966,13 @@ where { assert_eq!(results.len(), inputs.len()); + // Try Nassau's quasi-inverse first + if let Some(res) = (self as &dyn Any).downcast_ref::>() + && let Some(result) = res.nassau_apply_quasi_inverse(results, b, inputs) + { + return result; + } + if let Some(qi) = self.differential(b.s()).quasi_inverse(b.t()) { for (input, result) in inputs.iter().zip_eq(results) { qi.apply(result.into(), 1, input.into()); @@ -1014,9 +1047,11 @@ mod tests { res.load_quasi_inverse = false; let b = Bidegree::s_t(8, 8); - res.compute_through_bidegree(b); + // Compute one extra homological degree: when Nassau is active, qi data for (s, t) + // is written during the computation at (s+1, t). + res.compute_through_bidegree(Bidegree::s_t(b.s() + 1, b.t())); - assert!(res.differential(8).quasi_inverse(8).is_none()); + assert!(res.differential(b.s()).quasi_inverse(b.t()).is_none()); let v = FpVector::new(res.prime(), res.module(7).dimension(8)); let mut w = FpVector::new(res.prime(), res.module(8).dimension(8)); diff --git a/ext/src/utils.rs b/ext/src/utils.rs index acfb352a49..88f58aab06 100644 --- a/ext/src/utils.rs +++ b/ext/src/utils.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, sync::Arc}; use algebra::{ - AlgebraType, MilnorAlgebra, SteenrodAlgebra, + AlgebraType, SteenrodAlgebra, module::{FDModule, Module, SteenrodModule, steenrod_module}, }; use anyhow::{Context, anyhow}; @@ -16,17 +16,9 @@ use crate::{ save::SaveDirectory, }; -// We build docs with --all-features so the docs are at the feature = "nassau" version -#[cfg(not(feature = "nassau"))] +/// The resolution type returned by [`query_module`] and [`construct`]. pub type QueryModuleResolution = Resolution; -/// The type returned by [`query_module`]. The value of this type depends on whether -/// [`nassau`](crate::nassau) is enabled. In any case, it is an augmented free chain complex over -/// either [`SteenrodAlgebra`] or [`MilnorAlgebra`] and supports the `compute_through_stem` -/// function. -#[cfg(feature = "nassau")] -pub type QueryModuleResolution = crate::nassau::Resolution>; - const STATIC_MODULES_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../ext/steenrod_modules"); /// A config object is an object that specifies how a Steenrod module should be constructed. @@ -133,8 +125,7 @@ impl> TryFrom<(Value, T)> for Config { /// - `save_file`: The save file for the module. If it points to an invalid save file, an error is /// returned. /// -/// This dispatches to either [`construct_nassau`] or [`construct_standard`] depending on whether -/// the `nassau` feature is enabled. +/// See [`construct_standard`] for what this accepts. pub fn construct( module_spec: T, save_dir: impl Into, @@ -143,55 +134,7 @@ where anyhow::Error: From, T: TryInto, { - #[cfg(feature = "nassau")] - { - construct_nassau(module_spec, save_dir) - } - - #[cfg(not(feature = "nassau"))] - { - construct_standard(module_spec, save_dir) - } -} - -/// See [`construct`] -pub fn construct_nassau( - module_spec: T, - save_dir: impl Into, -) -> anyhow::Result>> -where - anyhow::Error: From, - T: TryInto, -{ - let Config { - module: json, - algebra, - } = module_spec.try_into()?; - - if algebra == AlgebraType::Adem { - return Err(anyhow!("Nassau's algorithm requires Milnor's basis")); - } - if !json["profile"].is_null() { - return Err(anyhow!( - "Nassau's algorithm does not support non-trivial profile" - )); - } - if json["p"].as_i64() != Some(2) { - return Err(anyhow!("Nassau's algorithm does not support odd primes")); - } - if json["type"].as_str() != Some("finite dimensional module") { - return Err(anyhow!( - "Nassau's algorithm only supports finite dimensional modules" - )); - } - - let algebra = Arc::new(MilnorAlgebra::new(fp::prime::TWO, false)); - let module = Arc::new(FDModule::from_json(Arc::clone(&algebra), &json)?); - - if !json["cofiber"].is_null() { - return Err(anyhow!("Nassau's algorithm does not support cofiber")); - } - crate::nassau::Resolution::new_with_save(module, save_dir) + construct_standard(module_spec, save_dir) } /// See [`construct`] @@ -353,18 +296,7 @@ pub fn query_module_only( construct(module, save_dir).context("Failed to load module from save file")?; let load_quasi_inverse = load_quasi_inverse && resolution.save_dir().is_none(); - - #[cfg(not(feature = "nassau"))] - { - resolution.load_quasi_inverse = load_quasi_inverse; - } - - #[cfg(feature = "nassau")] - assert!( - !load_quasi_inverse, - "Quasi inverse loading not support with Nassau. Please use a save directory instead" - ); - + resolution.load_quasi_inverse = load_quasi_inverse; resolution.set_name(name); Ok(resolution) @@ -447,19 +379,8 @@ pub fn get_unit( bivec::BiVec::from_vec(0, vec![1]), ); - #[cfg(feature = "nassau")] - { - Arc::new(crate::nassau::Resolution::new_with_save( - Arc::new(module), - save_dir, - )?) - } - - #[cfg(not(feature = "nassau"))] - { - let cc = FiniteChainComplex::ccdz(Arc::new(steenrod_module::erase(module))); - Arc::new(Resolution::new_with_save(Arc::new(cc), save_dir)?) - } + let cc = FiniteChainComplex::ccdz(Arc::new(steenrod_module::erase(module))); + Arc::new(Resolution::new_with_save(Arc::new(cc), save_dir)?) }; Ok((is_unit, unit)) diff --git a/ext/tests/milnor_vs_nassau.rs b/ext/tests/milnor_vs_nassau.rs index 2e6e18b81c..e0ff34f008 100644 --- a/ext/tests/milnor_vs_nassau.rs +++ b/ext/tests/milnor_vs_nassau.rs @@ -1,10 +1,14 @@ use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - utils::{construct_nassau, construct_standard}, + utils::construct_standard, }; use rstest::rstest; use sseq::coordinates::Bidegree; +/// Compare a resolution with Nassau (via save directory) against one without. +/// +/// When a save directory is provided and the module is eligible, Nassau's algorithm +/// is used automatically at runtime. #[rstest] #[trace] #[case("S_2", 30)] @@ -13,11 +17,20 @@ use sseq::coordinates::Bidegree; #[case("Csigma", 30)] fn compare(#[case] module_name: &str, #[case] max_degree: i32) { let max = Bidegree::s_t(max_degree, max_degree); - let a = construct_standard::(module_name, None).unwrap(); - let b = construct_nassau(module_name, None).unwrap(); - a.compute_through_bidegree(max); - b.compute_through_bidegree(max); + // Without save dir: classical algorithm + let classical = construct_standard::(module_name, None).unwrap(); - assert_eq!(a.graded_dimension_string(), b.graded_dimension_string()); + // With save dir: Nassau's algorithm will be used if eligible + let save_dir = tempfile::tempdir().unwrap(); + let nassau = + construct_standard::(module_name, Some(save_dir.path().to_owned())).unwrap(); + + classical.compute_through_bidegree(max); + nassau.compute_through_bidegree(max); + + assert_eq!( + classical.graded_dimension_string(), + nassau.graded_dimension_string() + ); } diff --git a/ext/tests/save_load_resolution.rs b/ext/tests/save_load_resolution.rs index 556161514f..acf461e9fe 100644 --- a/ext/tests/save_load_resolution.rs +++ b/ext/tests/save_load_resolution.rs @@ -20,9 +20,11 @@ fn set_readonly(p: &Path, readonly: bool) { fn lock_tempdir(dir: &Path) { let mut dir: PathBuf = dir.into(); - for kind in SaveKind::resolution_data() { + for kind in SaveKind::resolution_data().chain(SaveKind::nassau_data()) { dir.push(format!("{}s", kind.name())); - set_readonly(&dir, true); + if dir.exists() { + set_readonly(&dir, true); + } dir.pop(); } set_readonly(&dir, true); @@ -33,9 +35,11 @@ fn unlock_tempdir(dir: &Path) { set_readonly(dir, false); let mut dir: PathBuf = dir.into(); - for kind in SaveKind::resolution_data() { + for kind in SaveKind::resolution_data().chain(SaveKind::nassau_data()) { dir.push(format!("{}s", kind.name())); - set_readonly(&dir, false); + if dir.exists() { + set_readonly(&dir, false); + } dir.pop(); } } @@ -100,15 +104,17 @@ fn test_save_load() { } #[test] -#[should_panic(expected = "Invalid header: algebra was 0x20000 but expected 0x28000")] +#[should_panic(expected = "Invalid header: algebra was 0x30000 but expected 0x38000")] fn wrong_algebra() { let tempdir = tempfile::TempDir::new().unwrap(); + + // We use p = 3 to disable Nassau's algorithm let resolution1 = - construct_standard::("S_2@adem", Some(tempdir.path().into())).unwrap(); + construct_standard::("S_3@adem", Some(tempdir.path().into())).unwrap(); resolution1.compute_through_bidegree(Bidegree::s_t(2, 2)); let resolution2 = - construct_standard::("S_2@milnor", Some(tempdir.path().into())).unwrap(); + construct_standard::("S_3@milnor", Some(tempdir.path().into())).unwrap(); resolution2.compute_through_bidegree(Bidegree::s_t(2, 2)); } @@ -289,7 +295,7 @@ fn test_checksum() { .compute_through_bidegree(Bidegree::s_t(2, 2)); let mut path = tempdir.path().to_owned(); - path.push("differentials/2_2_differential"); + path.push("nassau_differentials/2_2_nassau_differential"); let mut file = OpenOptions::new() .read(true) From edb3611ee681d23d7c2665611a5bf0c4541f77d1 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer Date: Sat, 4 Apr 2026 20:22:10 -0400 Subject: [PATCH 3/3] WIP unify-resolution-dispatch --- ext/examples/mahowald_invariant.rs | 4 +- ext/src/lib.rs | 1 - .../classical.rs} | 169 ++---- ext/src/resolution/mod.rs | 150 +++++ ext/src/{ => resolution}/nassau.rs | 545 ++++++++++++------ ext/src/utils.rs | 81 ++- ext/tests/milnor_vs_adem.rs | 10 +- ext/tests/milnor_vs_nassau.rs | 7 +- ext/tests/module_construct_error.rs | 4 +- 9 files changed, 663 insertions(+), 308 deletions(-) rename ext/src/{resolution.rs => resolution/classical.rs} (87%) create mode 100644 ext/src/resolution/mod.rs rename ext/src/{ => resolution}/nassau.rs (68%) diff --git a/ext/examples/mahowald_invariant.rs b/ext/examples/mahowald_invariant.rs index 0afe97f056..40ae448045 100644 --- a/ext/examples/mahowald_invariant.rs +++ b/ext/examples/mahowald_invariant.rs @@ -109,7 +109,7 @@ struct MahowaldInvariant { } fn resolve_s_2(s_2_path: Option, k_max: i32) -> Result> { - let s_2_resolution = Arc::new(utils::construct_standard("S_2", s_2_path)?); + let s_2_resolution = Arc::new(utils::construct("S_2", s_2_path)?); // Here are some bounds on the bidegrees in which we have should have resolutions available. // // A class in stem n won't be detected before RP_-{n+1}_inf, so we can only detect Mahowald @@ -142,7 +142,7 @@ impl PKData { if let Some(p) = p_k_path.as_mut() { p.push(PathBuf::from(&format!("RP_{minus_k}_inf", minus_k = -k))); }; - let resolution = Arc::new(utils::construct_standard( + let resolution = Arc::new(utils::construct( (p_k_config, AlgebraType::Milnor), p_k_path, )?); diff --git a/ext/src/lib.rs b/ext/src/lib.rs index bef4a91310..61886dcf60 100644 --- a/ext/src/lib.rs +++ b/ext/src/lib.rs @@ -176,7 +176,6 @@ use algebra::module::SteenrodModule; use crate::chain_complex::FiniteChainComplex; pub type CCC = FiniteChainComplex; -pub mod nassau; pub mod secondary; pub mod utils; diff --git a/ext/src/resolution.rs b/ext/src/resolution/classical.rs similarity index 87% rename from ext/src/resolution.rs rename to ext/src/resolution/classical.rs index 925b6451b5..d42b1de64d 100644 --- a/ext/src/resolution.rs +++ b/ext/src/resolution/classical.rs @@ -1,9 +1,4 @@ -//! This module exports the [`Resolution`] object, which is a chain complex resolving a module. In -//! particular, this contains the core logic that compute minimal resolutions. -use std::{ - any::Any, - sync::{Arc, Mutex, mpsc}, -}; +use std::sync::{Arc, Mutex, mpsc}; use algebra::{ Algebra, MuAlgebra, @@ -29,7 +24,7 @@ use crate::{ utils::parallel::ParallelGuard, }; -/// In [`MuResolution::compute_through_stem`] and [`MuResolution::compute_through_bidegree`], we pass +/// In [`MuClassicalResolution::compute_through_stem`] and [`MuClassicalResolution::compute_through_bidegree`], we pass /// this struct around to inform the supervisor what bidegrees have been computed. We use an /// explicit struct instead of a tuple to avoid an infinite type problem. struct SenderData { @@ -75,29 +70,25 @@ impl SenderData { /// number if needs be, but up to the 140th stem we only see at most 8 new generators. const MAX_NEW_GENS: usize = 10; -pub type Resolution = MuResolution; -pub type UnstableResolution = MuResolution; - -/// A minimal resolution of a chain complex. The functions [`MuResolution::compute_through_stem`] and -/// [`MuResolution::compute_through_bidegree`] extends the minimal resolution to the given bidegree. -pub struct MuResolution +/// A minimal resolution of a chain complex. The functions [`MuClassicalResolution::compute_through_stem`] and +/// [`MuClassicalResolution::compute_through_bidegree`] extends the minimal resolution to the given bidegree. +pub struct MuClassicalResolution where CC::Algebra: MuAlgebra, { - pub(crate) name: String, - pub(crate) lock: Mutex<()>, - pub(crate) complex: Arc, - pub(crate) modules: OnceBiVec>>, - pub(crate) zero_module: Arc>, - pub(crate) chain_maps: OnceBiVec>>, - pub(crate) differentials: - OnceVec>>>, + name: String, + lock: Mutex<()>, + complex: Arc, + modules: OnceBiVec>>, + zero_module: Arc>, + chain_maps: OnceBiVec>>, + differentials: OnceVec>>>, /// For each *internal* degree, store the kernel of the most recently calculated chain map as /// returned by `generate_old_kernel_and_compute_new_kernel`, to be used if we run /// compute_through_degree again. - pub(crate) kernels: DashMap, - pub(crate) save_dir: SaveDirectory, + kernels: DashMap, + save_dir: SaveDirectory, /// Whether we should save newly computed data to the disk. This has no effect if there is no /// save file. Defaults to `self.save_dir.is_some()`. @@ -116,15 +107,10 @@ where pub load_quasi_inverse: bool, } -impl MuResolution +impl MuClassicalResolution where CC::Algebra: MuAlgebra, { - pub fn new(complex: Arc) -> Self { - // It doesn't error if the save file is None - Self::new_with_save(complex, None).unwrap() - } - pub fn new_with_save( complex: Arc, save_dir: impl Into, @@ -350,19 +336,8 @@ where /// To run `step_resolution(s, t)`, we must have already had run `step_resolution(s, t - 1)` /// and `step_resolution(s - 1, t - 1)`. It is more efficient if we have in fact run /// `step_resolution(s - 1, t)`, so try your best to arrange calls to be run in this order. - fn step_resolution(&self, b: Bidegree) { - use crate::CCC; - if let Some(res) = (self as &dyn Any).downcast_ref::>() - && res.try_nassau_step(b).is_ok() - { - return; - } - - self.step_resolution_classical(b); - } - #[tracing::instrument(skip(self), fields(%b, num_new_gens, density))] - fn step_resolution_classical(&self, b: Bidegree) { + fn step_resolution(&self, b: Bidegree) { if b.s() == 0 { self.zero_module.extend_by_zero(b.t()); } @@ -828,12 +803,6 @@ where self.extend_through_degree(max.s()); self.algebra().compute_basis(max.t() - min_degree); - // Check once whether Nassau is active. If so, skip kernel precomputation at the stem - // boundary, since Nassau doesn't use kernels. - let is_nassau = (self as &dyn Any) - .downcast_ref::>() - .is_some_and(|r| r.nassau_context().is_some()); - let tracing_span = tracing::Span::current(); maybe_rayon::in_place_scope(|scope| { let _tracing_guard = tracing_span.enter(); @@ -889,25 +858,20 @@ where // We are computing a normal step f(b + Bidegree::s_t(0, 1), sender); } else if distance == 1 && b.s() < max.s() { - if is_nassau { - // Nassau doesn't use kernels; just mark this bidegree as done. - SenderData::send(b + Bidegree::s_t(0, 1), false, sender); - } else { - // We compute the kernel at the edge if necessary - let next_b = b + Bidegree::s_t(0, 1); - if !self.has_computed_bidegree(b + Bidegree::s_t(1, 1)) - && (self.save_dir.is_none() - || !self - .save_file(SaveKind::Differential, b + Bidegree::s_t(1, 1)) - .exists(self.save_dir.read().cloned().unwrap())) - { - scope.spawn(move |_| { - self.kernels.insert(next_b, self.get_kernel(next_b)); - SenderData::send(next_b, false, sender); - }); - } else { + // We compute the kernel at the edge if necessary + let next_b = b + Bidegree::s_t(0, 1); + if !self.has_computed_bidegree(b + Bidegree::s_t(1, 1)) + && (self.save_dir.is_none() + || !self + .save_file(SaveKind::Differential, b + Bidegree::s_t(1, 1)) + .exists(self.save_dir.read().cloned().unwrap())) + { + scope.spawn(move |_| { + self.kernels.insert(next_b, self.get_kernel(next_b)); SenderData::send(next_b, false, sender); - } + }); + } else { + SenderData::send(next_b, false, sender); } } if new { @@ -918,7 +882,7 @@ where } } -impl ChainComplex for MuResolution +impl ChainComplex for MuClassicalResolution where CC::Algebra: MuAlgebra, { @@ -966,13 +930,6 @@ where { assert_eq!(results.len(), inputs.len()); - // Try Nassau's quasi-inverse first - if let Some(res) = (self as &dyn Any).downcast_ref::>() - && let Some(result) = res.nassau_apply_quasi_inverse(results, b, inputs) - { - return result; - } - if let Some(qi) = self.differential(b.s()).quasi_inverse(b.t()) { for (input, result) in inputs.iter().zip_eq(results) { qi.apply(result.into(), 1, input.into()); @@ -995,7 +952,7 @@ where } } -impl AugmentedChainComplex for MuResolution +impl AugmentedChainComplex for MuClassicalResolution where CC::Algebra: MuAlgebra, { @@ -1013,50 +970,50 @@ where #[cfg(test)] mod tests { - use expect_test::expect; + // use expect_test::expect; - use super::*; - use crate::{chain_complex::FreeChainComplex, utils::construct_standard}; + // use super::*; + // use crate::{chain_complex::FreeChainComplex, utils::construct_standard}; #[test] fn test_restart_stem() { - let res = construct_standard::("S_2", None).unwrap(); - res.compute_through_stem(Bidegree::n_s(14, 8)); - res.compute_through_bidegree(Bidegree::s_t(5, 19)); - - expect![[r#" - · - · · - · · · · - · · · · - · · · · · - · · · · · · · - · · · · · · · · · - · · · · · - · - "#]] - .assert_eq(&res.graded_dimension_string()); + todo!() + // let res = construct_standard::("S_2", None).unwrap(); + // res.compute_through_stem(Bidegree::n_s(14, 8)); + // res.compute_through_bidegree(Bidegree::s_t(5, 19)); + + // expect![[r#" + // · + // · · + // · · · · + // · · · · + // · · · · · + // · · · · · · · + // · · · · · · · · · + // · · · · · + // · + // "#]] + // .assert_eq(&res.graded_dimension_string()); } #[test] fn test_apply_quasi_inverse() { - let tempdir = tempfile::TempDir::new().unwrap(); + todo!() + // let tempdir = tempfile::TempDir::new().unwrap(); - let mut res = - construct_standard::("S_2", Some(tempdir.path().into())).unwrap(); - res.load_quasi_inverse = false; + // let mut res = + // construct_standard::("S_2", Some(tempdir.path().into())).unwrap(); + // res.load_quasi_inverse = false; - let b = Bidegree::s_t(8, 8); - // Compute one extra homological degree: when Nassau is active, qi data for (s, t) - // is written during the computation at (s+1, t). - res.compute_through_bidegree(Bidegree::s_t(b.s() + 1, b.t())); + // let b = Bidegree::s_t(8, 8); + // res.compute_through_bidegree(b); - assert!(res.differential(b.s()).quasi_inverse(b.t()).is_none()); + // assert!(res.differential(8).quasi_inverse(8).is_none()); - let v = FpVector::new(res.prime(), res.module(7).dimension(8)); - let mut w = FpVector::new(res.prime(), res.module(8).dimension(8)); + // let v = FpVector::new(res.prime(), res.module(7).dimension(8)); + // let mut w = FpVector::new(res.prime(), res.module(8).dimension(8)); - assert!(res.apply_quasi_inverse(&mut [w.as_slice_mut()], b, &[v.as_slice()])); - assert!(w.is_zero()); + // assert!(res.apply_quasi_inverse(&mut [w.as_slice_mut()], b, &[v.as_slice()])); + // assert!(w.is_zero()); } } diff --git a/ext/src/resolution/mod.rs b/ext/src/resolution/mod.rs new file mode 100644 index 0000000000..d6aea6cb3d --- /dev/null +++ b/ext/src/resolution/mod.rs @@ -0,0 +1,150 @@ +//! This module exports the [`Resolution`] object, which is a chain complex resolving a module. In +//! particular, this contains the core logic that compute minimal resolutions. + +use std::sync::Arc; + +use algebra::{ + MilnorAlgebra, MuAlgebra, SteenrodAlgebra, + module::{FDModule, MuFreeModule, homomorphism::MuFreeModuleHomomorphism}, +}; +use sseq::coordinates::Bidegree; + +use crate::{ + chain_complex::{AugmentedChainComplex, ChainComplex}, + save::SaveDirectory, +}; + +mod classical; +mod nassau; + +use classical::MuClassicalResolution; +use nassau::NassauResolution; + +pub type Resolution = MuResolution; +pub type UnstableResolution = MuResolution; + +pub enum MuResolution +where + CC::Algebra: MuAlgebra, +{ + Classical(MuClassicalResolution), + Nassau(NassauResolution), +} + +impl MuResolution +where + CC::Algebra: MuAlgebra, +{ + pub fn new(complex: Arc) -> Self { + // It doesn't error if the save file is None + Self::new_with_save(complex, None).unwrap() + } + + pub fn new_with_save( + complex: Arc, + save_dir: impl Into, + ) -> anyhow::Result { + Ok(match Self::try_into_nassau(complex) { + Ok(module) => Self::Nassau(NassauResolution::new_with_save(module, save_dir)?), + Err(complex) => { + Self::Classical(MuClassicalResolution::new_with_save(complex, save_dir)?) + } + }) + } + + fn try_into_nassau(complex: Arc) -> Result>, Arc> { + todo!() + } + + pub fn name(&self) -> &str { + match self { + Self::Classical(classical) => classical.name(), + Self::Nassau(nassau) => nassau.name(), + } + } + + pub fn compute_through_stem(&self, max: Bidegree) { + match self { + Self::Classical(classical) => classical.compute_through_stem(max), + Self::Nassau(nassau) => nassau.compute_through_stem(max), + } + } + + pub fn set_load_quasi_inverse(&mut self, load_quasi_inverse: bool) { + match self { + Self::Classical(classical) => classical.load_quasi_inverse = load_quasi_inverse, + Self::Nassau(nassau) => assert!( + !load_quasi_inverse || nassau.save_dir().is_some(), + "Quasi inverse loading not supported with Nassau. Please use a save directory \ + instead" + ), + } + } + + pub fn set_name(&mut self, name: String) { + match self { + Self::Classical(classical) => classical.set_name(name), + Self::Nassau(nassau) => nassau.set_name(name), + } + } +} + +impl ChainComplex for MuResolution +where + CC::Algebra: MuAlgebra, +{ + type Algebra = CC::Algebra; + type Homomorphism = MuFreeModuleHomomorphism>; + type Module = MuFreeModule; + + fn algebra(&self) -> Arc { + match self { + Self::Classical(classical) => classical.algebra(), + Self::Nassau(nassau) => todo!(), + } + } + + fn min_degree(&self) -> i32 { + todo!() + } + + fn zero_module(&self) -> Arc { + todo!() + } + + fn module(&self, homological_degree: i32) -> Arc { + todo!() + } + + fn differential(&self, s: i32) -> Arc { + todo!() + } + + fn has_computed_bidegree(&self, b: sseq::coordinates::Bidegree) -> bool { + todo!() + } + + fn compute_through_bidegree(&self, b: sseq::coordinates::Bidegree) { + todo!() + } + + fn next_homological_degree(&self) -> i32 { + todo!() + } +} + +impl AugmentedChainComplex for MuResolution +where + CC::Algebra: MuAlgebra, +{ + type ChainMap = as AugmentedChainComplex>::ChainMap; + type TargetComplex = as AugmentedChainComplex>::TargetComplex; + + fn target(&self) -> Arc { + todo!() + } + + fn chain_map(&self, s: i32) -> Arc { + todo!() + } +} diff --git a/ext/src/nassau.rs b/ext/src/resolution/nassau.rs similarity index 68% rename from ext/src/nassau.rs rename to ext/src/resolution/nassau.rs index 858c037c7d..a9b8d8064c 100644 --- a/ext/src/nassau.rs +++ b/ext/src/resolution/nassau.rs @@ -1,36 +1,70 @@ //! This module implements [Nassau's algorithm](https://arxiv.org/abs/1910.04063). //! -//! Nassau's algorithm is a specialized algorithm for computing minimal resolutions at the prime 2 -//! using the Milnor basis. It is implemented as methods on -//! [`Resolution`](crate::resolution::Resolution) that are dispatched to at runtime when -//! the conditions are met (prime 2, Milnor algebra, bounded module, save directory present). - -use std::{fmt::Display, io}; +//! The main export is the [`Resolution`] object, which is a resolution at the prime 2 using +//! Nassau's algorithm. It aims to provide an API similar to +//! [`resolution::Resolution`](crate::resolution::Resolution). From an API point of view, the main +//! difference between the two is that our `Resolution` is a chain complex over [`MilnorAlgebra`] +//! over [`SteenrodAlgebra`](algebra::SteenrodAlgebra). + +use std::{ + fmt::Display, + io, + sync::{Arc, Mutex, mpsc}, +}; use algebra::{ - combinatorics, + Algebra, combinatorics, milnor_algebra::{MilnorAlgebra, PPartEntry}, module::{ - FreeModule, GeneratorData, Module, - homomorphism::{FreeModuleHomomorphism, ModuleHomomorphism}, + FDModule, FreeModule, GeneratorData, Module, + homomorphism::{FreeModuleHomomorphism, FullModuleHomomorphism, ModuleHomomorphism}, }, }; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use fp::{ matrix::{AugmentedMatrix, Matrix}, - prime::TWO, + prime::{TWO, ValidPrime}, vector::{FpSlice, FpSliceMut, FpVector}, }; use itertools::Itertools; +use once::OnceBiVec; use sseq::coordinates::{Bidegree, BidegreeGenerator}; use crate::{ - chain_complex::{AugmentedChainComplex, ChainComplex, FreeChainComplex}, - resolution::Resolution, - save::SaveKind, + chain_complex::{AugmentedChainComplex, ChainComplex, FiniteChainComplex, FreeChainComplex}, + save::{SaveDirectory, SaveKind}, utils::{LogWriter, parallel::ParallelGuard}, }; +/// See [`resolution::SenderData`](../resolution/struct.SenderData.html). This differs by not having the `new` field. +struct SenderData { + b: Bidegree, + retry: bool, + sender: mpsc::Sender, +} + +impl SenderData { + pub(crate) fn send(b: Bidegree, sender: mpsc::Sender) { + sender + .send(Self { + b, + retry: false, + sender: sender.clone(), + }) + .unwrap() + } + + pub(crate) fn send_retry(b: Bidegree, sender: mpsc::Sender) { + sender + .send(Self { + b, + retry: true, + sender: sender.clone(), + }) + .unwrap() + } +} + const MAX_NEW_GENS: usize = 10; /// A Milnor subalgebra to be used in [Nassau's algorithm](https://arxiv.org/abs/1910.04063). This @@ -75,13 +109,11 @@ impl MilnorSubalgebra { /// Give a list of basis elements in degree `degree` that has signature `signature`. /// - /// The `milnor` parameter provides access to the P-part table. The `module` can be over any - /// algebra type as long as it provides generator offset data (the offsets are identical when - /// the algebra is actually Milnor). - fn signature_mask<'a, A: algebra::Algebra>( + /// This requires passing the algebra for borrow checker reasons. + fn signature_mask<'a>( &'a self, - milnor: &'a MilnorAlgebra, - module: &'a FreeModule, + algebra: &'a MilnorAlgebra, + module: &'a FreeModule, degree: i32, signature: &'a [PPartEntry], ) -> impl Iterator + 'a { @@ -91,7 +123,7 @@ impl MilnorSubalgebra { start: [offset], end: _, }| { - milnor + algebra .ppart_table(degree - gen_deg) .iter() .enumerate() @@ -108,26 +140,24 @@ impl MilnorSubalgebra { /// Get the matrix of a free module homomorphism when restricted to the subquotient given by /// the signature. - /// - /// The `milnor` parameter provides access to the P-part table. - fn signature_matrix( + fn signature_matrix( &self, - milnor: &MilnorAlgebra, - hom: &FreeModuleHomomorphism>, + hom: &FreeModuleHomomorphism>, degree: i32, signature: &[PPartEntry], ) -> Matrix { let p = hom.prime(); let source = hom.source(); let target = hom.target(); + let algebra = target.algebra(); let target_degree = degree - hom.degree_shift(); let target_mask: Vec = self - .signature_mask(milnor, &target, degree - hom.degree_shift(), signature) + .signature_mask(&algebra, &target, degree - hom.degree_shift(), signature) .collect(); let source_mask: Vec = self - .signature_mask(milnor, &source, degree, signature) + .signature_mask(&algebra, &source, degree, signature) .collect(); let mut scratch = FpVector::new(p, target.dimension(target_degree)); @@ -350,47 +380,62 @@ enum Magic { Fix = -3, } -/// Context for Nassau's algorithm, extracted once per step. -/// -/// Holds onto the `Arc` so that the `&MilnorAlgebra` reference remains valid. -pub(crate) struct NassauContext { - algebra: std::sync::Arc, +type FDMM = FDModule; + +/// A resolution of a finite-dimensional module using Nassau's algorithm +pub struct NassauResolution { + lock: Mutex<()>, + name: String, + max_degree: i32, + modules: OnceBiVec>>, + zero_module: Arc>, + differentials: OnceBiVec>>>, + target: Arc>, + chain_maps: OnceBiVec>>, + save_dir: SaveDirectory, } -impl NassauContext { - /// Get the inner [`MilnorAlgebra`]. - pub fn milnor(&self) -> &MilnorAlgebra { - match &*self.algebra { - algebra::SteenrodAlgebra::MilnorAlgebra(m) => m, - _ => unreachable!("NassauContext is only created when algebra is MilnorAlgebra"), - } +impl NassauResolution { + pub fn name(&self) -> &str { + &self.name } -} -impl Resolution { - /// Check all Nassau preconditions. Returns context if eligible. - /// - /// Nassau's algorithm requires: - /// - Prime 2 - /// - Milnor basis (the algebra is `SteenrodAlgebra::MilnorAlgebra`) - /// - Bounded target module (finite-dimensional) - /// - A save directory (Nassau stores qi data on disk) - pub(crate) fn nassau_context(&self) -> Option { - if self.prime() != TWO { - return None; - } - let algebra = self.algebra(); - if !matches!(&*algebra, algebra::SteenrodAlgebra::MilnorAlgebra(_)) { - return None; - } - self.target().module(0).max_degree()?; - if self.save_dir.is_none() { - return None; + pub fn set_name(&mut self, name: String) { + self.name = name; + } + + pub fn new(module: Arc) -> Self { + Self::new_with_save(module, None).unwrap() + } + + pub fn new_with_save( + module: Arc, + save_dir: impl Into, + ) -> anyhow::Result { + let save_dir = save_dir.into(); + let max_degree = module.max_degree().unwrap(); + let target = Arc::new(FiniteChainComplex::ccdz(module)); + + if let Some(p) = save_dir.write() { + for subdir in SaveKind::nassau_data() { + subdir.create_dir(p)?; + } } - Some(NassauContext { algebra }) + + Ok(Self { + lock: Mutex::new(()), + zero_module: Arc::new(FreeModule::new(target.algebra(), "F_{-1}".to_string(), 0)), + name: String::new(), + modules: OnceBiVec::new(0), + differentials: OnceBiVec::new(0), + chain_maps: OnceBiVec::new(0), + target, + max_degree, + save_dir, + }) } - fn nassau_add_generators(&self, b: Bidegree, num_new_gens: usize) { + fn add_generators(&self, b: Bidegree, num_new_gens: usize) { let gen_names = (0..num_new_gens) .map(|idx| format!("x_{:#}", BidegreeGenerator::new(b, idx))) .collect(); @@ -398,8 +443,47 @@ impl Resolution { .add_generators(b.t(), num_new_gens, Some(gen_names)); } + /// This function prepares the Resolution object to perform computations up to the + /// specified s degree. It does *not* perform any computations by itself. It simply lengthens + /// the `OnceVec`s `modules`, `chain_maps`, etc. to the right length. + fn extend_through_degree(&self, max_s: i32) { + let min_degree = self.min_degree(); + + self.modules.extend(max_s, |i| { + Arc::new(FreeModule::new( + Arc::clone(&self.algebra()), + format!("F{i}"), + min_degree, + )) + }); + + self.differentials.extend(0, |_| { + Arc::new(FreeModuleHomomorphism::new( + Arc::clone(&self.modules[0]), + Arc::clone(&self.zero_module), + 0, + )) + }); + + self.differentials.extend(max_s, |i| { + Arc::new(FreeModuleHomomorphism::new( + Arc::clone(&self.modules[i]), + Arc::clone(&self.modules[i - 1]), + 0, + )) + }); + + self.chain_maps.extend(max_s, |i| { + Arc::new(FreeModuleHomomorphism::new( + Arc::clone(&self.modules[i]), + self.target.module(i), + 0, + )) + }); + } + #[tracing::instrument(skip_all, fields(signature = ?signature, throughput))] - fn nassau_write_qi( + fn write_qi( f: &mut Option, scratch: &mut FpVector, signature: &[PPartEntry], @@ -451,15 +535,13 @@ impl Resolution { Ok(()) } - fn nassau_write_differential( + fn write_differential( &self, b: Bidegree, num_new_gens: usize, target_dim: usize, ) -> anyhow::Result<()> { - if self.should_save - && let Some(dir) = self.save_dir.write() - { + if let Some(dir) = self.save_dir.write() { let mut f = self .save_file(SaveKind::NassauDifferential, b) .create_file(dir.clone(), false); @@ -473,39 +555,37 @@ impl Resolution { Ok(()) } - #[tracing::instrument(skip(self, milnor), fields(%b, %subalgebra, num_new_gens, density))] - fn nassau_step_with_subalgebra( + #[tracing::instrument(skip(self), fields(%b, %subalgebra, num_new_gens, density))] + fn step_resolution_with_subalgebra( &self, b: Bidegree, subalgebra: MilnorSubalgebra, - milnor: &MilnorAlgebra, ) -> anyhow::Result<()> { let end = || { tracing::Span::current().record("num_new_gens", self.number_of_gens_in_bidegree(b)); tracing::Span::current().record( "density", - self.differential(b.s()).differential_density(b.t()) * 100.0, + self.differentials[b.s()].differential_density(b.t()) * 100.0, ); }; let p = self.prime(); let mut scratch = FpVector::new(p, 0); - let target = self.module(b.s() - 1); + let target = &*self.modules[b.s() - 1]; + let algebra = target.algebra(); let zero_sig = subalgebra.zero_signature(); let target_dim = target.dimension(b.t()); let target_mask: Vec = subalgebra - .signature_mask(milnor, &target, b.t(), &zero_sig) + .signature_mask(&algebra, target, b.t(), &zero_sig) .collect(); let target_masked_dim = target_mask.len(); - let next = self.module(b.s() - 2); + let next = &self.modules[b.s() - 2]; next.compute_basis(b.t()); - let mut f = if self.should_save - && let Some(dir) = self.save_dir().write() - { + let mut f = if let Some(dir) = self.save_dir().write() { let mut f = self .save_file(SaveKind::NassauQi, b - Bidegree::s_t(1, 0)) .create_file(dir.to_owned(), true); @@ -519,14 +599,13 @@ impl Resolution { let guard = tracing::info_span!("step", signature = ?zero_sig).entered(); let next_mask: Vec = subalgebra - .signature_mask(milnor, &next, b.t(), &zero_sig) + .signature_mask(&algebra, &self.modules[b.s() - 2], b.t(), &zero_sig) .collect(); let next_masked_dim = next_mask.len(); let full_matrix = { let _guard = ParallelGuard::new(); - self.differential(b.s() - 1) - .get_partial_matrix(b.t(), &target_mask) + self.differentials[b.s() - 1].get_partial_matrix(b.t(), &target_mask) }; let mut masked_matrix = AugmentedMatrix::new(p, target_masked_dim, [next_masked_dim, target_masked_dim]); @@ -538,7 +617,7 @@ impl Resolution { masked_matrix.row_reduce(); let kernel = masked_matrix.compute_kernel(); - Self::nassau_write_qi( + Self::write_qi( &mut f, &mut scratch, &zero_sig, @@ -554,8 +633,7 @@ impl Resolution { } // Compute image - let d_s = self.differential(b.s()); - let mut n = subalgebra.signature_matrix(milnor, &d_s, b.t(), &zero_sig); + let mut n = subalgebra.signature_matrix(&self.differentials[b.s()], b.t(), &zero_sig); n.row_reduce(); let next_row = n.rows(); @@ -565,7 +643,7 @@ impl Resolution { assert_eq!(num_new_gens, 0, "Adding generators at {b}"); } - self.nassau_add_generators(b, num_new_gens); + self.add_generators(b, num_new_gens); let mut xs = vec![FpVector::new(p, target_dim); num_new_gens]; let mut dxs = vec![FpVector::new(p, next.dimension(b.t())); num_new_gens]; @@ -591,8 +669,8 @@ impl Resolution { let _guard = tracing::info_span!("step", ?signature).entered(); target_mask.clear(); next_mask.clear(); - target_mask.extend(subalgebra.signature_mask(milnor, &target, b.t(), &signature)); - next_mask.extend(subalgebra.signature_mask(milnor, &next, b.t(), &signature)); + target_mask.extend(subalgebra.signature_mask(&algebra, target, b.t(), &signature)); + next_mask.extend(subalgebra.signature_mask(&algebra, next, b.t(), &signature)); let full_matrix = { let _guard = ParallelGuard::new(); @@ -629,7 +707,7 @@ impl Resolution { dx.as_slice_mut().add(full_matrix.row(i), 1); } } - Self::nassau_write_qi( + Self::write_qi( &mut f, &mut scratch, &signature, @@ -649,20 +727,20 @@ impl Resolution { f.write_u64::(Magic::End as u64)?; } - self.nassau_write_differential(b, num_new_gens, target_dim)?; + self.write_differential(b, num_new_gens, target_dim)?; Ok(()) } /// Step resolution for s = 0 #[tracing::instrument(skip(self))] - fn nassau_step0(&self, t: i32) { + fn step0(&self, t: i32) { self.zero_module.extend_by_zero(t); - let source_module = self.module(0); - let target_module = self.target().module(0); + let source_module = &self.modules[0]; + let target_module = self.target.module(0); - let chain_map = self.chain_map(0); - let d = self.differential(0); + let chain_map = &self.chain_maps[0]; + let d = &self.differentials[0]; let source_dim = source_module.dimension(t); let target_dim = target_module.dimension(t); @@ -691,7 +769,7 @@ impl Resolution { let num_new_gens = matrix.extend_to_surjection(0, target_dim, 0).len(); - self.nassau_add_generators(Bidegree::s_t(0, t), num_new_gens); + self.add_generators(Bidegree::s_t(0, t), num_new_gens); chain_map.add_generators_from_matrix_rows( t, @@ -710,12 +788,12 @@ impl Resolution { /// Step resolution for s = 1 #[tracing::instrument(skip(self))] - fn nassau_step1(&self, t: i32) -> anyhow::Result<()> { + fn step1(&self, t: i32) -> anyhow::Result<()> { let p = self.prime(); - let source_module = self.module(1); - let target_module = self.module(0); - let cc_module = self.target().module(0); + let source_module = &self.modules[1]; + let target_module = &self.modules[0]; + let cc_module = self.target.module(0); let source_dim = source_module.dimension(t); let target_dim = target_module.dimension(t); @@ -724,14 +802,12 @@ impl Resolution { AugmentedMatrix::<2>::new(p, target_dim, [cc_module.dimension(t), target_dim]); { let _guard = ParallelGuard::new(); - self.chain_map(0).get_matrix(matrix.segment(0, 0), t); + self.chain_maps[0].get_matrix(matrix.segment(0, 0), t); } matrix.segment(1, 1).add_identity(); matrix.row_reduce(); let desired_image = matrix.compute_kernel(); - let d1 = self.differential(1); - let mut matrix = AugmentedMatrix::<2>::new_with_capacity( p, source_dim, @@ -741,30 +817,31 @@ impl Resolution { ); { let _guard = ParallelGuard::new(); - d1.get_matrix(matrix.segment(0, 0), t); + self.differentials[1].get_matrix(matrix.segment(0, 0), t); } matrix.segment(1, 1).add_identity(); matrix.row_reduce(); let num_new_gens = matrix.extend_image(0, target_dim, &desired_image, 0).len(); - self.nassau_add_generators(Bidegree::s_t(1, t), num_new_gens); + self.add_generators(Bidegree::s_t(1, t), num_new_gens); - d1.add_generators_from_matrix_rows( + self.differentials[1].add_generators_from_matrix_rows( t, matrix .segment(0, 0) .row_slice(source_dim, source_dim + num_new_gens), ); - self.nassau_write_differential(Bidegree::s_t(1, t), num_new_gens, target_dim)?; + self.write_differential(Bidegree::s_t(1, t), num_new_gens, target_dim)?; Ok(()) } - fn nassau_step_with_result(&self, b: Bidegree, milnor: &MilnorAlgebra) -> anyhow::Result<()> { + fn step_resolution_with_result(&self, b: Bidegree) -> anyhow::Result<()> { + let p = self.prime(); let set_data = || { - let d = self.differential(b.s()); - let c = self.chain_map(b.s()); + let d = &self.differentials[b.s()]; + let c = &self.chain_maps[b.s()]; d.set_kernel(b.t(), None); d.set_image(b.t(), None); @@ -774,14 +851,13 @@ impl Resolution { c.set_image(b.t(), None); c.set_quasi_inverse(b.t(), None); }; - - self.module(b.s()).compute_basis(b.t()); + self.modules[b.s()].compute_basis(b.t()); if b.s() > 0 { - self.module(b.s() - 1).compute_basis(b.t()); + self.modules[b.s() - 1].compute_basis(b.t()); } if b.s() == 0 { - self.nassau_step0(b.t()); + self.step0(b.t()); return Ok(()); } @@ -799,20 +875,15 @@ impl Resolution { // want to resolve further, it will be bigger. let saved_target_res_dimension = f.read_u64::()? as usize; - self.nassau_add_generators(b, num_new_gens); + self.add_generators(b, num_new_gens); let mut d_targets = Vec::with_capacity(num_new_gens); for _ in 0..num_new_gens { - d_targets.push(FpVector::from_bytes( - self.prime(), - saved_target_res_dimension, - &mut f, - )?); + d_targets.push(FpVector::from_bytes(p, saved_target_res_dimension, &mut f)?); } - self.differential(b.s()) - .add_generators_from_rows(b.t(), d_targets); + self.differentials[b.s()].add_generators_from_rows(b.t(), d_targets); set_data(); @@ -820,71 +891,167 @@ impl Resolution { } if b.s() == 1 { - self.nassau_step1(b.t())?; + self.step1(b.t())?; set_data(); return Ok(()); } - self.nassau_step_with_subalgebra( + self.step_resolution_with_subalgebra( b, - MilnorSubalgebra::optimal_for( - b - Bidegree::s_t( - 0, - self.target() - .module(0) - .max_degree() - .expect("checked before resolving"), - ), - ), - milnor, + MilnorSubalgebra::optimal_for(b - Bidegree::s_t(0, self.max_degree)), )?; - self.chain_map(b.s()).extend_by_zero(b.t()); + self.chain_maps[b.s()].extend_by_zero(b.t()); set_data(); Ok(()) } - /// Try to compute a Nassau step at the given bidegree. Returns `Ok(())` if Nassau was used, - /// `Err(())` if the conditions weren't met and classical should be used instead. - pub(crate) fn try_nassau_step(&self, b: Bidegree) -> Result<(), ()> { - let ctx = self.nassau_context().ok_or(())?; + fn step_resolution(&self, b: Bidegree) { + self.step_resolution_with_result(b) + .unwrap_or_else(|e| panic!("Error computing bidegree {b}: {e}")); + } - // Ensure nassau save dirs exist - if self.should_save - && let Some(p) = self.save_dir.write() - { - for subdir in SaveKind::nassau_data() { - subdir.create_dir(p).unwrap(); + /// This function resolves up till a fixed stem instead of a fixed t. + #[tracing::instrument(skip(self), fields(self = self.name, %max))] + pub fn compute_through_stem(&self, max: Bidegree) { + let _lock = self.lock.lock(); + + self.extend_through_degree(max.s()); + self.algebra().compute_basis(max.t()); + + let tracing_span = tracing::Span::current(); + maybe_rayon::in_place_scope(|scope| { + let _tracing_guard = tracing_span.enter(); + + // This algorithm is not optimal, as we compute (s, t) only after computing (s - 1, t) + // and (s, t - 1). In theory, it suffices to wait for (s, t - 1) and (s - 1, t - 1), + // but having the dimensions of the modules change halfway through the computation is + // annoying to do correctly. It seems more prudent to improve parallelism elsewhere. + + // Things that we have finished computing. + let mut progress: Vec = vec![-1; max.s() as usize + 1]; + // We will kickstart the process by pretending we have computed (0, - 1). So + // we must pretend we have only computed up to (0, - 2); + progress[0] = -2; + + let (sender, receiver) = mpsc::channel(); + SenderData::send(Bidegree::s_t(0, -1), sender); + + let f = |b, sender| { + if self.has_computed_bidegree(b) { + SenderData::send(b, sender); + } else { + let tracing_span = tracing_span.clone(); + scope.spawn(move |_| { + let _tracing_guard = tracing_span.enter(); + if crate::utils::parallel::is_in_parallel() { + SenderData::send_retry(b, sender); + return; + } + self.step_resolution(b); + SenderData::send(b, sender); + }); + } + }; + + while let Ok(SenderData { b, retry, sender }) = receiver.recv() { + if retry { + f(b, sender); + continue; + } + assert!(progress[b.s() as usize] == b.t() - 1); + progress[b.s() as usize] = b.t(); + + // How far we are from the last one for this s. + let distance = max.n() - b.n() + 1; + + if b.s() < max.s() && progress[b.s() as usize + 1] == b.t() - 1 { + f(b + Bidegree::s_t(1, 0), sender.clone()); + } + + if distance > 1 && (b.s() == 0 || progress[b.s() as usize - 1] > b.t()) { + // We are computing a normal step + f(b + Bidegree::s_t(0, 1), sender); + } else if distance == 1 && b.s() < max.s() { + SenderData::send(b + Bidegree::s_t(0, 1), sender); + } + } + }); + } +} + +impl ChainComplex for NassauResolution { + type Algebra = MilnorAlgebra; + type Homomorphism = FreeModuleHomomorphism>; + type Module = FreeModule; + + fn prime(&self) -> ValidPrime { + TWO + } + + fn algebra(&self) -> Arc { + self.zero_module.algebra() + } + + fn module(&self, s: i32) -> Arc { + Arc::clone(&self.modules[s]) + } + + fn zero_module(&self) -> Arc { + Arc::clone(&self.zero_module) + } + + fn min_degree(&self) -> i32 { + 0 + } + + fn has_computed_bidegree(&self, b: Bidegree) -> bool { + self.differentials.len() > b.s() && self.differential(b.s()).next_degree() > b.t() + } + + fn differential(&self, s: i32) -> Arc { + Arc::clone(&self.differentials[s]) + } + + #[tracing::instrument(skip(self), fields(self = self.name, %max))] + fn compute_through_bidegree(&self, max: Bidegree) { + let _lock = self.lock.lock(); + + self.extend_through_degree(max.s()); + self.algebra().compute_basis(max.t()); + + for t in 0..=max.t() { + for s in 0..=max.s() { + let b = Bidegree::s_t(s, t); + if self.has_computed_bidegree(b) { + continue; + } + self.step_resolution(b); } } + } - self.nassau_step_with_result(b, ctx.milnor()) - .unwrap_or_else(|e| panic!("Error computing bidegree {b} with Nassau: {e}")); - Ok(()) + fn next_homological_degree(&self) -> i32 { + self.modules.len() } - /// Try to apply quasi-inverse using Nassau's saved data. - pub(crate) fn nassau_apply_quasi_inverse( - &self, - results: &mut [T], - b: Bidegree, - inputs: &[S], - ) -> Option + fn save_dir(&self) -> &SaveDirectory { + &self.save_dir + } + + fn apply_quasi_inverse(&self, results: &mut [T], b: Bidegree, inputs: &[S]) -> bool where for<'a> &'a mut T: Into>, for<'a> &'a S: Into>, { - let ctx = self.nassau_context()?; - let milnor = ctx.milnor(); - - let mut f = if let Some(dir) = self.save_dir.read() - && let Some(f) = self.save_file(SaveKind::NassauQi, b).open_file(dir.clone()) - { - f + let mut f = if let Some(dir) = self.save_dir.read() { + if let Some(f) = self.save_file(SaveKind::NassauQi, b).open_file(dir.clone()) { + f + } else { + return false; + } } else { - // No NassauQi file at this bidegree (e.g. at the boundary of the computed range). - // Return None to fall through to the classical qi path. - return None; + return false; }; let p = self.prime(); @@ -892,15 +1059,15 @@ impl Resolution { let target_dim = f.read_u64::().unwrap() as usize; let zero_mask_dim = f.read_u64::().unwrap() as usize; let subalgebra = MilnorSubalgebra::from_bytes(&mut f).unwrap(); - let source = self.module(b.s()); - let target = self.module(b.s() - 1); - let diff = self.differential(b.s()); + let source = &self.modules[b.s()]; + let target = &self.modules[b.s() - 1]; + let algebra = target.algebra(); let mut inputs: Vec = inputs.iter().map(|x| x.into().to_owned()).collect(); let mut mask: Vec = Vec::with_capacity(zero_mask_dim + 8); mask.extend(subalgebra.signature_mask( - milnor, - &source, + &algebra, + source, b.t(), &subalgebra.zero_signature(), )); @@ -931,7 +1098,7 @@ impl Resolution { assert_eq!(mask.len(), zero_mask_dim + num_new_gens); let target_zero_mask: Vec = subalgebra - .signature_mask(milnor, &target, b.t(), &subalgebra.zero_signature()) + .signature_mask(&algebra, target, b.t(), &subalgebra.zero_signature()) .collect(); let mut matrix = AugmentedMatrix::<3>::new( p, @@ -940,7 +1107,7 @@ impl Resolution { ); for i in 0..num_new_gens { - let dx = diff.output(b.t(), i); + let dx = self.differentials[b.s()].output(b.t(), i); matrix .row_segment_mut(i, 1, 1) .slice_mut(0, dx.len()) @@ -963,7 +1130,7 @@ impl Resolution { let signature = subalgebra.signature_from_bytes(&mut f).unwrap(); mask.clear(); - mask.extend(subalgebra.signature_mask(milnor, &source, b.t(), &signature)); + mask.extend(subalgebra.signature_mask(&algebra, source, b.t(), &signature)); scratch0.set_scratch_vector_size(mask.len()); } else if col == Magic::Fix as usize { // We need to fix the differential problem @@ -1040,14 +1207,50 @@ impl Resolution { target.element_to_string(b.t(), dx.as_slice()) ); } - Some(true) + true + } +} + +impl AugmentedChainComplex for NassauResolution { + type ChainMap = FreeModuleHomomorphism; + type TargetComplex = FiniteChainComplex>; + + fn target(&self) -> Arc { + Arc::clone(&self.target) + } + + fn chain_map(&self, s: i32) -> Arc { + Arc::clone(&self.chain_maps[s]) } } #[cfg(test)] mod tests { + // use expect_test::expect; + use super::*; + #[test] + fn test_restart_stem() { + todo!() + // let res = crate::utils::construct_standard("S_2", None).unwrap(); + // res.compute_through_stem(Bidegree::n_s(14, 8)); + // res.compute_through_bidegree(Bidegree::s_t(5, 19)); + + // expect![[r#" + // · + // · · + // · · · · + // · · · · + // · · · · · + // · · · · · · · + // · · · · · · · · · + // · · · · · + // · + // "#]] + // .assert_eq(&res.graded_dimension_string()); + } + #[test] fn test_signature_iterator() { let subalgebra = MilnorSubalgebra::new(vec![2, 1]); diff --git a/ext/src/utils.rs b/ext/src/utils.rs index 88f58aab06..25d6b5148a 100644 --- a/ext/src/utils.rs +++ b/ext/src/utils.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, sync::Arc}; use algebra::{ - AlgebraType, SteenrodAlgebra, + AlgebraType, MilnorAlgebra, SteenrodAlgebra, module::{FDModule, Module, SteenrodModule, steenrod_module}, }; use anyhow::{Context, anyhow}; @@ -126,19 +126,7 @@ impl> TryFrom<(Value, T)> for Config { /// returned. /// /// See [`construct_standard`] for what this accepts. -pub fn construct( - module_spec: T, - save_dir: impl Into, -) -> anyhow::Result -where - anyhow::Error: From, - T: TryInto, -{ - construct_standard(module_spec, save_dir) -} - -/// See [`construct`] -pub fn construct_standard( +pub fn construct( module_spec: T, save_dir: impl Into, ) -> anyhow::Result> @@ -211,6 +199,63 @@ where crate::resolution::MuResolution::new_with_save(chain_complex, save_dir) } +// pub fn construct( +// module_spec: T, +// save_dir: impl Into, +// ) -> anyhow::Result +// where +// anyhow::Error: From, +// T: TryInto, +// { +// #[cfg(feature = "nassau")] +// { +// construct_nassau(module_spec, save_dir) +// } + +// #[cfg(not(feature = "nassau"))] +// { +// construct_standard(module_spec, save_dir) +// } +// } + +// pub fn construct_nassau( +// module_spec: T, +// save_dir: impl Into, +// ) -> anyhow::Result>> +// where +// anyhow::Error: From, +// T: TryInto, +// { +// let Config { +// module: json, +// algebra, +// } = module_spec.try_into()?; + +// if algebra == AlgebraType::Adem { +// return Err(anyhow!("Nassau's algorithm requires Milnor's basis")); +// } +// if !json["profile"].is_null() { +// return Err(anyhow!( +// "Nassau's algorithm does not support non-trivial profile" +// )); +// } +// if json["p"].as_i64() != Some(2) { +// return Err(anyhow!("Nassau's algorithm does not support odd primes")); +// } +// if json["type"].as_str() != Some("finite dimensional module") { +// return Err(anyhow!( +// "Nassau's algorithm only supports finite dimensional modules" +// )); +// } + +// let algebra = Arc::new(MilnorAlgebra::new(fp::prime::TWO, false)); +// let module = Arc::new(FDModule::from_json(Arc::clone(&algebra), &json)?); + +// if !json["cofiber"].is_null() { +// return Err(anyhow!("Nassau's algorithm does not support cofiber")); +// } +// crate::nassau::Resolution::new_with_save(module, save_dir) +// } /// Load a module specification from a JSON file. /// @@ -296,7 +341,9 @@ pub fn query_module_only( construct(module, save_dir).context("Failed to load module from save file")?; let load_quasi_inverse = load_quasi_inverse && resolution.save_dir().is_none(); - resolution.load_quasi_inverse = load_quasi_inverse; + + resolution.set_load_quasi_inverse(load_quasi_inverse); + resolution.set_name(name); Ok(resolution) @@ -351,7 +398,7 @@ pub fn query_unstable_module(load_quasi_inverse: bool) -> anyhow::Result::Ok(PathBuf::from(x)) diff --git a/ext/tests/milnor_vs_adem.rs b/ext/tests/milnor_vs_adem.rs index af0b24a5f6..2ef9381ecd 100644 --- a/ext/tests/milnor_vs_adem.rs +++ b/ext/tests/milnor_vs_adem.rs @@ -1,6 +1,6 @@ use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - utils::{construct, construct_standard}, + utils::construct, }; use rstest::rstest; use sseq::coordinates::Bidegree; @@ -21,8 +21,8 @@ use sseq::coordinates::Bidegree; #[case("ksp", 30)] fn compare(#[case] module_name: &str, #[case] max_degree: i32) { let max = Bidegree::s_t(max_degree, max_degree); - let a = construct((module_name, "adem"), None).unwrap(); - let b = construct((module_name, "milnor"), None).unwrap(); + let a = construct::((module_name, "adem"), None).unwrap(); + let b = construct::((module_name, "milnor"), None).unwrap(); a.compute_through_bidegree(max); b.compute_through_bidegree(max); @@ -41,8 +41,8 @@ fn compare(#[case] module_name: &str, #[case] max_degree: i32) { #[case("Calpha[15]", 50)] fn compare_unstable(#[case] module_name: &str, #[case] max_degree: i32) { let max = Bidegree::s_t(max_degree, max_degree); - let a = construct_standard::((module_name, "adem"), None).unwrap(); - let b = construct_standard::((module_name, "milnor"), None).unwrap(); + let a = construct::((module_name, "adem"), None).unwrap(); + let b = construct::((module_name, "milnor"), None).unwrap(); a.compute_through_bidegree(max); b.compute_through_bidegree(max); diff --git a/ext/tests/milnor_vs_nassau.rs b/ext/tests/milnor_vs_nassau.rs index e0ff34f008..024e8e1415 100644 --- a/ext/tests/milnor_vs_nassau.rs +++ b/ext/tests/milnor_vs_nassau.rs @@ -1,6 +1,6 @@ use ext::{ chain_complex::{ChainComplex, FreeChainComplex}, - utils::construct_standard, + utils::construct, }; use rstest::rstest; use sseq::coordinates::Bidegree; @@ -19,12 +19,11 @@ fn compare(#[case] module_name: &str, #[case] max_degree: i32) { let max = Bidegree::s_t(max_degree, max_degree); // Without save dir: classical algorithm - let classical = construct_standard::(module_name, None).unwrap(); + let classical = construct::(module_name, None).unwrap(); // With save dir: Nassau's algorithm will be used if eligible let save_dir = tempfile::tempdir().unwrap(); - let nassau = - construct_standard::(module_name, Some(save_dir.path().to_owned())).unwrap(); + let nassau = construct::(module_name, Some(save_dir.path().to_owned())).unwrap(); classical.compute_through_bidegree(max); nassau.compute_through_bidegree(max); diff --git a/ext/tests/module_construct_error.rs b/ext/tests/module_construct_error.rs index b6fa6e1948..2b3d26e8f5 100644 --- a/ext/tests/module_construct_error.rs +++ b/ext/tests/module_construct_error.rs @@ -19,6 +19,6 @@ fn module_construct_error() { } fn test(json: Value) { - assert!(construct((json.clone(), "adem"), None).is_err()); - assert!(construct((json, "milnor"), None).is_err()); + assert!(construct::((json.clone(), "adem"), None).is_err()); + assert!(construct::((json, "milnor"), None).is_err()); }