mahowald_invariant/
mahowald_invariant.rs

1//! Computes algebraic Mahowald invariants (aka algebraic root invariants).
2//!
3//! Sample output (with `Max k = 7`):
4//! ```
5//! M({basis element}) = {mahowald_invariant}[ mod {indeterminacy}]
6//! M(x_(0, 0, 0)) = x_(0, 0, 0)
7//! M(x_(1, 1, 0)) = x_(1, 2, 0)
8//! M(x_(2, 2, 0)) = x_(2, 4, 0)
9//! M(x_(1, 2, 0)) = x_(1, 4, 0)
10//! M(x_(3, 3, 0)) = x_(3, 6, 0)
11//! M(x_(2, 4, 0)) = x_(2, 8, 0)
12//! M(x_(1, 4, 0)) = x_(1, 8, 0)
13//! M(x_(2, 5, 0)) = x_(2, 10, 0)
14//! M(x_(3, 6, 0)) = x_(3, 12, 0)
15//! ```
16//!
17//! ---
18//!
19//! Here is a brief overview of what this example computes.
20//! For details and beyond, see for instance
21//! "[The root invariant in homotopy theory][mahowald--ravenel]" or
22//! "[The Bredon-Löffler conjecture][bruner--greenlees]" (where the latter also contains machine
23//! computations similar to what this example does).
24//! In the following, we abbreviate `Ext^{s,t}_A(-, F_2)` as `Ext^{s,t}(-)`.
25//!
26//! Let `M_k` be the cohomology of `RP_-k_inf`.
27//! There is an isomorphism  `Ext^{s, t}(F_2) ~ lim_k Ext^{s, t-1}(M_k)`
28//! induced by the (-1)-cell `S^{-1} -> RP_-k_inf` at each level.
29//! Let `x` be a class in `Ext^{s, t}(F_2)`.
30//! Then there is a minimal `k` such that its image in `Ext^{s, t-1}(M_k)` is non-trivial.
31//! Using the long exact sequence induced by the (co)fiber sequence
32//! `S^{-k} -> RP_-k_inf -> RP_{-k+1}_inf` on the level of `Ext`, that image can be lifted to a
33//! class `M(x)` in `Ext^{s, t + k - 1}`, which is (a representative for) the *(algebraic) Mahowald
34//! invariant of `x`*.
35//!
36//! This script computes these lifts (and their indeterminacy) by resolving
37//! `F_2` resp. `M_k`s and constructing [`ResolutionHomomorphism`]s
38//! corresponding to the bottom and (-1)-cells.
39//! Given `Max k`, it will print Mahowald invariants of the `F_2`-basis elements of
40//! `Ext^{*,*}(F_2)` that are detected in `Ext^{*,*}(M_k)` for the first time for some
41//! `k <= Max k`.
42//!
43//! [mahowald--ravenel]: https://www.sciencedirect.com/science/article/pii/004093839390055Z
44//! [bruner--greenlees]: https://projecteuclid.org/journals/experimental-mathematics/volume-4/issue-4/The-Bredon-L%C3%B6ffler-conjecture/em/1047674389.full
45
46use std::{fmt, iter, num::NonZeroI32, path::PathBuf, sync::Arc};
47
48use algebra::{
49    AlgebraType, SteenrodAlgebra,
50    module::{Module, homomorphism::ModuleHomomorphism},
51};
52use anyhow::Result;
53use ext::{
54    chain_complex::{ChainComplex, FiniteChainComplex, FreeChainComplex},
55    resolution::MuResolution,
56    resolution_homomorphism::{MuResolutionHomomorphism, ResolutionHomomorphism},
57    utils,
58};
59use fp::{
60    matrix::Matrix,
61    prime::TWO,
62    vector::{FpSlice, FpVector},
63};
64use serde_json::json;
65use sseq::coordinates::{Bidegree, BidegreeElement, BidegreeGenerator};
66
67fn main() -> Result<()> {
68    ext::utils::init_logging()?;
69
70    let s_2_path: Option<PathBuf> = query::optional("Save directory for S_2", str::parse);
71    let p_k_prefix: Option<PathBuf> = query::optional(
72        "Directory containing save directories for RP_-k_inf's",
73        str::parse,
74    );
75    // Going up to k=25 is nice because then we see an invariant that is not a basis element
76    // and one that has non-trivial indeterminacy.
77    let k_max = query::with_default("Max k (positive)", "25", str::parse::<NonZeroI32>).get();
78
79    let s_2_resolution = resolve_s_2(s_2_path, k_max)?;
80
81    println!("M({{basis element}}) = {{mahowald_invariant}}[ mod {{indeterminacy}}]");
82    for k in 1..=k_max {
83        let p_k = PKData::try_new(k, &p_k_prefix, &s_2_resolution)?;
84        for mi in p_k.mahowald_invariants() {
85            println!("{mi}")
86        }
87    }
88
89    Ok(())
90}
91
92type Resolution =
93    MuResolution<false, FiniteChainComplex<Box<dyn Module<Algebra = SteenrodAlgebra>>>>;
94
95type Homomorphism = MuResolutionHomomorphism<false, Resolution, Resolution>;
96
97struct PKData {
98    k: i32,
99    resolution: Arc<Resolution>,
100    bottom_cell: Homomorphism,
101    minus_one_cell: Homomorphism,
102    s_2_resolution: Arc<Resolution>,
103}
104
105struct MahowaldInvariant {
106    g: BidegreeGenerator,
107    output_t: i32,
108    invariant: FpVector,
109    indeterminacy_basis: Vec<FpVector>,
110}
111
112fn resolve_s_2(s_2_path: Option<PathBuf>, k_max: i32) -> Result<Arc<Resolution>> {
113    let s_2_resolution = Arc::new(utils::construct_standard("S_2", s_2_path)?);
114    // Here are some bounds on the bidegrees in which we have should have resolutions available.
115    //
116    // A class in stem n won't be detected before RP_-{n+1}_inf, so we can only detect Mahowald
117    // invariants of classes in stems <=k_max-1.
118    // If an element in stem k_max-1 is detected in RP_-{k_max}_inf, then its Mahowald invariant
119    // will be in stem 2*k_max-2, so we should resolve S_2 up to that stem.
120    //
121    // As for the filtration s, resolving up to (k/2)+1 will cover all classes in positive stems up
122    // to k-1 because of the Adams vanishing line.
123    // In the zero stem, the Mahowald invariant of x_(i, i, 0) (i.e. (h_0)^i) is the first element
124    // of filtration i that is in a positive stem.
125    // As that element appears by stem 2*i, resolving RP_-k_inf up to filtration (k/2)+1 is also
126    // sufficient to detect Mahowald invariants of elements in the zero stem.
127    s_2_resolution.compute_through_stem(Bidegree::n_s(2 * k_max - 2, k_max / 2 + 1));
128    Ok(s_2_resolution)
129}
130
131impl PKData {
132    fn try_new(
133        k: i32,
134        p_k_prefix: &Option<PathBuf>,
135        s_2_resolution: &Arc<Resolution>,
136    ) -> Result<Self> {
137        let p_k_config = json! ({
138            "p": 2,
139            "type": "real projective space",
140            "min": -k,
141        });
142        let mut p_k_path = p_k_prefix.clone();
143        if let Some(p) = p_k_path.as_mut() {
144            p.push(PathBuf::from(&format!("RP_{minus_k}_inf", minus_k = -k)));
145        };
146        let resolution = Arc::new(utils::construct_standard(
147            (p_k_config, AlgebraType::Milnor),
148            p_k_path,
149        )?);
150        // As mentioned before, RP_-k_inf won't detect Mahowald invariants of any classes in the
151        // k-stem and beyond or of any classes of filtration higher than k/2+1.
152        resolution.compute_through_stem(Bidegree::n_s(k - 2, k / 2 + 1));
153
154        let bottom_cell = ResolutionHomomorphism::from_class(
155            String::from("bottom_cell"),
156            resolution.clone(),
157            s_2_resolution.clone(),
158            Bidegree::s_t(0, -k),
159            &[1],
160        );
161        bottom_cell.extend_all();
162
163        let minus_one_cell = ResolutionHomomorphism::from_class(
164            String::from("minus_one_cell"),
165            resolution.clone(),
166            s_2_resolution.clone(),
167            Bidegree::s_t(0, -1),
168            &[1],
169        );
170        minus_one_cell.extend_all();
171
172        Ok(PKData {
173            k,
174            resolution,
175            bottom_cell,
176            minus_one_cell,
177            s_2_resolution: s_2_resolution.clone(),
178        })
179    }
180
181    fn mahowald_invariants(&self) -> impl Iterator<Item = MahowaldInvariant> + '_ {
182        self.s_2_resolution
183            .iter_stem()
184            .flat_map(|b| self.mahowald_invariants_for_bidegree(b))
185    }
186
187    fn mahowald_invariants_for_bidegree(
188        &self,
189        b: Bidegree,
190    ) -> Box<dyn Iterator<Item = MahowaldInvariant> + '_> {
191        let b_p_k = b - Bidegree::s_t(0, 1);
192        if self.resolution.has_computed_bidegree(b_p_k) {
193            let b_bottom = b_p_k + Bidegree::s_t(0, self.k);
194            let bottom_s_2_gens = self.s_2_resolution.number_of_gens_in_bidegree(b_bottom);
195            let minus_one_s_2_gens = self.s_2_resolution.number_of_gens_in_bidegree(b);
196            let p_k_gens = self.resolution.number_of_gens_in_bidegree(b_p_k);
197            if bottom_s_2_gens > 0 && minus_one_s_2_gens > 0 && p_k_gens > 0 {
198                let bottom_cell_map = self.bottom_cell.get_map(b_bottom.s());
199                let mut matrix = vec![vec![0; p_k_gens]; bottom_s_2_gens];
200                for p_k_gen in 0..p_k_gens {
201                    let output = bottom_cell_map.output(b_p_k.t(), p_k_gen);
202                    for (s_2_gen, row) in matrix.iter_mut().enumerate() {
203                        let index = bottom_cell_map.target().operation_generator_to_index(
204                            0,
205                            0,
206                            b_bottom.t(),
207                            s_2_gen,
208                        );
209                        row[p_k_gen] = output.entry(index);
210                    }
211                }
212                let (padded_columns, mut matrix) = Matrix::augmented_from_vec(TWO, &matrix);
213                let rank = matrix.row_reduce();
214
215                if rank > 0 {
216                    let kernel_subspace = matrix.compute_kernel(padded_columns);
217                    let indeterminacy_basis: Vec<FpVector> =
218                        kernel_subspace.basis().map(FpSlice::to_owned).collect();
219                    let image_subspace = matrix.compute_image(p_k_gens, padded_columns);
220                    let quasi_inverse = matrix.compute_quasi_inverse(p_k_gens, padded_columns);
221
222                    let it = (0..minus_one_s_2_gens).filter_map(move |i| {
223                        let mut image = FpVector::new(TWO, p_k_gens);
224                        let g = BidegreeGenerator::new(b, i);
225                        self.minus_one_cell.act(image.as_slice_mut(), 1, g);
226                        if !image.is_zero() && image_subspace.contains(image.as_slice()) {
227                            let mut invariant = FpVector::new(TWO, bottom_s_2_gens);
228                            quasi_inverse.apply(invariant.as_slice_mut(), 1, image.as_slice());
229                            Some(MahowaldInvariant {
230                                g,
231                                output_t: b_bottom.t(),
232                                invariant,
233                                indeterminacy_basis: indeterminacy_basis.clone(),
234                            })
235                        } else {
236                            None
237                        }
238                    });
239                    return Box::new(it);
240                }
241            }
242        }
243
244        Box::new(iter::empty())
245    }
246}
247
248impl fmt::Display for MahowaldInvariant {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
250        let output_t = self.output_t;
251        let f2_vec_to_sum = |v: &FpVector| {
252            let elt = BidegreeElement::new(Bidegree::s_t(self.g.s(), output_t), v.clone());
253            elt.to_basis_string()
254        };
255        let indeterminacy_info = if self.indeterminacy_basis.is_empty() {
256            String::new()
257        } else {
258            format!(
259                " mod <{inner}>",
260                inner = self
261                    .indeterminacy_basis
262                    .iter()
263                    .map(f2_vec_to_sum)
264                    .collect::<Vec<_>>()
265                    .join(", ")
266            )
267        };
268        let invariant = f2_vec_to_sum(&self.invariant);
269        write!(f, "M(x_{g}) = {invariant}{indeterminacy_info}", g = self.g)
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use rstest::rstest;
276
277    use super::*;
278
279    #[rstest]
280    #[case(1, 0, 0, 0, 0, vec![1], 0)]
281    #[case(5, 1, 4, 0, 8, vec![1], 0)]
282    #[case(18, 3, 17, 0, 34, vec![0, 1], 0)]
283    #[case(25, 6, 20, 0, 44, vec![1, 0], 1)]
284    fn test_mahowald_invariants(
285        #[case] k: i32,
286        #[case] s: i32,
287        #[case] input_t: i32,
288        #[case] input_i: usize,
289        #[case] output_t: i32,
290        #[case] invariant: Vec<u32>,
291        #[case] indeterminacy_dim: usize,
292    ) {
293        let g = BidegreeGenerator::new(Bidegree::s_t(s, input_t), input_i);
294        let s_2_resolution = resolve_s_2(None, k).unwrap();
295        let p_k = PKData::try_new(k, &None, &s_2_resolution).unwrap();
296        for mi in p_k.mahowald_invariants_for_bidegree(g.degree()) {
297            if mi.g.idx() == g.idx() {
298                assert_eq!(mi.output_t, output_t);
299                assert_eq!(Vec::from(&mi.invariant), invariant);
300                assert_eq!(mi.indeterminacy_basis.len(), indeterminacy_dim);
301                return;
302            }
303        }
304        panic!("could not find Mahowald invariant")
305    }
306}