ext/
yoneda.rs

1use std::sync::Arc;
2
3use algebra::{
4    AdemAlgebra, Algebra, GeneratedAlgebra, MilnorAlgebra, SteenrodAlgebra,
5    module::{
6        FDModule, FreeModule, Module, QuotientModule as QM,
7        homomorphism::{
8            FreeModuleHomomorphism, FullModuleHomomorphism, IdentityHomomorphism,
9            ModuleHomomorphism, QuotientHomomorphism, QuotientHomomorphismSource,
10        },
11    },
12};
13use bivec::BiVec;
14use fp::{
15    matrix::{AugmentedMatrix, Matrix, Subspace},
16    vector::FpVector,
17};
18use rustc_hash::FxHashSet as HashSet;
19use sseq::coordinates::Bidegree;
20
21use crate::{
22    chain_complex::{
23        AugmentedChainComplex, BoundedChainComplex, ChainComplex, ChainMap,
24        FiniteAugmentedChainComplex, FiniteChainComplex, FreeChainComplex,
25    },
26    resolution_homomorphism::ResolutionHomomorphism,
27};
28
29const PENALTY_UNIT: i32 = 10000;
30
31pub type Yoneda<CC> = FiniteAugmentedChainComplex<
32    FDModule<<CC as ChainComplex>::Algebra>,
33    FullModuleHomomorphism<FDModule<<CC as ChainComplex>::Algebra>>,
34    FullModuleHomomorphism<
35        FDModule<<CC as ChainComplex>::Algebra>,
36        <<CC as AugmentedChainComplex>::TargetComplex as ChainComplex>::Module,
37    >,
38    <CC as AugmentedChainComplex>::TargetComplex,
39>;
40
41fn rate_operation<A: Algebra>(algebra: &Arc<A>, op_deg: i32, op_idx: usize) -> i32 {
42    let algebra = &**algebra as &dyn std::any::Any;
43
44    if let Some(algebra) = algebra.downcast_ref::<SteenrodAlgebra>() {
45        match algebra {
46            SteenrodAlgebra::AdemAlgebra(a) => rate_adem_operation(a, op_deg, op_idx),
47            SteenrodAlgebra::MilnorAlgebra(a) => rate_milnor_operation(a, op_deg, op_idx),
48        }
49    } else if let Some(algebra) = algebra.downcast_ref::<MilnorAlgebra>() {
50        rate_milnor_operation(algebra, op_deg, op_idx)
51    } else if let Some(algebra) = algebra.downcast_ref::<AdemAlgebra>() {
52        rate_adem_operation(algebra, op_deg, op_idx)
53    } else {
54        0
55    }
56}
57
58fn rate_milnor_operation(algebra: &MilnorAlgebra, deg: i32, idx: usize) -> i32 {
59    let elt = algebra.basis_element_from_index(deg, idx);
60    elt.p_part
61        .iter()
62        .enumerate()
63        .map(|(i, &r)| r.count_ones() << i)
64        .sum::<u32>() as i32
65}
66
67fn rate_adem_operation(algebra: &AdemAlgebra, deg: i32, idx: usize) -> i32 {
68    let elt = algebra.basis_element_from_index(deg, idx);
69    elt.ps.iter().map(|&r| r.count_ones()).sum::<u32>() as i32
70}
71
72fn split_mut_borrow<T>(v: &mut [T], i: usize, j: usize) -> (&mut T, &mut T) {
73    assert!(i < j);
74    let (first, second) = v.split_at_mut(j);
75    (&mut first[i], &mut second[0])
76}
77
78pub fn yoneda_representative_element<CC>(cc: Arc<CC>, b: Bidegree, class: &[u32]) -> Yoneda<CC>
79where
80    CC: FreeChainComplex
81        + AugmentedChainComplex<
82            ChainMap = FreeModuleHomomorphism<
83                <<CC as AugmentedChainComplex>::TargetComplex as ChainComplex>::Module,
84            >,
85        >,
86    CC::TargetComplex: BoundedChainComplex,
87    CC::Algebra: GeneratedAlgebra,
88{
89    let p = cc.prime();
90
91    let target = FDModule::new(cc.algebra(), "".to_string(), BiVec::from_vec(0, vec![1]));
92    let map = FreeModuleHomomorphism::new(cc.module(b.s()), Arc::new(target), b.t());
93    let mut rows = vec![FpVector::new(p, 1); cc.number_of_gens_in_bidegree(b)];
94    for (&i, row) in std::iter::zip(class, &mut rows) {
95        row.set_entry(0, i);
96    }
97
98    map.add_generators_from_rows(b.t(), rows);
99
100    let cm = ChainMap {
101        s_shift: b.s(),
102        chain_maps: vec![map],
103    };
104    let yoneda = Arc::new(yoneda_representative(Arc::clone(&cc), cm));
105
106    // We now do some safety checks
107    let module = cc.target().module(0);
108
109    for t in cc.min_degree()..=b.t() {
110        assert_eq!(
111            yoneda.euler_characteristic(t),
112            module.dimension(t) as isize,
113            "Incorrect Euler characteristic at t = {t}",
114        );
115    }
116
117    let f = ResolutionHomomorphism::from_module_homomorphism(
118        "".to_string(),
119        Arc::clone(&cc),
120        Arc::clone(&yoneda),
121        &FullModuleHomomorphism::identity_homomorphism(module),
122    );
123
124    f.extend_through_stem(b);
125    let final_map = f.get_map(b.s());
126    for (i, &v) in class.iter().enumerate() {
127        assert_eq!(final_map.output(b.t(), i).len(), 1);
128        assert_eq!(final_map.output(b.t(), i).entry(0), v);
129    }
130
131    drop(f);
132    Arc::try_unwrap(yoneda).unwrap_or_else(|_| unreachable!())
133}
134
135/// This function produces a quasi-isomorphic quotient of `cc` (as an augmented chain complex) that `map` factors through
136pub fn yoneda_representative<CC>(
137    cc: Arc<CC>,
138    map: ChainMap<FreeModuleHomomorphism<impl Module<Algebra = CC::Algebra>>>,
139) -> Yoneda<CC>
140where
141    CC: FreeChainComplex
142        + AugmentedChainComplex<
143            ChainMap = FreeModuleHomomorphism<
144                <<CC as AugmentedChainComplex>::TargetComplex as ChainComplex>::Module,
145            >,
146        >,
147    CC::TargetComplex: BoundedChainComplex,
148    CC::Algebra: GeneratedAlgebra,
149{
150    yoneda_representative_with_strategy(
151        cc,
152        map,
153        |module: &FreeModule<CC::Algebra>, subspace: &Subspace, t: i32, i: usize| {
154            let opgen = module.index_to_op_gen(t, i);
155
156            let mut pref = rate_operation(
157                &module.algebra(),
158                opgen.operation_degree,
159                opgen.operation_index,
160            );
161
162            for row in subspace.iter() {
163                if row.entry(i) != 0 {
164                    pref += PENALTY_UNIT;
165                }
166            }
167            pref
168        },
169    )
170}
171
172#[tracing::instrument(skip_all)]
173pub fn yoneda_representative_with_strategy<CC>(
174    cc: Arc<CC>,
175    map: ChainMap<FreeModuleHomomorphism<impl Module<Algebra = CC::Algebra>>>,
176    strategy: impl Fn(&CC::Module, &Subspace, i32, usize) -> i32,
177) -> Yoneda<CC>
178where
179    CC: FreeChainComplex
180        + AugmentedChainComplex<
181            ChainMap = FreeModuleHomomorphism<
182                <<CC as AugmentedChainComplex>::TargetComplex as ChainComplex>::Module,
183            >,
184        >,
185    CC::TargetComplex: BoundedChainComplex,
186    CC::Algebra: GeneratedAlgebra,
187{
188    let p = cc.prime();
189    let target_cc = cc.target();
190    let algebra = cc.algebra();
191
192    let t_shift: i32 = map.chain_maps[0].degree_shift();
193    let s_shift: i32 = map.s_shift;
194
195    let s_max = std::cmp::max(target_cc.max_s(), map.s_shift + map.chain_maps.len() as i32) - 1;
196
197    let t_max = {
198        // The maximum t required by the augmentation maps
199        let t_max_aug: Vec<i32> = (0..=s_max)
200            .map(|s| {
201                let mut t_max = cc.min_degree();
202                if s < target_cc.max_s() {
203                    t_max = std::cmp::max(t_max, target_cc.module(s).max_degree().unwrap())
204                }
205                if s >= map.s_shift
206                    && let Some(f) = map.chain_maps.get((s - map.s_shift) as usize)
207                {
208                    t_max =
209                        std::cmp::max(t_max, f.degree_shift() + f.target().max_degree().unwrap());
210                }
211                t_max
212            })
213            .collect();
214
215        let mut t_max = vec![cc.min_degree(); s_max as usize + 1];
216        for s in (0..=s_max as usize).rev() {
217            t_max[s] = t_max_aug[s];
218            if s < s_max as usize {
219                // the differential of the classes required to exist by the augmentation
220                t_max[s] = std::cmp::max(t_max[s], t_max_aug[s + 1]);
221
222                // The rest of the contents of t_max[s + 1] arise from the images of differentials,
223                // which have zero differential. So we can subtract one.
224                t_max[s] = std::cmp::max(t_max[s], t_max[s + 1] - 1);
225            }
226        }
227        t_max
228    };
229
230    let t_min = cc.min_degree();
231
232    let mut modules = (0..=s_max)
233        .map(|s| QM::new(cc.module(s), t_max[s as usize]))
234        .collect::<Vec<_>>();
235
236    for s in (1..=s_max).rev() {
237        let span = tracing::info_span!("Cleaning yoneda representative", s);
238        let _tracing_guard = span.enter();
239        let t_max = t_max[s as usize];
240        let mut differential_images: BiVec<Subspace> = {
241            let mut result = BiVec::new(t_min);
242
243            if s < s_max {
244                let prev = &modules[s as usize + 1];
245                let curr = &modules[s as usize];
246                let d = cc.differential(s + 1);
247
248                result.extend_with(t_max, |t| {
249                    let mut differentials =
250                        Matrix::new(p, prev.dimension(t), curr.module.dimension(t));
251
252                    for (i, mut row) in differentials.iter_mut().enumerate() {
253                        let j = prev.basis_list[t][i];
254                        d.apply_to_basis_element(row.copy(), 1, t, j);
255                        curr.reduce(t, row);
256                    }
257
258                    let mut result = Subspace::from_matrix(differentials);
259
260                    let dim = result.dimension();
261                    result.update_then_row_reduce(|result_matrix| result_matrix.trim(0, dim, 0));
262                    result
263                });
264            }
265            result
266        };
267
268        let (target, source) = split_mut_borrow(&mut modules, s as usize - 1, s as usize);
269        let d = cc.differential(s);
270
271        // This is used for sanity checking.
272        let dim_diff: BiVec<isize> = {
273            let mut dim_diff = BiVec::new(t_min);
274            dim_diff.extend_with(t_max, |t| {
275                source.dimension(t) as isize - target.dimension(t) as isize
276            });
277            dim_diff
278        };
279
280        macro_rules! check {
281            ($t:ident) => {
282                if $t <= target.truncation {
283                    assert_eq!(
284                        source.dimension($t) as isize - target.dimension($t) as isize,
285                        dim_diff[$t],
286                        "Failed dimension check at (s, t) = ({s}, {t})",
287                        t = $t
288                    );
289                }
290            };
291        }
292
293        // First, we try to kill off whole generators
294        if s < s_max {
295            let mut prev_differentials: BiVec<Option<Subspace>> =
296                BiVec::with_capacity(t_min, t_max + 1);
297            let mut prev_subspaces: BiVec<Option<Subspace>> =
298                BiVec::with_capacity(t_min, t_max + 1);
299            let mut prev_basis_list: BiVec<Option<Vec<usize>>> =
300                BiVec::with_capacity(t_min, t_max + 1);
301
302            for _ in t_min..=t_max {
303                prev_differentials.push(None);
304                prev_subspaces.push(None);
305                prev_basis_list.push(None);
306            }
307
308            // Collect for borrow checker purposes
309            let source_gens: Vec<_> = source.module.iter_gens(t_max).collect();
310            'gen_loop: for (gen_deg, gen_idx) in source_gens {
311                // Check if augmentation map is non-zero on the generator
312                if s < target_cc.max_s() && target_cc.module(s).dimension(gen_deg) > 0 {
313                    let m = cc.chain_map(s);
314                    if !m.output(gen_deg, gen_idx).is_zero() {
315                        continue 'gen_loop;
316                    }
317                }
318
319                // Check if preserve map is non-zero on the generator
320                if s >= s_shift
321                    && gen_deg >= t_shift
322                    && let Some(m) = map.chain_maps.get((s - s_shift) as usize)
323                    && m.target().dimension(gen_deg - t_shift) > 0
324                    && !m.output(gen_deg, gen_idx).is_zero()
325                {
326                    continue 'gen_loop;
327                }
328
329                for t in gen_deg..=t_max {
330                    let diff_im = &mut differential_images[t];
331                    if diff_im.is_empty() {
332                        continue;
333                    }
334
335                    prev_differentials[t] = Some(diff_im.clone());
336                    prev_subspaces[t] = Some(source.subspaces[t].clone());
337                    prev_basis_list[t] = Some(source.basis_list[t].clone());
338
339                    let start = source.module.generator_offset(t, gen_deg, gen_idx);
340                    let end = start + algebra.dimension(t - gen_deg);
341
342                    source.quotient_basis_elements(t, start..end);
343
344                    let needs_to_revert = diff_im.update_then_row_reduce(|diff_im_matrix| {
345                        for mut row in diff_im_matrix.iter_mut() {
346                            source.reduce(t, row.copy());
347                            if row.as_slice().is_zero() {
348                                return true;
349                            }
350                        }
351
352                        diff_im_matrix.row_reduce();
353                        if diff_im_matrix.row(diff_im_matrix.rows() - 1).is_zero() {
354                            return true;
355                        }
356                        false
357                    });
358
359                    if needs_to_revert {
360                        for t in gen_deg..=t {
361                            if prev_differentials[t].is_none() {
362                                continue;
363                            }
364                            differential_images[t] = prev_differentials[t].take().unwrap();
365                            source.subspaces[t] = prev_subspaces[t].take().unwrap();
366                            source.basis_list[t] = prev_basis_list[t].take().unwrap();
367                        }
368                        continue 'gen_loop;
369                    }
370                }
371
372                // We are free to clear this basis element. Do it
373                for t in gen_deg..=t_max {
374                    let start = source.module.generator_offset(t, gen_deg, gen_idx);
375                    let end = start + algebra.dimension(t - gen_deg);
376
377                    if prev_differentials[t].is_none() {
378                        // We previously skipped this because there were no differentials to check
379                        source.quotient_basis_elements(t, start..end);
380                    } else {
381                        prev_differentials[t] = None;
382                        prev_subspaces[t] = None;
383                        prev_basis_list[t] = None;
384                    }
385
386                    let mut indices = start..end;
387                    target.quotient_vectors(t, |row| {
388                        d.apply_to_basis_element(row, 1, t, indices.next()?);
389                        Some(())
390                    });
391                    check!(t);
392                }
393            }
394        }
395
396        // Now we clean up the module degree by degree from above.
397        for t in (t_min..=t_max).rev() {
398            if t - s < cc.min_degree() {
399                continue;
400            }
401            if source.dimension(t) == 0 {
402                continue;
403            }
404
405            let keep: Option<&Subspace> = if s < s_max {
406                Some(&differential_images[t])
407            } else {
408                None
409            };
410
411            let augmentation_map = if s < target_cc.max_s() && target_cc.module(s).dimension(t) > 0
412            {
413                Some(cc.chain_map(s))
414            } else {
415                None
416            };
417            let preserve_map = if s >= s_shift && t >= t_shift {
418                map.chain_maps.get((s - s_shift) as usize).and_then(|m| {
419                    if m.target().dimension(t - t_shift) > 0 {
420                        Some(m)
421                    } else {
422                        None
423                    }
424                })
425            } else {
426                None
427            };
428
429            // We can only quotient out by things in the kernel of the augmentation maps *and* the
430            // steenrod operations. Moreover, the space we quotient out must be complementary to
431            // the image of the differentials. The function computes the kernel and a list of
432            // elements that span the image of the differentials.
433            let (mut matrix, mut images) =
434                compute_kernel_image(source, augmentation_map.as_deref(), preserve_map, keep, t);
435
436            matrix.row_reduce();
437
438            let subspace = &source.subspaces[t];
439            let mut pivot_columns: Vec<(i32, usize)> = matrix
440                .pivots()
441                .iter()
442                .enumerate()
443                .filter_map(|(i, &v)| {
444                    if v >= 0 {
445                        Some((strategy(&source.module, subspace, t, i), i))
446                    } else {
447                        None
448                    }
449                })
450                .collect::<Vec<_>>();
451            let orig_pivot_columns: Vec<usize> = pivot_columns.iter().map(|&(_, i)| i).collect();
452
453            pivot_columns.sort_unstable();
454
455            let chosen_cols: HashSet<usize> = HashSet::from_iter(
456                images.find_pivots_permutation(pivot_columns.iter().map(|(_, i)| *i)),
457            );
458
459            let get_source_iter = || {
460                std::iter::zip(matrix.iter(), orig_pivot_columns.iter()).filter_map(|(row, col)| {
461                    if chosen_cols.contains(col) {
462                        None
463                    } else {
464                        Some(row)
465                    }
466                })
467            };
468            let mut source_iter = get_source_iter();
469            let mut source_iter2 = get_source_iter();
470
471            source.quotient_vectors(t, |mut row| {
472                row.add(source_iter.next()?, 1);
473                Some(())
474            });
475
476            target.quotient_vectors(t, |row| {
477                d.apply(row, 1, t, source_iter2.next()?);
478                Some(())
479            });
480
481            if s != s_max {
482                check!(t);
483            }
484        }
485    }
486
487    let modules = modules.into_iter().map(Arc::new).collect::<Vec<_>>();
488
489    let differentials: Vec<_> = (0..s_max)
490        .map(|s| {
491            Arc::new(FullModuleHomomorphism::from(&QuotientHomomorphism::new(
492                cc.differential(s + 1),
493                Arc::clone(&modules[s as usize + 1]),
494                Arc::clone(&modules[s as usize]),
495            )))
496        })
497        .collect();
498
499    let chain_maps = (0..=s_max)
500        .map(|s| {
501            Arc::new(FullModuleHomomorphism::from(
502                &QuotientHomomorphismSource::new(cc.chain_map(s), Arc::clone(&modules[s as usize])),
503            ))
504        })
505        .collect::<Vec<_>>();
506
507    let yoneda_rep =
508        FiniteChainComplex::new(modules, differentials).augment(cc.target(), chain_maps);
509
510    yoneda_rep.map(|m| FDModule::from(m))
511}
512
513/// This function does the following computation:
514///
515/// Given the source module `source` and a subspace `keep`, the function returns the subspace of all
516/// elements in `source` of degree `t` that are killed by all non-trivial actions of the algebra,
517/// followed by a list of elements that span the intersection between this subspace and `keep`.
518///
519/// If `keep` is `None`, it is interpreted as the empty subspace.
520fn compute_kernel_image<M: Module, F: ModuleHomomorphism, G: ModuleHomomorphism>(
521    source: &QM<M>,
522    augmentation_map: Option<&F>,
523    preserve_map: Option<&G>,
524    keep: Option<&Subspace>,
525    t: i32,
526) -> (Matrix, Matrix)
527where
528    M::Algebra: GeneratedAlgebra,
529{
530    let algebra = source.algebra();
531    let p = algebra.prime();
532
533    let mut generators: Vec<(i32, usize)> = Vec::new();
534    let mut target_dims = Vec::new();
535
536    let source_orig_dimension = source.module.dimension(t);
537    let source_dimension = source.dimension(t);
538
539    for op_deg in 1..=source.max_degree().unwrap() - t {
540        for op_idx in algebra.generators(op_deg) {
541            generators.push((op_deg, op_idx));
542            target_dims.push(source.module.dimension(t + op_deg));
543        }
544    }
545
546    if let Some(m) = &augmentation_map {
547        target_dims.push(m.target().dimension(t));
548    }
549
550    if let Some(m) = &preserve_map {
551        let dim = m.target().dimension(t - m.degree_shift());
552        target_dims.push(dim);
553    }
554
555    let total_dimension: usize = target_dims.iter().sum();
556    let mut matrix = AugmentedMatrix::new(
557        p,
558        source_dimension,
559        [
560            total_dimension + source_orig_dimension,
561            source_orig_dimension,
562        ],
563    );
564
565    for (row_idx, &i) in source.basis_list[t].iter().enumerate() {
566        let mut offset = 0;
567        let mut row = matrix.row_segment_mut(row_idx, 0, 0);
568
569        let mut cols = target_dims.iter().copied();
570        for (op_deg, op_idx) in &generators {
571            let len = cols.next().unwrap();
572            source.act_on_original_basis(
573                row.slice_mut(offset, offset + len),
574                1,
575                *op_deg,
576                *op_idx,
577                t,
578                i,
579            );
580            offset += len;
581        }
582
583        if let Some(m) = &augmentation_map {
584            let len = cols.next().unwrap();
585            m.apply_to_basis_element(row.slice_mut(offset, offset + len), 1, t, i);
586            offset += len;
587        }
588
589        if let Some(m) = &preserve_map {
590            let len = cols.next().unwrap();
591            m.apply_to_basis_element(row.slice_mut(offset, offset + len), 1, t, i);
592            offset += len;
593        }
594
595        let mut slice = row.slice_mut(offset, offset + source_orig_dimension);
596        slice.set_entry(i, 1);
597        if let Some(keep) = &keep {
598            keep.reduce(slice);
599        }
600
601        let mut row = matrix.row_segment_mut(row_idx, 1, 1);
602        row.set_entry(i, 1);
603    }
604    matrix.row_reduce();
605
606    let first_kernel_row = matrix.find_first_row_in_block(total_dimension);
607    let first_image_row = matrix.find_first_row_in_block(matrix.start[1]);
608    let image_rows = matrix.rows() - first_image_row;
609
610    let matrix = matrix.into_tail_segment(first_kernel_row, source_dimension, 1);
611
612    let mut image_matrix = Matrix::new(p, image_rows, source_orig_dimension);
613    for (mut target, source) in std::iter::zip(
614        image_matrix.iter_mut(),
615        matrix.iter().skip(first_image_row - first_kernel_row),
616    ) {
617        target.assign(source);
618    }
619    (matrix, image_matrix)
620}