From d731aa315dc0f947c699342f77414fd6e9da75a1 Mon Sep 17 00:00:00 2001 From: Joey Beauvais-Feisthauer Date: Mon, 29 Jun 2026 02:46:30 -0400 Subject: [PATCH] Split up `ext::secondary` file --- ext/src/chain_complex/chain_homotopy.rs | 304 +++++++++ ext/src/chain_complex/mod.rs | 2 +- ext/src/resolution.rs | 172 ++++++ ext/src/resolution_homomorphism.rs | 384 ++++++++++++ ext/src/secondary.rs | 777 +----------------------- 5 files changed, 875 insertions(+), 764 deletions(-) diff --git a/ext/src/chain_complex/chain_homotopy.rs b/ext/src/chain_complex/chain_homotopy.rs index 7bf90a2df5..c0ec0fd0d2 100644 --- a/ext/src/chain_complex/chain_homotopy.rs +++ b/ext/src/chain_complex/chain_homotopy.rs @@ -284,3 +284,307 @@ impl< &self.save_dir } } + +// The secondary lift of a `ChainHomotopy` lives here, beside the primary object it lifts, rather +// than in the monolithic `secondary` module. This keeps `secondary.rs` to the shared lift machinery +// and pairs each variant with its primary for locality. The module is `pub(crate)`; +// `SecondaryChainHomotopy` is re-exported from `crate::secondary` so the public API path is +// unchanged. +pub(crate) mod secondary { + use std::sync::Arc; + + use algebra::{ + module::{Module, homomorphism::ModuleHomomorphism}, + pair_algebra::PairAlgebra, + }; + use dashmap::DashMap; + use fp::vector::FpVector; + use once::OnceBiVec; + use sseq::coordinates::{Bidegree, BidegreeGenerator, BidegreeRange}; + + use super::ChainHomotopy; + use crate::{ + chain_complex::FreeChainComplex, + resolution_homomorphism::ResolutionHomomorphism, + save::{SaveDirectory, SaveKind}, + secondary::{ + CompositeData, LAMBDA_BIDEGREE, SecondaryHomotopy, SecondaryLift, + SecondaryResolutionHomomorphism, + }, + }; + + #[doc(hidden)] + pub struct SecondaryChainHomotopy< + S: FreeChainComplex, + T: FreeChainComplex + Sync, + U: FreeChainComplex + Sync, + > + where + S::Algebra: PairAlgebra, + { + underlying: Arc>, + left: Arc>, + right: Arc>, + left_lambda: Option>>, + right_lambda: Option>>, + homotopies: OnceBiVec>, + intermediates: DashMap, + } + + impl< + S: FreeChainComplex, + T: FreeChainComplex + Sync, + U: FreeChainComplex + Sync, + > SecondaryLift for SecondaryChainHomotopy + where + S::Algebra: PairAlgebra, + { + type Algebra = S::Algebra; + type Source = S; + type Target = U; + type Underlying = ChainHomotopy; + + const HIT_GENERATOR: bool = true; + + fn underlying(&self) -> Arc { + Arc::clone(&self.underlying) + } + + fn algebra(&self) -> Arc { + self.left.algebra() + } + + fn source(&self) -> Arc { + self.left.source() + } + + fn target(&self) -> Arc { + self.right.target() + } + + fn shift(&self) -> Bidegree { + Bidegree::s_t( + self.underlying.shift().s(), + self.left.shift().t() + self.right.shift().t(), + ) + } + + fn max(&self) -> BidegreeRange<'_, Self> { + BidegreeRange::new( + self, + std::cmp::min( + self.right.secondary_target().max().s() + self.shift().s() - 1, + self.left.secondary_source().max().s(), + ), + &|selff, s| { + std::cmp::min( + selff.left.secondary_source().max().t(s), + if s == selff.shift().s() { + i32::MAX + } else { + selff + .right + .secondary_target() + .max() + .t(s - selff.shift().s() + 1) + + selff.shift().t() + }, + ) + }, + ) + } + + fn homotopies(&self) -> &OnceBiVec> { + &self.homotopies + } + + fn intermediates(&self) -> &DashMap { + &self.intermediates + } + + fn save_dir(&self) -> &SaveDirectory { + self.underlying.save_dir() + } + + fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { + let p = self.prime(); + let neg_1 = p - 1; + let shifted_b = g.degree() - self.shift(); + + let target = self.target().module(shifted_b.s() - 1); + + let mut result = FpVector::new(p, target.dimension(shifted_b.t() - 1)); + + self.homotopies[g.s() - 1].act( + result.as_slice_mut(), + 1, + g.t(), + self.source() + .differential(g.s()) + .output(g.t(), g.idx()) + .as_slice(), + false, + ); + + self.right.secondary_target().homotopies()[shifted_b.s() + 1].act( + result.as_slice_mut(), + 1, + shifted_b.t(), + self.underlying + .homotopy(g.s()) + .output(g.t(), g.idx()) + .as_slice(), + true, + ); + + self.underlying.homotopy(g.s() - 2).apply( + result.as_slice_mut(), + neg_1, + g.t() - 1, + self.left.secondary_source().homotopies()[g.s()] + .homotopies + .output(g.t(), g.idx()) + .as_slice(), + ); + + let left_shifted_b = g.degree() - self.left.underlying().shift; + self.right.homotopies()[left_shifted_b.s()].act( + result.as_slice_mut(), + neg_1, + left_shifted_b.t(), + self.left + .underlying() + .get_map(g.s()) + .output(g.t(), g.idx()) + .as_slice(), + true, + ); + + // This is inefficient if both right_lambda and right are non-zero, but this is not needed atm + // and the change would not be user-facing. + if let Some(right_lambda) = &self.right_lambda { + right_lambda.get_map(left_shifted_b.s()).apply( + result.as_slice_mut(), + neg_1, + left_shifted_b.t(), + self.left + .underlying() + .get_map(g.s()) + .output(g.t(), g.idx()) + .as_slice(), + ); + } + + self.right + .underlying() + .get_map(left_shifted_b.s() - 1) + .apply( + result.as_slice_mut(), + neg_1, + left_shifted_b.t() - 1, + self.left.homotopies()[g.s()] + .homotopies + .output(g.t(), g.idx()) + .as_slice(), + ); + + if let Some(left_lambda) = &self.left_lambda { + self.right + .underlying() + .get_map(left_shifted_b.s() - 1) + .apply( + result.as_slice_mut(), + neg_1, + left_shifted_b.t() - 1, + left_lambda.get_map(g.s()).output(g.t(), g.idx()).as_slice(), + ); + } + result + } + + fn composite(&self, s: i32) -> CompositeData { + let p = self.prime(); + // This is -1 mod p^2 + let neg_1 = p * p - 1; + + vec![ + ( + neg_1, + self.underlying.left().get_map(s), + self.underlying + .right() + .get_map(s - self.left.underlying().shift.s()), + ), + ( + 1, + self.underlying.homotopy(s), + self.target().differential(s - self.shift().s() + 1), + ), + ( + 1, + self.source().differential(s), + self.underlying.homotopy(s - 1), + ), + ] + } + } + + impl< + S: FreeChainComplex, + T: FreeChainComplex + Sync, + U: FreeChainComplex + Sync, + > SecondaryChainHomotopy + where + S::Algebra: PairAlgebra, + { + pub fn new( + left: Arc>, + right: Arc>, + left_lambda: Option>>, + right_lambda: Option>>, + underlying: Arc>, + ) -> Self { + assert!(Arc::ptr_eq(&underlying.left(), &left.underlying())); + assert!(Arc::ptr_eq(&underlying.right(), &right.underlying())); + + if let Some(left_lambda) = &left_lambda { + assert!(Arc::ptr_eq(&left_lambda.source, &underlying.left().source)); + assert!(Arc::ptr_eq(&left_lambda.target, &underlying.left().target)); + + assert_eq!(left_lambda.shift, underlying.left().shift + LAMBDA_BIDEGREE); + } + + if let Some(right_lambda) = &right_lambda { + assert!(Arc::ptr_eq( + &right_lambda.source, + &underlying.right().source + )); + assert!(Arc::ptr_eq( + &right_lambda.target, + &underlying.right().target + )); + + assert_eq!( + right_lambda.shift, + underlying.right().shift + LAMBDA_BIDEGREE + ); + } + + if let Some(p) = underlying.save_dir().write() { + for subdir in SaveKind::secondary_data() { + subdir.create_dir(p).unwrap(); + } + } + + Self { + left, + right, + left_lambda, + right_lambda, + homotopies: OnceBiVec::new(underlying.shift().s()), + underlying, + intermediates: DashMap::new(), + } + } + } +} diff --git a/ext/src/chain_complex/mod.rs b/ext/src/chain_complex/mod.rs index 2cafb4f671..cf9b275801 100644 --- a/ext/src/chain_complex/mod.rs +++ b/ext/src/chain_complex/mod.rs @@ -1,4 +1,4 @@ -mod chain_homotopy; +pub(crate) mod chain_homotopy; mod finite_chain_complex; use std::sync::Arc; diff --git a/ext/src/resolution.rs b/ext/src/resolution.rs index 1d138cafd4..d37edd20ed 100644 --- a/ext/src/resolution.rs +++ b/ext/src/resolution.rs @@ -978,6 +978,178 @@ where } } +// The secondary lift of a `Resolution` lives here, beside the primary object it lifts, rather than +// in the monolithic `secondary` module. This keeps `secondary.rs` to the shared lift machinery and +// pairs each variant with its primary for locality. The module is `pub(crate)`; `SecondaryResolution` +// is re-exported from `crate::secondary` so the public API path is unchanged. +pub(crate) mod secondary { + use std::sync::Arc; + + use algebra::{module::Module, pair_algebra::PairAlgebra}; + use dashmap::DashMap; + use fp::vector::FpVector; + use once::OnceBiVec; + use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator, BidegreeRange}; + + use crate::{ + chain_complex::FreeChainComplex, + save::{SaveDirectory, SaveKind}, + secondary::{CompositeData, SecondaryHomotopy, SecondaryLift}, + }; + + pub struct SecondaryResolution + where + CC::Algebra: PairAlgebra, + { + underlying: Arc, + /// s -> t -> idx -> homotopy + pub(crate) homotopies: OnceBiVec>, + intermediates: DashMap, + } + + impl SecondaryLift for SecondaryResolution + where + CC::Algebra: PairAlgebra, + { + type Algebra = CC::Algebra; + type Source = CC; + type Target = CC; + type Underlying = CC; + + fn underlying(&self) -> Arc { + Arc::clone(&self.underlying) + } + + fn algebra(&self) -> Arc { + self.underlying.algebra() + } + + fn source(&self) -> Arc { + Arc::clone(&self.underlying) + } + + fn target(&self) -> Arc { + Arc::clone(&self.underlying) + } + + fn shift(&self) -> Bidegree { + Bidegree::s_t(2, 0) + } + + fn max(&self) -> BidegreeRange<'_, Self> { + BidegreeRange::new( + self, + self.underlying.next_homological_degree(), + &|selff, s| { + std::cmp::min( + selff.underlying.module(s).max_computed_degree(), + selff.underlying.module(s - 2).max_computed_degree() + 1, + ) + 1 + }, + ) + } + + fn homotopies(&self) -> &OnceBiVec> { + &self.homotopies + } + + fn intermediates(&self) -> &DashMap { + &self.intermediates + } + + fn save_dir(&self) -> &SaveDirectory { + self.underlying.save_dir() + } + + fn composite(&self, s: i32) -> CompositeData { + let d1 = self.underlying.differential(s); + let d0 = self.underlying.differential(s - 1); + vec![(1, d1, d0)] + } + + fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { + let p = self.prime(); + let target = self.underlying.module(g.s() - 3); + let mut result = FpVector::new(p, target.dimension(g.t() - 1)); + let d = self.underlying.differential(g.s()); + self.homotopies[g.s() - 1].act( + result.as_slice_mut(), + 1, + g.t(), + d.output(g.t(), g.idx()).as_slice(), + false, + ); + result + } + } + + impl SecondaryResolution + where + CC::Algebra: PairAlgebra, + { + pub fn new(cc: Arc) -> Self { + if let Some(p) = cc.save_dir().write() { + for subdir in SaveKind::secondary_data() { + subdir.create_dir(p).unwrap(); + } + } + + Self { + underlying: cc, + homotopies: OnceBiVec::new(2), + intermediates: DashMap::new(), + } + } + + pub fn homotopy(&self, s: i32) -> &SecondaryHomotopy { + &self.homotopies[s] + } + + pub fn e3_page(&self) -> sseq::Sseq<2, sseq::Adams> { + let p = self.prime(); + + let mut sseq = self.underlying.to_sseq(); + + let mut source_vec = FpVector::new(p, 0); + let mut target_vec = FpVector::new(p, 0); + + for b in self.underlying.iter_stem() { + if b.t() > 0 + && self + .underlying + .has_computed_bidegree(b + Bidegree::n_s(-1, 2)) + { + let m = self.homotopy(b.s() + 2).homotopies.hom_k(b.t()); + if m.is_empty() || m[0].is_empty() { + continue; + } + + source_vec.set_scratch_vector_size(m.len()); + target_vec.set_scratch_vector_size(m[0].len()); + + for (i, row) in m.into_iter().enumerate() { + source_vec.set_to_zero(); + source_vec.set_entry(i, 1); + target_vec.copy_from_slice(&row); + + let source = BidegreeElement::new(b, source_vec); + sseq.add_differential(2, &source, target_vec.as_slice()); + + source_vec = source.into_vec(); + } + } + } + + for b in self.underlying.iter_stem() { + if sseq.invalid(b) { + sseq.update_degree(b); + } + } + sseq + } + } +} + #[cfg(test)] mod tests { use expect_test::expect; diff --git a/ext/src/resolution_homomorphism.rs b/ext/src/resolution_homomorphism.rs index 905bf6020a..271c7318d3 100644 --- a/ext/src/resolution_homomorphism.rs +++ b/ext/src/resolution_homomorphism.rs @@ -491,3 +491,387 @@ where } } } + +// The secondary lift of a `ResolutionHomomorphism` lives here, beside the primary object it lifts, +// rather than in the monolithic `secondary` module. This keeps `secondary.rs` to the shared lift +// machinery and pairs each variant with its primary for locality. The module is `pub(crate)`; +// `SecondaryResolutionHomomorphism` is re-exported from `crate::secondary` so the public API path is +// unchanged. +pub(crate) mod secondary { + use std::sync::Arc; + + use algebra::{ + module::{Module, homomorphism::ModuleHomomorphism}, + pair_algebra::PairAlgebra, + }; + use dashmap::DashMap; + use fp::{ + matrix::Matrix, + vector::{FpSlice, FpSliceMut, FpVector}, + }; + use itertools::Itertools; + use once::OnceBiVec; + use sseq::coordinates::{Bidegree, BidegreeGenerator, BidegreeRange}; + + use super::ResolutionHomomorphism; + use crate::{ + chain_complex::FreeChainComplex, + save::{SaveDirectory, SaveKind}, + secondary::{ + CompositeData, LAMBDA_BIDEGREE, SecondaryHomotopy, SecondaryLift, SecondaryResolution, + }, + }; + + // Rustdoc ICE's when trying to document this struct. See + // https://github.com/rust-lang/rust/issues/91380 + #[doc(hidden)] + pub struct SecondaryResolutionHomomorphism< + CC1: FreeChainComplex, + CC2: FreeChainComplex, + > + where + CC1::Algebra: PairAlgebra, + { + source: Arc>, + target: Arc>, + underlying: Arc>, + /// input s -> homotopy + homotopies: OnceBiVec>, + intermediates: DashMap, + } + + impl> SecondaryLift + for SecondaryResolutionHomomorphism + where + CC1::Algebra: PairAlgebra, + { + type Algebra = CC1::Algebra; + type Source = CC1; + type Target = CC2; + type Underlying = ResolutionHomomorphism; + + fn underlying(&self) -> Arc { + Arc::clone(&self.underlying) + } + + fn algebra(&self) -> Arc { + self.source.algebra() + } + + fn source(&self) -> Arc { + Arc::clone(&self.source.underlying()) + } + + fn target(&self) -> Arc { + Arc::clone(&self.target.underlying()) + } + + fn shift(&self) -> Bidegree { + self.underlying.shift + Bidegree::s_t(1, 0) + } + + fn max(&self) -> BidegreeRange<'_, Self> { + BidegreeRange::new( + self, + self.underlying.next_homological_degree(), + &|selff, s| { + std::cmp::min( + selff.underlying.get_map(s).next_degree(), + std::cmp::min( + selff.source.homotopies[s].homotopies.next_degree(), + if s == selff.shift().s() { + i32::MAX + } else { + selff.target.homotopies[s + 1 - selff.shift().s()] + .composites + .max_degree() + + selff.shift().t() + + 1 + }, + ), + ) + }, + ) + } + + fn homotopies(&self) -> &OnceBiVec> { + &self.homotopies + } + + fn intermediates(&self) -> &DashMap { + &self.intermediates + } + + fn save_dir(&self) -> &SaveDirectory { + self.underlying.save_dir() + } + + fn composite(&self, s: i32) -> CompositeData { + let p = self.prime(); + // This is -1 mod p^2 + let neg_1 = p * p - 1; + + let d_source = self.source.underlying().differential(s); + let d_target = self + .target + .underlying() + .differential(s + 1 - self.shift().s()); + + let c1 = self.underlying.get_map(s); + let c0 = self.underlying.get_map(s - 1); + + vec![(neg_1, d_source, c0), (1, c1, d_target)] + } + + fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { + let p = self.prime(); + let neg_1 = p - 1; + let shifted_b = g.degree() - self.shift(); + let target = self.target().module(shifted_b.s() - 1); + + let mut result = FpVector::new(p, target.dimension(shifted_b.t() - 1)); + let d = self.source().differential(g.s()); + + self.homotopies[g.s() - 1].act( + result.as_slice_mut(), + neg_1, + g.t(), + d.output(g.t(), g.idx()).as_slice(), + false, + ); + self.target.homotopy(shifted_b.s() + 1).act( + result.as_slice_mut(), + neg_1, + shifted_b.t(), + self.underlying + .get_map(g.s()) + .output(g.t(), g.idx()) + .as_slice(), + true, + ); + self.underlying.get_map(g.s() - 2).apply( + result.as_slice_mut(), + 1, + g.t() - 1, + self.source + .homotopy(g.s()) + .homotopies + .output(g.t(), g.idx()) + .as_slice(), + ); + + result + } + } + + impl> + SecondaryResolutionHomomorphism + where + CC1::Algebra: PairAlgebra, + { + pub fn new( + source: Arc>, + target: Arc>, + underlying: Arc>, + ) -> Self { + assert!(Arc::ptr_eq(&underlying.source, &source.underlying())); + assert!(Arc::ptr_eq(&underlying.target, &target.underlying())); + + if let Some(p) = underlying.save_dir().write() { + for subdir in SaveKind::secondary_data() { + subdir.create_dir(p).unwrap(); + } + } + + Self { + source, + target, + homotopies: OnceBiVec::new(underlying.shift.s() + 1), + underlying, + intermediates: DashMap::new(), + } + } + + pub fn name(&self) -> String { + let name = self.underlying.name(); + if name.starts_with('[') || name.starts_with('λ') { + name.to_owned() + } else { + format!("[{name}]") + } + } + + pub fn homotopy(&self, s: i32) -> &SecondaryHomotopy { + &self.homotopies[s] + } + + pub fn secondary_source(&self) -> Arc> { + Arc::clone(&self.source) + } + + pub fn secondary_target(&self) -> Arc> { + Arc::clone(&self.target) + } + + /// A version of [`hom_k`] but with a non-trivial λ part. + pub fn hom_k_with<'a>( + &self, + lambda_part: Option<&ResolutionHomomorphism>, + sseq: Option<&sseq::Sseq<2, sseq::Adams>>, + b: Bidegree, + inputs: impl Iterator>, + outputs: impl Iterator>, + ) { + let source = b + self.shift() - Bidegree::s_t(1, 0); + let lambda_source = source + LAMBDA_BIDEGREE; + + let p = self.prime(); + let h_0 = self.algebra().p_tilde(); + + let source_num_gens = self.source().number_of_gens_in_bidegree(source); + let lambda_num_gens = self.source().number_of_gens_in_bidegree(lambda_source); + + let m0 = self.underlying.get_map(source.s()).hom_k(b.t()); + let mut m1 = + Matrix::from_vec(p, &self.homotopy(lambda_source.s()).homotopies.hom_k(b.t())); + if let Some(lambda_part) = lambda_part { + m1 += &Matrix::from_vec(p, &lambda_part.get_map(lambda_source.s()).hom_k(b.t())); + } + + // The multiplication by p map + let mp = Matrix::from_vec( + p, + &self + .source() + .filtration_one_product(1, h_0, source) + .unwrap(), + ); + + let sign = if (self.underlying.shift.s() * b.t()) % 2 == 1 { + p * p - 1 + } else { + 1 + }; + let filtration_one_sign = if (b.t() % 2) == 1 { p - 1 } else { 1 }; + + let page_data = sseq.map(|sseq| { + let d = sseq.page_data(lambda_source); + &d[std::cmp::min(3, d.len() - 1)] + }); + + let mut scratch0: Vec = Vec::new(); + for (input, mut out) in inputs.zip_eq(outputs) { + scratch0.clear(); + scratch0.resize(source_num_gens, 0); + for (i, v) in input.iter_nonzero() { + scratch0 + .iter_mut() + .zip_eq(&m0[i]) + .for_each(|(a, b)| *a += v * b * sign); + out.slice_mut(source_num_gens, source_num_gens + lambda_num_gens) + .add(m1.row(i), (v * sign) % p); + } + for (i, v) in scratch0.iter().enumerate() { + out.add_basis_element(i, *v % p); + + let extra = *v / p; + out.slice_mut(source_num_gens, source_num_gens + lambda_num_gens) + .add(mp.row(i), (extra * filtration_one_sign) % p); + } + if let Some(page_data) = page_data { + page_data.reduce_by_quotient( + out.slice_mut(source_num_gens, source_num_gens + lambda_num_gens), + ); + } + } + } + + /// Compute the induced map on Mod_{C\lambda^2} homotopy groups. This only computes it on + /// standard lifts on elements in Ext. `outputs` is an iterator of `FpSliceMut`s whose lengths + /// are equal to the total dimension of `(s + shift_s, t + shift_t)` and `(s + shift_s + 1, t + + /// shift_t + 1)`. The first chunk records the Ext part of the result, and the second chunk + /// records the λ part of the result. + /// + /// This reduces the λ part of the result by the image of d₂. + /// + /// # Arguments + /// - `sseq`: A sseq object that records the $d_2$ differentials. If present, reduce the value + /// of the map by the image of $d_2$. + pub fn hom_k<'a>( + &self, + sseq: Option<&sseq::Sseq<2, sseq::Adams>>, + b: Bidegree, + inputs: impl Iterator>, + outputs: impl Iterator>, + ) { + self.hom_k_with(None, sseq, b, inputs, outputs); + } + + /// Given an element b whose product with this is null, find the element whose $d_2$ hits the + /// λ part of the composition. + /// + /// # Arguments: + /// - `sseq`: spectral sequence object of the source + pub fn product_nullhomotopy( + &self, + lambda_part: Option<&ResolutionHomomorphism>, + sseq: &sseq::Sseq<2, sseq::Adams>, + b: Bidegree, + class: FpSlice, + ) -> FpVector { + let p = self.prime(); + let shift = self.underlying.shift; + + let result_num_gens = self + .source() + .number_of_gens_in_bidegree(shift + b - Bidegree::s_t(1, 0)); + + let lambda_num_gens = self + .source() + .number_of_gens_in_bidegree(b + shift + LAMBDA_BIDEGREE); + + let lower_num_gens = self.source().number_of_gens_in_bidegree(b + shift); + + let target_num_gens = self.target().number_of_gens_in_bidegree(b); + let target_lambda_num_gens = self + .target() + .number_of_gens_in_bidegree(b + LAMBDA_BIDEGREE); + + let mut output_class = FpVector::new(p, result_num_gens); + if result_num_gens == 0 || lambda_num_gens == 0 { + return output_class; + } + + let mut prod_value = FpVector::new(p, lower_num_gens + lambda_num_gens); + self.hom_k_with( + lambda_part, + None, + b, + [class.restrict(0, target_num_gens)].into_iter(), + [prod_value.as_slice_mut()].into_iter(), + ); + assert!(prod_value.slice(0, lower_num_gens).is_zero()); + + let matrix = Matrix::from_vec( + p, + &self + .underlying + .get_map((b + shift + LAMBDA_BIDEGREE).s()) + .hom_k((b + LAMBDA_BIDEGREE).t()), + ); + matrix.apply( + prod_value.slice_mut(lower_num_gens, lower_num_gens + lambda_num_gens), + 1, + class.restrict(target_num_gens, target_num_gens + target_lambda_num_gens), + ); + + let diff_source = b + shift - Bidegree::n_s(-1, 1); + sseq.differentials(diff_source)[2].quasi_inverse( + output_class.as_slice_mut(), + prod_value.slice(lower_num_gens, lower_num_gens + lambda_num_gens), + ); + + output_class + } + } +} diff --git a/ext/src/secondary.rs b/ext/src/secondary.rs index 34aaa6dd7f..e04b852a36 100644 --- a/ext/src/secondary.rs +++ b/ext/src/secondary.rs @@ -12,19 +12,28 @@ use bivec::BiVec; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use dashmap::DashMap; use fp::{ - matrix::Matrix, prime::ValidPrime, vector::{FpSlice, FpSliceMut, FpVector}, }; -use itertools::Itertools; use maybe_rayon::prelude::*; use once::OnceBiVec; -use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator, BidegreeRange}; +use sseq::coordinates::{Bidegree, BidegreeGenerator, BidegreeRange}; use tracing::Level; +// This module holds the shared machinery for secondary lifts (the `SecondaryLift` trait, +// `SecondaryHomotopy`, etc.). The concrete lift types used to live here too, but they were moved +// next to their primary counterparts to keep this file from growing without bound and to improve +// locality: each secondary variant now sits beside the primary object it lifts (e.g. +// `SecondaryResolution` next to `Resolution`). Those variants live in private `secondary` +// submodules and are re-exported here so that the public API is unchanged — callers still find them +// under `crate::secondary`. +pub use crate::{ + chain_complex::chain_homotopy::secondary::SecondaryChainHomotopy, + resolution::secondary::SecondaryResolution, + resolution_homomorphism::secondary::SecondaryResolutionHomomorphism, +}; use crate::{ - chain_complex::{ChainComplex, ChainHomotopy, FreeChainComplex}, - resolution_homomorphism::ResolutionHomomorphism, + chain_complex::{ChainComplex, FreeChainComplex}, save::{SaveDirectory, SaveFile, SaveKind}, }; @@ -659,764 +668,6 @@ pub trait SecondaryLift: Sync + Sized { } } -pub struct SecondaryResolution -where - CC::Algebra: PairAlgebra, -{ - underlying: Arc, - /// s -> t -> idx -> homotopy - pub(crate) homotopies: OnceBiVec>, - intermediates: DashMap, -} - -impl SecondaryLift for SecondaryResolution -where - CC::Algebra: PairAlgebra, -{ - type Algebra = CC::Algebra; - type Source = CC; - type Target = CC; - type Underlying = CC; - - fn underlying(&self) -> Arc { - Arc::clone(&self.underlying) - } - - fn algebra(&self) -> Arc { - self.underlying.algebra() - } - - fn source(&self) -> Arc { - Arc::clone(&self.underlying) - } - - fn target(&self) -> Arc { - Arc::clone(&self.underlying) - } - - fn shift(&self) -> Bidegree { - Bidegree::s_t(2, 0) - } - - fn max(&self) -> BidegreeRange<'_, Self> { - BidegreeRange::new( - self, - self.underlying.next_homological_degree(), - &|selff, s| { - std::cmp::min( - selff.underlying.module(s).max_computed_degree(), - selff.underlying.module(s - 2).max_computed_degree() + 1, - ) + 1 - }, - ) - } - - fn homotopies(&self) -> &OnceBiVec> { - &self.homotopies - } - - fn intermediates(&self) -> &DashMap { - &self.intermediates - } - - fn save_dir(&self) -> &SaveDirectory { - self.underlying.save_dir() - } - - fn composite(&self, s: i32) -> CompositeData { - let d1 = self.underlying.differential(s); - let d0 = self.underlying.differential(s - 1); - vec![(1, d1, d0)] - } - - fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { - let p = self.prime(); - let target = self.underlying.module(g.s() - 3); - let mut result = FpVector::new(p, target.dimension(g.t() - 1)); - let d = self.underlying.differential(g.s()); - self.homotopies[g.s() - 1].act( - result.as_slice_mut(), - 1, - g.t(), - d.output(g.t(), g.idx()).as_slice(), - false, - ); - result - } -} - -impl SecondaryResolution -where - CC::Algebra: PairAlgebra, -{ - pub fn new(cc: Arc) -> Self { - if let Some(p) = cc.save_dir().write() { - for subdir in SaveKind::secondary_data() { - subdir.create_dir(p).unwrap(); - } - } - - Self { - underlying: cc, - homotopies: OnceBiVec::new(2), - intermediates: DashMap::new(), - } - } - - pub fn homotopy(&self, s: i32) -> &SecondaryHomotopy { - &self.homotopies[s] - } - - pub fn e3_page(&self) -> sseq::Sseq<2, sseq::Adams> { - let p = self.prime(); - - let mut sseq = self.underlying.to_sseq(); - - let mut source_vec = FpVector::new(p, 0); - let mut target_vec = FpVector::new(p, 0); - - for b in self.underlying.iter_stem() { - if b.t() > 0 - && self - .underlying - .has_computed_bidegree(b + Bidegree::n_s(-1, 2)) - { - let m = self.homotopy(b.s() + 2).homotopies.hom_k(b.t()); - if m.is_empty() || m[0].is_empty() { - continue; - } - - source_vec.set_scratch_vector_size(m.len()); - target_vec.set_scratch_vector_size(m[0].len()); - - for (i, row) in m.into_iter().enumerate() { - source_vec.set_to_zero(); - source_vec.set_entry(i, 1); - target_vec.copy_from_slice(&row); - - let source = BidegreeElement::new(b, source_vec); - sseq.add_differential(2, &source, target_vec.as_slice()); - - source_vec = source.into_vec(); - } - } - } - - for b in self.underlying.iter_stem() { - if sseq.invalid(b) { - sseq.update_degree(b); - } - } - sseq - } -} - -// Rustdoc ICE's when trying to document this struct. See -// https://github.com/rust-lang/rust/issues/91380 -#[doc(hidden)] -pub struct SecondaryResolutionHomomorphism< - CC1: FreeChainComplex, - CC2: FreeChainComplex, -> where - CC1::Algebra: PairAlgebra, -{ - source: Arc>, - target: Arc>, - underlying: Arc>, - /// input s -> homotopy - homotopies: OnceBiVec>, - intermediates: DashMap, -} - -impl> SecondaryLift - for SecondaryResolutionHomomorphism -where - CC1::Algebra: PairAlgebra, -{ - type Algebra = CC1::Algebra; - type Source = CC1; - type Target = CC2; - type Underlying = ResolutionHomomorphism; - - fn underlying(&self) -> Arc { - Arc::clone(&self.underlying) - } - - fn algebra(&self) -> Arc { - self.source.algebra() - } - - fn source(&self) -> Arc { - Arc::clone(&self.source.underlying) - } - - fn target(&self) -> Arc { - Arc::clone(&self.target.underlying) - } - - fn shift(&self) -> Bidegree { - self.underlying.shift + Bidegree::s_t(1, 0) - } - - fn max(&self) -> BidegreeRange<'_, Self> { - BidegreeRange::new( - self, - self.underlying.next_homological_degree(), - &|selff, s| { - std::cmp::min( - selff.underlying.get_map(s).next_degree(), - std::cmp::min( - selff.source.homotopies[s].homotopies.next_degree(), - if s == selff.shift().s() { - i32::MAX - } else { - selff.target.homotopies[s + 1 - selff.shift().s()] - .composites - .max_degree() - + selff.shift().t() - + 1 - }, - ), - ) - }, - ) - } - - fn homotopies(&self) -> &OnceBiVec> { - &self.homotopies - } - - fn intermediates(&self) -> &DashMap { - &self.intermediates - } - - fn save_dir(&self) -> &SaveDirectory { - self.underlying.save_dir() - } - - fn composite(&self, s: i32) -> CompositeData { - let p = self.prime(); - // This is -1 mod p^2 - let neg_1 = p * p - 1; - - let d_source = self.source.underlying.differential(s); - let d_target = self - .target - .underlying - .differential(s + 1 - self.shift().s()); - - let c1 = self.underlying.get_map(s); - let c0 = self.underlying.get_map(s - 1); - - vec![(neg_1, d_source, c0), (1, c1, d_target)] - } - - fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { - let p = self.prime(); - let neg_1 = p - 1; - let shifted_b = g.degree() - self.shift(); - let target = self.target().module(shifted_b.s() - 1); - - let mut result = FpVector::new(p, target.dimension(shifted_b.t() - 1)); - let d = self.source().differential(g.s()); - - self.homotopies[g.s() - 1].act( - result.as_slice_mut(), - neg_1, - g.t(), - d.output(g.t(), g.idx()).as_slice(), - false, - ); - self.target.homotopy(shifted_b.s() + 1).act( - result.as_slice_mut(), - neg_1, - shifted_b.t(), - self.underlying - .get_map(g.s()) - .output(g.t(), g.idx()) - .as_slice(), - true, - ); - self.underlying.get_map(g.s() - 2).apply( - result.as_slice_mut(), - 1, - g.t() - 1, - self.source - .homotopy(g.s()) - .homotopies - .output(g.t(), g.idx()) - .as_slice(), - ); - - result - } -} - -impl> - SecondaryResolutionHomomorphism -where - CC1::Algebra: PairAlgebra, -{ - pub fn new( - source: Arc>, - target: Arc>, - underlying: Arc>, - ) -> Self { - assert!(Arc::ptr_eq(&underlying.source, &source.underlying)); - assert!(Arc::ptr_eq(&underlying.target, &target.underlying)); - - if let Some(p) = underlying.save_dir().write() { - for subdir in SaveKind::secondary_data() { - subdir.create_dir(p).unwrap(); - } - } - - Self { - source, - target, - homotopies: OnceBiVec::new(underlying.shift.s() + 1), - underlying, - intermediates: DashMap::new(), - } - } - - pub fn name(&self) -> String { - let name = self.underlying.name(); - if name.starts_with('[') || name.starts_with('λ') { - name.to_owned() - } else { - format!("[{name}]") - } - } - - pub fn homotopy(&self, s: i32) -> &SecondaryHomotopy { - &self.homotopies[s] - } - - /// A version of [`hom_k`] but with a non-trivial λ part. - pub fn hom_k_with<'a>( - &self, - lambda_part: Option<&ResolutionHomomorphism>, - sseq: Option<&sseq::Sseq<2, sseq::Adams>>, - b: Bidegree, - inputs: impl Iterator>, - outputs: impl Iterator>, - ) { - let source = b + self.shift() - Bidegree::s_t(1, 0); - let lambda_source = source + LAMBDA_BIDEGREE; - - let p = self.prime(); - let h_0 = self.algebra().p_tilde(); - - let source_num_gens = self.source().number_of_gens_in_bidegree(source); - let lambda_num_gens = self.source().number_of_gens_in_bidegree(lambda_source); - - let m0 = self.underlying.get_map(source.s()).hom_k(b.t()); - let mut m1 = Matrix::from_vec(p, &self.homotopy(lambda_source.s()).homotopies.hom_k(b.t())); - if let Some(lambda_part) = lambda_part { - m1 += &Matrix::from_vec(p, &lambda_part.get_map(lambda_source.s()).hom_k(b.t())); - } - - // The multiplication by p map - let mp = Matrix::from_vec( - p, - &self - .source() - .filtration_one_product(1, h_0, source) - .unwrap(), - ); - - let sign = if (self.underlying.shift.s() * b.t()) % 2 == 1 { - p * p - 1 - } else { - 1 - }; - let filtration_one_sign = if (b.t() % 2) == 1 { p - 1 } else { 1 }; - - let page_data = sseq.map(|sseq| { - let d = sseq.page_data(lambda_source); - &d[std::cmp::min(3, d.len() - 1)] - }); - - let mut scratch0: Vec = Vec::new(); - for (input, mut out) in inputs.zip_eq(outputs) { - scratch0.clear(); - scratch0.resize(source_num_gens, 0); - for (i, v) in input.iter_nonzero() { - scratch0 - .iter_mut() - .zip_eq(&m0[i]) - .for_each(|(a, b)| *a += v * b * sign); - out.slice_mut(source_num_gens, source_num_gens + lambda_num_gens) - .add(m1.row(i), (v * sign) % p); - } - for (i, v) in scratch0.iter().enumerate() { - out.add_basis_element(i, *v % p); - - let extra = *v / p; - out.slice_mut(source_num_gens, source_num_gens + lambda_num_gens) - .add(mp.row(i), (extra * filtration_one_sign) % p); - } - if let Some(page_data) = page_data { - page_data.reduce_by_quotient( - out.slice_mut(source_num_gens, source_num_gens + lambda_num_gens), - ); - } - } - } - - /// Compute the induced map on Mod_{C\lambda^2} homotopy groups. This only computes it on - /// standard lifts on elements in Ext. `outputs` is an iterator of `FpSliceMut`s whose lengths - /// are equal to the total dimension of `(s + shift_s, t + shift_t)` and `(s + shift_s + 1, t + - /// shift_t + 1)`. The first chunk records the Ext part of the result, and the second chunk - /// records the λ part of the result. - /// - /// This reduces the λ part of the result by the image of d₂. - /// - /// # Arguments - /// - `sseq`: A sseq object that records the $d_2$ differentials. If present, reduce the value - /// of the map by the image of $d_2$. - pub fn hom_k<'a>( - &self, - sseq: Option<&sseq::Sseq<2, sseq::Adams>>, - b: Bidegree, - inputs: impl Iterator>, - outputs: impl Iterator>, - ) { - self.hom_k_with(None, sseq, b, inputs, outputs); - } - - /// Given an element b whose product with this is null, find the element whose $d_2$ hits the - /// λ part of the composition. - /// - /// # Arguments: - /// - `sseq`: spectral sequence object of the source - pub fn product_nullhomotopy( - &self, - lambda_part: Option<&ResolutionHomomorphism>, - sseq: &sseq::Sseq<2, sseq::Adams>, - b: Bidegree, - class: FpSlice, - ) -> FpVector { - let p = self.prime(); - let shift = self.underlying.shift; - - let result_num_gens = self - .source() - .number_of_gens_in_bidegree(shift + b - Bidegree::s_t(1, 0)); - - let lambda_num_gens = self - .source() - .number_of_gens_in_bidegree(b + shift + LAMBDA_BIDEGREE); - - let lower_num_gens = self.source().number_of_gens_in_bidegree(b + shift); - - let target_num_gens = self.target().number_of_gens_in_bidegree(b); - let target_lambda_num_gens = self - .target() - .number_of_gens_in_bidegree(b + LAMBDA_BIDEGREE); - - let mut output_class = FpVector::new(p, result_num_gens); - if result_num_gens == 0 || lambda_num_gens == 0 { - return output_class; - } - - let mut prod_value = FpVector::new(p, lower_num_gens + lambda_num_gens); - self.hom_k_with( - lambda_part, - None, - b, - [class.restrict(0, target_num_gens)].into_iter(), - [prod_value.as_slice_mut()].into_iter(), - ); - assert!(prod_value.slice(0, lower_num_gens).is_zero()); - - let matrix = Matrix::from_vec( - p, - &self - .underlying - .get_map((b + shift + LAMBDA_BIDEGREE).s()) - .hom_k((b + LAMBDA_BIDEGREE).t()), - ); - matrix.apply( - prod_value.slice_mut(lower_num_gens, lower_num_gens + lambda_num_gens), - 1, - class.restrict(target_num_gens, target_num_gens + target_lambda_num_gens), - ); - - let diff_source = b + shift - Bidegree::n_s(-1, 1); - sseq.differentials(diff_source)[2].quasi_inverse( - output_class.as_slice_mut(), - prod_value.slice(lower_num_gens, lower_num_gens + lambda_num_gens), - ); - - output_class - } -} - -#[doc(hidden)] -pub struct SecondaryChainHomotopy< - S: FreeChainComplex, - T: FreeChainComplex + Sync, - U: FreeChainComplex + Sync, -> where - S::Algebra: PairAlgebra, -{ - underlying: Arc>, - left: Arc>, - right: Arc>, - left_lambda: Option>>, - right_lambda: Option>>, - homotopies: OnceBiVec>, - intermediates: DashMap, -} - -impl< - S: FreeChainComplex, - T: FreeChainComplex + Sync, - U: FreeChainComplex + Sync, -> SecondaryLift for SecondaryChainHomotopy -where - S::Algebra: PairAlgebra, -{ - type Algebra = S::Algebra; - type Source = S; - type Target = U; - type Underlying = ChainHomotopy; - - const HIT_GENERATOR: bool = true; - - fn underlying(&self) -> Arc { - Arc::clone(&self.underlying) - } - - fn algebra(&self) -> Arc { - self.left.algebra() - } - - fn source(&self) -> Arc { - self.left.source() - } - - fn target(&self) -> Arc { - self.right.target() - } - - fn shift(&self) -> Bidegree { - Bidegree::s_t( - self.underlying.shift().s(), - self.left.shift().t() + self.right.shift().t(), - ) - } - - fn max(&self) -> BidegreeRange<'_, Self> { - BidegreeRange::new( - self, - std::cmp::min( - self.right.target.max().s() + self.shift().s() - 1, - self.left.source.max().s(), - ), - &|selff, s| { - std::cmp::min( - selff.left.source.max().t(s), - if s == selff.shift().s() { - i32::MAX - } else { - selff.right.target.max().t(s - selff.shift().s() + 1) + selff.shift().t() - }, - ) - }, - ) - } - - fn homotopies(&self) -> &OnceBiVec> { - &self.homotopies - } - - fn intermediates(&self) -> &DashMap { - &self.intermediates - } - - fn save_dir(&self) -> &SaveDirectory { - self.underlying.save_dir() - } - - fn compute_intermediate(&self, g: BidegreeGenerator) -> FpVector { - let p = self.prime(); - let neg_1 = p - 1; - let shifted_b = g.degree() - self.shift(); - - let target = self.target().module(shifted_b.s() - 1); - - let mut result = FpVector::new(p, target.dimension(shifted_b.t() - 1)); - - self.homotopies[g.s() - 1].act( - result.as_slice_mut(), - 1, - g.t(), - self.source() - .differential(g.s()) - .output(g.t(), g.idx()) - .as_slice(), - false, - ); - - self.right.target.homotopies()[shifted_b.s() + 1].act( - result.as_slice_mut(), - 1, - shifted_b.t(), - self.underlying - .homotopy(g.s()) - .output(g.t(), g.idx()) - .as_slice(), - true, - ); - - self.underlying.homotopy(g.s() - 2).apply( - result.as_slice_mut(), - neg_1, - g.t() - 1, - self.left.source.homotopies()[g.s()] - .homotopies - .output(g.t(), g.idx()) - .as_slice(), - ); - - let left_shifted_b = g.degree() - self.left.underlying.shift; - self.right.homotopies()[left_shifted_b.s()].act( - result.as_slice_mut(), - neg_1, - left_shifted_b.t(), - self.left - .underlying - .get_map(g.s()) - .output(g.t(), g.idx()) - .as_slice(), - true, - ); - - // This is inefficient if both right_lambda and right are non-zero, but this is not needed atm - // and the change would not be user-facing. - if let Some(right_lambda) = &self.right_lambda { - right_lambda.get_map(left_shifted_b.s()).apply( - result.as_slice_mut(), - neg_1, - left_shifted_b.t(), - self.left - .underlying - .get_map(g.s()) - .output(g.t(), g.idx()) - .as_slice(), - ); - } - - self.right.underlying.get_map(left_shifted_b.s() - 1).apply( - result.as_slice_mut(), - neg_1, - left_shifted_b.t() - 1, - self.left.homotopies()[g.s()] - .homotopies - .output(g.t(), g.idx()) - .as_slice(), - ); - - if let Some(left_lambda) = &self.left_lambda { - self.right.underlying.get_map(left_shifted_b.s() - 1).apply( - result.as_slice_mut(), - neg_1, - left_shifted_b.t() - 1, - left_lambda.get_map(g.s()).output(g.t(), g.idx()).as_slice(), - ); - } - result - } - - fn composite(&self, s: i32) -> CompositeData { - let p = self.prime(); - // This is -1 mod p^2 - let neg_1 = p * p - 1; - - vec![ - ( - neg_1, - self.underlying.left().get_map(s), - self.underlying - .right() - .get_map(s - self.left.underlying.shift.s()), - ), - ( - 1, - self.underlying.homotopy(s), - self.target().differential(s - self.shift().s() + 1), - ), - ( - 1, - self.source().differential(s), - self.underlying.homotopy(s - 1), - ), - ] - } -} - -impl< - S: FreeChainComplex, - T: FreeChainComplex + Sync, - U: FreeChainComplex + Sync, -> SecondaryChainHomotopy -where - S::Algebra: PairAlgebra, -{ - pub fn new( - left: Arc>, - right: Arc>, - left_lambda: Option>>, - right_lambda: Option>>, - underlying: Arc>, - ) -> Self { - assert!(Arc::ptr_eq(&underlying.left(), &left.underlying)); - assert!(Arc::ptr_eq(&underlying.right(), &right.underlying)); - - if let Some(left_lambda) = &left_lambda { - assert!(Arc::ptr_eq(&left_lambda.source, &underlying.left().source)); - assert!(Arc::ptr_eq(&left_lambda.target, &underlying.left().target)); - - assert_eq!(left_lambda.shift, underlying.left().shift + LAMBDA_BIDEGREE); - } - - if let Some(right_lambda) = &right_lambda { - assert!(Arc::ptr_eq( - &right_lambda.source, - &underlying.right().source - )); - assert!(Arc::ptr_eq( - &right_lambda.target, - &underlying.right().target - )); - - assert_eq!( - right_lambda.shift, - underlying.right().shift + LAMBDA_BIDEGREE - ); - } - - if let Some(p) = underlying.save_dir().write() { - for subdir in SaveKind::secondary_data() { - subdir.create_dir(p).unwrap(); - } - } - - Self { - left, - right, - left_lambda, - right_lambda, - homotopies: OnceBiVec::new(underlying.shift().s()), - underlying, - intermediates: DashMap::new(), - } - } -} - #[cfg(test)] mod tests { use serde_json::json;