Skip to main content

amari_enumerative/
csm.rs

1//! Chern-Schwartz-MacPherson (CSM) Classes
2//!
3//! Extends intersection theory to singular varieties via CSM classes.
4//! CSM classes generalize Chern classes to possibly singular spaces
5//! using the constructible function approach.
6//!
7//! # Key Ideas
8//!
9//! - For a smooth variety X, c\_SM(X) = c(TX) ∩ \[X\] (the usual total Chern class)
10//! - For singular X, CSM classes are defined via a natural transformation from
11//!   constructible functions to homology
12//! - On Grassmannians, CSM classes of Schubert cells expand in the Schubert basis
13//!
14//! # Contracts
15//!
16//! - CSM classes respect inclusion-exclusion
17//! - Euler characteristic = degree of CSM class (integration over ambient space)
18//! - For smooth Schubert varieties, CSM = Chern of tangent bundle
19
20use crate::littlewood_richardson::{lr_coefficient, schubert_product, Partition};
21use crate::namespace::Namespace;
22use std::collections::BTreeMap;
23
24/// CSM class of a constructible set, expanded in the Schubert basis of a Grassmannian.
25///
26/// ```text
27/// c_SM(Z) = Σ_λ a_λ · σ_λ
28/// ```
29///
30/// where a_λ are integer coefficients.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct CSMClass {
33    /// Expansion coefficients in the Schubert basis: partition → coefficient
34    pub coefficients: BTreeMap<Partition, i64>,
35    /// Grassmannian parameters
36    pub grassmannian: (usize, usize),
37}
38
39impl CSMClass {
40    /// CSM class of a Schubert cell Ω°_λ in Gr(k, n).
41    ///
42    /// For a Schubert cell (open stratum), the CSM class is computed via
43    /// inclusion-exclusion from the CSM classes of Schubert varieties:
44    ///
45    /// ```text
46    /// c_SM(Ω°_λ) = Σ_{μ ≤ λ} (-1)^{|μ|-|λ|} c_SM(Ω_μ)
47    /// ```
48    ///
49    /// For the top cell (empty partition), c\_SM = σ\_∅ = \[Gr\].
50    ///
51    /// # Contract
52    ///
53    /// ```text
54    /// requires: partition fits in k × (n-k) box
55    /// ensures: euler_characteristic() == 1 for any cell
56    /// ```
57    pub fn of_schubert_cell(partition: &[usize], grassmannian: (usize, usize)) -> Self {
58        let (_k, _n) = grassmannian;
59
60        if partition.is_empty() || partition.iter().all(|&p| p == 0) {
61            // Top cell: c_SM(Ω°_∅) = [Gr(k,n)]
62            let mut coefficients = BTreeMap::new();
63            coefficients.insert(Partition::empty(), 1);
64            return CSMClass {
65                coefficients,
66                grassmannian,
67            };
68        }
69
70        // For a general cell, use the formula:
71        // c_SM(Ω°_λ) = σ_λ + (correction terms from smaller cells)
72        //
73        // Simplified approach: the leading term is always σ_λ
74        let mut coefficients = BTreeMap::new();
75        let part = Partition::new(partition.to_vec());
76        coefficients.insert(part, 1);
77
78        // Add correction terms from the boundary structure
79        // For cells of codimension 1 less, subtract their CSM contributions
80        for i in 0..partition.len() {
81            if partition[i] > 0 {
82                let mut smaller = partition.to_vec();
83                smaller[i] -= 1;
84                let smaller_part = Partition::new(smaller);
85                if !smaller_part.parts.is_empty() {
86                    *coefficients.entry(smaller_part).or_insert(0) += 1;
87                }
88            }
89        }
90
91        CSMClass {
92            coefficients,
93            grassmannian,
94        }
95    }
96
97    /// CSM class of a Schubert variety Ω_λ (closure of the cell).
98    ///
99    /// ```text
100    /// c_SM(Ω_λ) = Σ_{μ ≥ λ} c_SM(Ω°_μ)
101    /// ```
102    ///
103    /// where the sum is over all partitions μ in the Bruhat order above λ.
104    ///
105    /// # Contract
106    ///
107    /// ```text
108    /// requires: partition fits in k × (n-k) box
109    /// ensures: result.coefficients is nonempty
110    /// ```
111    pub fn of_schubert_variety(partition: &[usize], grassmannian: (usize, usize)) -> Self {
112        let (k, n) = grassmannian;
113        let m = n - k;
114
115        // Sum CSM classes of all cells in the closure
116        let mut total = BTreeMap::new();
117
118        // The variety Ω_λ = ∪_{μ ≥ λ} Ω°_μ (Bruhat decomposition)
119        // For simplicity, we start with the cell itself and add boundary cells
120        let cell_csm = Self::of_schubert_cell(partition, grassmannian);
121        for (part, coeff) in &cell_csm.coefficients {
122            *total.entry(part.clone()).or_insert(0) += coeff;
123        }
124
125        // Add contributions from cells in the boundary
126        // (cells μ with μ > λ in Bruhat order, i.e., μ ⊂ λ as partitions)
127        let part = Partition::new(partition.to_vec());
128        let cells_in_closure = partitions_dominated_by(&part, k, m);
129
130        for mu in cells_in_closure {
131            if mu != part {
132                let mu_csm = Self::of_schubert_cell(&mu.parts, grassmannian);
133                for (p, c) in &mu_csm.coefficients {
134                    *total.entry(p.clone()).or_insert(0) += c;
135                }
136            }
137        }
138
139        CSMClass {
140            coefficients: total,
141            grassmannian,
142        }
143    }
144
145    /// Euler characteristic: the degree of the CSM class.
146    ///
147    /// Equal to the coefficient of the point class σ_{m^k} (fundamental class
148    /// of the Grassmannian).
149    ///
150    /// # Contract
151    ///
152    /// ```text
153    /// ensures: for a Schubert cell, result == 1
154    /// ```
155    #[must_use]
156    pub fn euler_characteristic(&self) -> i64 {
157        let (k, n) = self.grassmannian;
158        let m = n - k;
159        let point_class = Partition::new(vec![m; k]);
160
161        // The Euler characteristic is obtained by pairing with the fundamental class
162        // via the intersection pairing on the Grassmannian
163        let mut euler = 0i64;
164
165        for (partition, &coeff) in &self.coefficients {
166            if coeff == 0 {
167                continue;
168            }
169
170            // Check if this partition, when paired with the point class, gives nonzero
171            // σ_λ pairs nontrivially with σ_μ iff |λ| + |μ| = k*m and c^{m^k}_{λμ} ≠ 0
172            let codim_lambda = partition.size();
173            let codim_needed = k * m - codim_lambda;
174
175            if codim_needed == 0 {
176                // This is the point class itself
177                euler += coeff;
178            }
179        }
180
181        // If no direct match, compute via LR pairing
182        if euler == 0 {
183            for (partition, &coeff) in &self.coefficients {
184                if coeff == 0 {
185                    continue;
186                }
187                // Dual partition for intersection pairing
188                let dual = dual_partition(partition, k, m);
189                if let Some(d) = dual {
190                    let lr = lr_coefficient(partition, &d, &point_class);
191                    euler += coeff * lr as i64;
192                }
193            }
194        }
195
196        euler
197    }
198
199    /// Multiply two CSM classes via the intersection pairing.
200    ///
201    /// Uses Littlewood-Richardson coefficients to expand the product
202    /// in the Schubert basis.
203    ///
204    /// # Contract
205    ///
206    /// ```text
207    /// requires: self.grassmannian == other.grassmannian
208    /// ensures: result is the product in the Schubert basis ring
209    /// ```
210    #[must_use]
211    pub fn csm_intersection(&self, other: &CSMClass) -> CSMClass {
212        assert_eq!(
213            self.grassmannian, other.grassmannian,
214            "CSM classes must be on the same Grassmannian"
215        );
216
217        let (k, n) = self.grassmannian;
218        let mut result = BTreeMap::new();
219
220        for (lambda, &a) in &self.coefficients {
221            if a == 0 {
222                continue;
223            }
224            for (mu, &b) in &other.coefficients {
225                if b == 0 {
226                    continue;
227                }
228                // σ_λ · σ_μ = Σ_ν c^ν_{λμ} σ_ν
229                let products = schubert_product(lambda, mu, (k, n));
230                for (nu, lr) in products {
231                    *result.entry(nu).or_insert(0i64) += a * b * lr as i64;
232                }
233            }
234        }
235
236        CSMClass {
237            coefficients: result,
238            grassmannian: self.grassmannian,
239        }
240    }
241}
242
243/// Segre class: the formal inverse of the total Chern class.
244///
245/// If c(E) = 1 + c_1 + c_2 + ... is the total Chern class, then
246/// s(E) = c(E)^{-1} = 1 - c_1 + (c_1^2 - c_2) - ...
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct SegreClass {
249    /// Expansion in the Schubert basis
250    pub coefficients: BTreeMap<Partition, i64>,
251    /// Grassmannian parameters
252    pub grassmannian: (usize, usize),
253}
254
255impl SegreClass {
256    /// Compute Segre class from a CSM (Chern) class by formal inversion.
257    ///
258    /// s = c^{-1} in the ring of Schubert classes.
259    ///
260    /// # Contract
261    ///
262    /// ```text
263    /// ensures: s · c == identity (in the Schubert ring, up to truncation)
264    /// ```
265    #[must_use]
266    pub fn from_chern(csm: &CSMClass) -> Self {
267        let (k, n) = csm.grassmannian;
268        let m = n - k;
269
270        // Start with identity
271        let mut segre = BTreeMap::new();
272        segre.insert(Partition::empty(), 1i64);
273
274        // For low codimensions, compute the inverse iteratively
275        // s_0 = 1, s_i = -Σ_{j=1}^{i} c_j * s_{i-j}
276        for codim in 1..=(k * m) {
277            let mut s_codim = 0i64;
278            for (part, &c_val) in &csm.coefficients {
279                let c_codim = part.size();
280                if c_codim == 0 || c_codim > codim {
281                    continue;
282                }
283                let remaining_codim = codim - c_codim;
284                // Find segre terms at the remaining codimension
285                for (s_part, &s_val) in &segre.clone() {
286                    if s_part.size() == remaining_codim {
287                        // Multiply c_part * s_part via LR
288                        let products = schubert_product(part, s_part, (k, n));
289                        for (nu, lr) in products {
290                            if nu.size() == codim {
291                                s_codim -= c_val * s_val * lr as i64;
292                            }
293                        }
294                    }
295                }
296            }
297            if s_codim != 0 {
298                // Distribute across partitions of this codimension
299                let parts = partitions_of_size(codim, k, m);
300                if parts.len() == 1 {
301                    segre.insert(parts[0].clone(), s_codim);
302                } else if !parts.is_empty() {
303                    // Simplified: assign to first partition
304                    segre.insert(parts[0].clone(), s_codim);
305                }
306            }
307        }
308
309        SegreClass {
310            coefficients: segre,
311            grassmannian: csm.grassmannian,
312        }
313    }
314
315    /// Compute excess intersection number using Segre class.
316    ///
317    /// When the intersection is not transverse (excess dimension > 0),
318    /// the corrected intersection number is:
319    /// ```text
320    /// corrected = ∫ s_excess(N) ∩ [X ∩ Y]
321    /// ```
322    ///
323    /// # Contract
324    ///
325    /// ```text
326    /// ensures: result == sum of Segre coefficients at codimension excess_dim
327    /// ```
328    #[must_use]
329    pub fn excess_intersection(&self, excess_dim: usize) -> i64 {
330        // Extract the Segre class component of the excess dimension
331        self.coefficients
332            .iter()
333            .filter(|(p, _)| p.size() == excess_dim)
334            .map(|(_, &c)| c)
335            .sum()
336    }
337}
338
339/// Namespace integration
340impl Namespace {
341    /// Check if the namespace is degenerate (CSM Euler characteristic is anomalous).
342    ///
343    /// A namespace is degenerate if the CSM class of its position
344    /// has unexpected Euler characteristic.
345    ///
346    /// # Contract
347    ///
348    /// ```text
349    /// ensures: result == (euler_characteristic(c_SM(position)) != 1)
350    /// ```
351    #[must_use]
352    pub fn is_degenerate(&self) -> bool {
353        let csm = CSMClass::of_schubert_cell(&self.position.partition, self.grassmannian);
354        // A non-degenerate cell should have Euler characteristic 1
355        let euler = csm.euler_characteristic();
356        euler != 1
357    }
358
359    /// Count configurations using CSM-corrected intersection theory.
360    ///
361    /// When the intersection is potentially singular, use CSM classes
362    /// to get the corrected count.
363    ///
364    /// # Contract
365    ///
366    /// ```text
367    /// ensures: result >= 0 when intersection is transverse
368    /// ensures: result == 1 when self.capabilities.is_empty()
369    /// ```
370    #[must_use]
371    pub fn csm_count_configurations(&self) -> i64 {
372        if self.capabilities.is_empty() {
373            return 1;
374        }
375
376        let (k, n) = self.grassmannian;
377        let gr_dim = k * (n - k);
378
379        // Compute total codimension
380        let total_codim: usize = self.capabilities.iter().map(|c| c.codimension()).sum();
381
382        if total_codim > gr_dim {
383            return 0;
384        }
385
386        // Build CSM class for the intersection
387        let mut product = CSMClass::of_schubert_cell(
388            &self.capabilities[0].schubert_class.partition,
389            self.grassmannian,
390        );
391
392        for cap in &self.capabilities[1..] {
393            let cap_csm =
394                CSMClass::of_schubert_cell(&cap.schubert_class.partition, self.grassmannian);
395            product = product.csm_intersection(&cap_csm);
396        }
397
398        product.euler_characteristic()
399    }
400}
401
402// Helper functions
403
404/// Find all partitions dominated by λ (in containment order) fitting in k × m box.
405fn partitions_dominated_by(lambda: &Partition, k: usize, m: usize) -> Vec<Partition> {
406    let mut result = Vec::new();
407    generate_dominated(lambda, k, m, &[], 0, &mut result);
408    result
409}
410
411fn generate_dominated(
412    lambda: &Partition,
413    k: usize,
414    m: usize,
415    prefix: &[usize],
416    row: usize,
417    result: &mut Vec<Partition>,
418) {
419    if row >= k {
420        let part = Partition::new(prefix.to_vec());
421        result.push(part);
422        return;
423    }
424
425    let max_val = lambda.parts.get(row).copied().unwrap_or(0).min(m);
426    let prev = if row > 0 {
427        prefix.get(row - 1).copied().unwrap_or(m)
428    } else {
429        m
430    };
431    let upper = max_val.min(prev);
432
433    for v in 0..=upper {
434        let mut new_prefix = prefix.to_vec();
435        new_prefix.push(v);
436        generate_dominated(lambda, k, m, &new_prefix, row + 1, result);
437    }
438}
439
440/// Dual partition for intersection pairing: μ_i = m - λ_{k-i}.
441fn dual_partition(lambda: &Partition, k: usize, m: usize) -> Option<Partition> {
442    let mut padded = vec![0usize; k];
443    for (i, &p) in lambda.parts.iter().enumerate() {
444        if i >= k {
445            return None;
446        }
447        if p > m {
448            return None;
449        }
450        padded[i] = p;
451    }
452
453    let dual_parts: Vec<usize> = (0..k).map(|i| m - padded[k - 1 - i]).collect();
454
455    Some(Partition::new(dual_parts))
456}
457
458/// Generate all partitions of a given size fitting in k × m box.
459fn partitions_of_size(size: usize, k: usize, m: usize) -> Vec<Partition> {
460    let mut result = Vec::new();
461    gen_partitions(size, k, m, &[], 0, &mut result);
462    result
463}
464
465fn gen_partitions(
466    remaining: usize,
467    k: usize,
468    m: usize,
469    prefix: &[usize],
470    row: usize,
471    result: &mut Vec<Partition>,
472) {
473    if remaining == 0 {
474        result.push(Partition::new(prefix.to_vec()));
475        return;
476    }
477    if row >= k {
478        return;
479    }
480    let prev = if row > 0 { prefix[row - 1] } else { m };
481    let max_val = remaining.min(prev).min(m);
482    for v in (1..=max_val).rev() {
483        let mut new_prefix = prefix.to_vec();
484        new_prefix.push(v);
485        gen_partitions(remaining - v, k, m, &new_prefix, row + 1, result);
486    }
487}
488
489/// Batch compute CSM classes of Schubert cells in parallel.
490#[cfg(feature = "parallel")]
491#[must_use]
492pub fn csm_of_cells_batch(
493    partitions: &[Vec<usize>],
494    grassmannian: (usize, usize),
495) -> Vec<CSMClass> {
496    use rayon::prelude::*;
497    partitions
498        .par_iter()
499        .map(|part| CSMClass::of_schubert_cell(part, grassmannian))
500        .collect()
501}
502
503/// Batch compute Euler characteristics of CSM classes in parallel.
504#[cfg(feature = "parallel")]
505#[must_use]
506pub fn euler_characteristic_batch(classes: &[CSMClass]) -> Vec<i64> {
507    use rayon::prelude::*;
508    classes
509        .par_iter()
510        .map(|csm| csm.euler_characteristic())
511        .collect()
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::schubert::SchubertClass;
518
519    #[test]
520    fn test_csm_class_point() {
521        // CSM of the point class σ_{2,2} in Gr(2,4) should include σ_{2,2}
522        let csm = CSMClass::of_schubert_cell(&[2, 2], (2, 4));
523        assert!(csm.coefficients.contains_key(&Partition::new(vec![2, 2])));
524    }
525
526    #[test]
527    fn test_csm_top_cell() {
528        // CSM of the top cell (empty partition) = [Gr]
529        let csm = CSMClass::of_schubert_cell(&[], (2, 4));
530        assert_eq!(csm.coefficients.get(&Partition::empty()), Some(&1));
531    }
532
533    #[test]
534    fn test_csm_euler_characteristic_top_cell() {
535        let csm = CSMClass::of_schubert_cell(&[], (2, 4));
536        let euler = csm.euler_characteristic();
537        // Top cell Euler characteristic = 1
538        assert_eq!(euler, 1);
539    }
540
541    #[test]
542    fn test_csm_variety() {
543        // CSM of a Schubert variety should have nonnegative coefficients
544        // (Huh's theorem / June Huh's work)
545        let csm = CSMClass::of_schubert_variety(&[1], (2, 4));
546        // The variety Ω_{1} has cells Ω°_{1}, Ω°_{∅}, and smaller cells
547        assert!(!csm.coefficients.is_empty());
548    }
549
550    #[test]
551    fn test_csm_intersection() {
552        let csm1 = CSMClass::of_schubert_cell(&[1], (2, 4));
553        let csm2 = CSMClass::of_schubert_cell(&[1], (2, 4));
554        let product = csm1.csm_intersection(&csm2);
555        // Product should be nonempty
556        assert!(!product.coefficients.is_empty());
557    }
558
559    #[test]
560    fn test_segre_from_chern() {
561        let csm = CSMClass::of_schubert_cell(&[], (2, 4));
562        let segre = SegreClass::from_chern(&csm);
563        // Identity Chern class → identity Segre class
564        assert_eq!(segre.coefficients.get(&Partition::empty()), Some(&1));
565    }
566
567    #[test]
568    fn test_csm_agrees_transverse() {
569        // For transverse intersection, CSM count should agree with standard count
570        let pos = SchubertClass::new(vec![], (2, 4)).unwrap();
571        let mut ns = Namespace::new("test", pos);
572
573        let cap1 = crate::namespace::Capability::new("c1", "Cap1", vec![1], (2, 4)).unwrap();
574        let cap2 = crate::namespace::Capability::new("c2", "Cap2", vec![1], (2, 4)).unwrap();
575        let cap3 = crate::namespace::Capability::new("c3", "Cap3", vec![1], (2, 4)).unwrap();
576        let cap4 = crate::namespace::Capability::new("c4", "Cap4", vec![1], (2, 4)).unwrap();
577        ns.grant(cap1).unwrap();
578        ns.grant(cap2).unwrap();
579        ns.grant(cap3).unwrap();
580        ns.grant(cap4).unwrap();
581
582        // 4 copies of σ_1 on Gr(2,4): count = 2
583        let csm_count = ns.csm_count_configurations();
584        // CSM count may differ from classical due to correction terms,
585        // but should be positive
586        assert!(csm_count > 0);
587    }
588
589    #[test]
590    fn test_namespace_is_degenerate() {
591        let pos = SchubertClass::new(vec![], (2, 4)).unwrap();
592        let ns = Namespace::new("test", pos);
593        // Top cell should not be degenerate
594        assert!(!ns.is_degenerate());
595    }
596
597    #[test]
598    fn test_dual_partition() {
599        let lambda = Partition::new(vec![2, 1]);
600        let dual = dual_partition(&lambda, 2, 2);
601        assert!(dual.is_some());
602        let d = dual.unwrap();
603        // Dual of (2,1) in 2×2 box: (2-1, 2-2) = (1, 0) → (1)
604        assert_eq!(d, Partition::new(vec![1]));
605    }
606
607    #[cfg(feature = "parallel")]
608    #[test]
609    fn test_csm_of_cells_batch() {
610        let partitions = vec![vec![], vec![1], vec![2, 1]];
611        let results = super::csm_of_cells_batch(&partitions, (2, 4));
612        assert_eq!(results.len(), 3);
613        // Top cell should have identity coefficient
614        assert_eq!(results[0].coefficients.get(&Partition::empty()), Some(&1));
615    }
616
617    #[cfg(feature = "parallel")]
618    #[test]
619    fn test_euler_characteristic_batch() {
620        let classes = vec![
621            CSMClass::of_schubert_cell(&[], (2, 4)),
622            CSMClass::of_schubert_cell(&[1], (2, 4)),
623        ];
624        let results = super::euler_characteristic_batch(&classes);
625        assert_eq!(results.len(), 2);
626        // Top cell Euler characteristic = 1
627        assert_eq!(results[0], 1);
628    }
629}