Skip to main content

amari_enumerative/
localization.rs

1//! Equivariant Localization via the Atiyah-Bott Fixed Point Formula
2//!
3//! Provides the fixed-point framework for Grassmannian intersection computations.
4//! The `EquivariantLocalizer` computes Schubert intersection numbers by delegating
5//! to the LR coefficient machinery, while exposing the torus action structure
6//! (fixed points, tangent weights, Euler classes) for geometric analysis.
7//!
8//! # The Formula
9//!
10//! ```text
11//! ∫_{Gr(k,n)} ω = Σ_{|I|=k} ω^T(e_I) / e_T(T_{e_I} Gr)
12//! ```
13//!
14//! # Contracts
15//!
16//! - Fixed points are exactly the C(n,k) coordinate subspaces
17//! - Tangent Euler class at each fixed point is nonzero (for distinct weights)
18//! - Results agree with LR coefficient computation
19
20use crate::schubert::{IntersectionResult, SchubertCalculus, SchubertClass};
21use crate::EnumerativeResult;
22use num_rational::Rational64;
23
24/// Torus weights for equivariant localization.
25///
26/// The standard torus T = (C*)^n acts on C^n with weights t_0, ..., t_{n-1}.
27/// For the standard choice, t_i = i + 1 (to avoid zero weights).
28#[derive(Debug, Clone)]
29pub struct TorusWeights {
30    /// The weights t_0, ..., t_{n-1}
31    pub weights: Vec<i64>,
32}
33
34impl TorusWeights {
35    /// Standard weights: t_i = i + 1.
36    ///
37    /// # Contract
38    ///
39    /// ```text
40    /// ensures: result.weights.len() == n
41    /// ensures: forall i. result.weights[i] == i + 1
42    /// ensures: all weights distinct and nonzero
43    /// ```
44    #[must_use]
45    pub fn standard(n: usize) -> Self {
46        Self {
47            weights: (1..=n as i64).collect(),
48        }
49    }
50
51    /// Custom weights. Validates that all weights are distinct and nonzero.
52    ///
53    /// # Contract
54    ///
55    /// ```text
56    /// requires: forall i. weights[i] != 0
57    /// requires: forall i != j. weights[i] != weights[j]
58    /// ```
59    pub fn custom(weights: Vec<i64>) -> EnumerativeResult<Self> {
60        if weights.contains(&0) {
61            return Err(crate::EnumerativeError::InvalidDimension(
62                "Torus weights must be nonzero".to_string(),
63            ));
64        }
65        let mut sorted = weights.clone();
66        sorted.sort_unstable();
67        if sorted.windows(2).any(|w| w[0] == w[1]) {
68            return Err(crate::EnumerativeError::InvalidDimension(
69                "Torus weights must be distinct".to_string(),
70            ));
71        }
72        Ok(Self { weights })
73    }
74}
75
76/// A torus-fixed point in Gr(k, n): a k-element subset of {0, ..., n-1}.
77///
78/// The fixed point e_I corresponds to the coordinate k-plane spanned
79/// by {e_i : i ∈ I}.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct FixedPoint {
82    /// The k-element subset I ⊂ {0, ..., n-1}
83    pub subset: Vec<usize>,
84    /// Grassmannian parameters
85    pub grassmannian: (usize, usize),
86}
87
88impl FixedPoint {
89    /// Create a fixed point from a k-element subset.
90    ///
91    /// # Contract
92    ///
93    /// ```text
94    /// requires: subset.len() == k
95    /// requires: forall i in subset. i < n
96    /// ```
97    pub fn new(mut subset: Vec<usize>, grassmannian: (usize, usize)) -> EnumerativeResult<Self> {
98        let (k, n) = grassmannian;
99        if subset.len() != k {
100            return Err(crate::EnumerativeError::InvalidDimension(format!(
101                "Fixed point subset must have {} elements, got {}",
102                k,
103                subset.len()
104            )));
105        }
106        if subset.iter().any(|&i| i >= n) {
107            return Err(crate::EnumerativeError::InvalidDimension(format!(
108                "Subset elements must be < {n}"
109            )));
110        }
111        subset.sort_unstable();
112        Ok(Self {
113            subset,
114            grassmannian,
115        })
116    }
117
118    /// Compute the equivariant Euler class of the tangent space at this fixed point.
119    ///
120    /// ```text
121    /// e_T(T_{e_I} Gr) = ∏_{i ∈ I, j ∉ I} (t_j - t_i)
122    /// ```
123    ///
124    /// # Contract
125    ///
126    /// ```text
127    /// ensures: result != 0 (when weights are distinct)
128    /// ```
129    #[must_use]
130    pub fn tangent_euler_class(&self, weights: &TorusWeights) -> Rational64 {
131        let (_, n) = self.grassmannian;
132        let complement: Vec<usize> = (0..n).filter(|i| !self.subset.contains(i)).collect();
133
134        let mut product = Rational64::from(1);
135        for &i in &self.subset {
136            for &j in &complement {
137                product *= Rational64::from(weights.weights[j] - weights.weights[i]);
138            }
139        }
140        product
141    }
142
143    /// Compute the Schubert partition associated with this fixed point.
144    ///
145    /// For I = {i_0 < ... < i_{k-1}}, the partition is λ_a = i_a - a.
146    #[must_use]
147    pub fn to_partition(&self) -> Vec<usize> {
148        let mut partition: Vec<usize> = self
149            .subset
150            .iter()
151            .enumerate()
152            .map(|(a, &i_a)| i_a - a)
153            .collect();
154        partition.sort_unstable_by(|a, b| b.cmp(a));
155        partition.retain(|&x| x > 0);
156        partition
157    }
158}
159
160/// Equivariant localizer for Grassmannian intersection computations.
161///
162/// Uses the Atiyah-Bott fixed point framework with Schubert calculus
163/// for reliable intersection number computation.
164#[derive(Debug)]
165pub struct EquivariantLocalizer {
166    /// Grassmannian parameters (k, n)
167    pub grassmannian: (usize, usize),
168    /// Torus weights
169    pub weights: TorusWeights,
170    /// Cached fixed points (lazily computed)
171    fixed_points: Option<Vec<FixedPoint>>,
172    /// Schubert calculus engine for actual computation
173    schubert_engine: SchubertCalculus,
174}
175
176impl EquivariantLocalizer {
177    /// Create a new localizer for Gr(k, n) with standard weights.
178    ///
179    /// # Contract
180    ///
181    /// ```text
182    /// requires: k <= n
183    /// ensures: result.fixed_point_count() == C(n, k)
184    /// ```
185    pub fn new(grassmannian: (usize, usize)) -> EnumerativeResult<Self> {
186        let (k, n) = grassmannian;
187        if k > n {
188            return Err(crate::EnumerativeError::InvalidDimension(format!(
189                "k={k} must be <= n={n} for Gr(k,n)"
190            )));
191        }
192        Ok(Self {
193            grassmannian,
194            weights: TorusWeights::standard(n),
195            fixed_points: None,
196            schubert_engine: SchubertCalculus::new(grassmannian),
197        })
198    }
199
200    /// Create a localizer with custom weights.
201    ///
202    /// # Contract
203    ///
204    /// ```text
205    /// requires: k <= n
206    /// requires: weights.len() == n
207    /// ```
208    pub fn with_weights(
209        grassmannian: (usize, usize),
210        weights: TorusWeights,
211    ) -> EnumerativeResult<Self> {
212        let (k, n) = grassmannian;
213        if k > n {
214            return Err(crate::EnumerativeError::InvalidDimension(format!(
215                "k={k} must be <= n={n}"
216            )));
217        }
218        if weights.weights.len() != n {
219            return Err(crate::EnumerativeError::InvalidDimension(format!(
220                "Need {n} weights, got {}",
221                weights.weights.len()
222            )));
223        }
224        Ok(Self {
225            grassmannian,
226            weights,
227            fixed_points: None,
228            schubert_engine: SchubertCalculus::new(grassmannian),
229        })
230    }
231
232    /// Number of fixed points: C(n, k).
233    #[must_use]
234    pub fn fixed_point_count(&self) -> usize {
235        let (k, n) = self.grassmannian;
236        binomial_usize(n, k)
237    }
238
239    /// Lazily compute and cache all fixed points.
240    fn ensure_fixed_points(&mut self) {
241        if self.fixed_points.is_some() {
242            return;
243        }
244        let (k, n) = self.grassmannian;
245        let subsets = k_subsets(n, k);
246        let points: Vec<FixedPoint> = subsets
247            .into_iter()
248            .map(|s| FixedPoint {
249                subset: s,
250                grassmannian: self.grassmannian,
251            })
252            .collect();
253        self.fixed_points = Some(points);
254    }
255
256    /// Get all fixed points.
257    pub fn fixed_points(&mut self) -> &[FixedPoint] {
258        self.ensure_fixed_points();
259        self.fixed_points.as_ref().unwrap()
260    }
261
262    /// Compute the localized intersection of multiple Schubert classes.
263    ///
264    /// Delegates to LR coefficient computation via `SchubertCalculus::multi_intersect`
265    /// for correctness, while the localization framework provides geometric structure.
266    ///
267    /// # Contract
268    ///
269    /// ```text
270    /// requires: total codimension == dim(Gr) for a finite answer
271    /// ensures: result agrees with LR coefficient computation
272    /// ```
273    pub fn localized_intersection(&mut self, classes: &[SchubertClass]) -> Rational64 {
274        let result = self.schubert_engine.multi_intersect(classes);
275        match result {
276            IntersectionResult::Finite(n) => Rational64::from(n as i64),
277            _ => Rational64::from(0),
278        }
279    }
280
281    /// Compute intersection result with codimension checks.
282    ///
283    /// Handles the three cases: overdetermined (Empty),
284    /// transverse (Finite), and underdetermined (PositiveDimensional).
285    ///
286    /// # Contract
287    ///
288    /// ```text
289    /// ensures: result == Empty when sum(codim) > dim(Gr)
290    /// ensures: result == Finite(n) when sum(codim) == dim(Gr)
291    /// ensures: result == PositiveDimensional when sum(codim) < dim(Gr)
292    /// ```
293    pub fn intersection_result(&mut self, classes: &[SchubertClass]) -> IntersectionResult {
294        self.schubert_engine.multi_intersect(classes)
295    }
296
297    /// Analyze the fixed-point contributions for a transverse intersection.
298    ///
299    /// Returns (fixed_point, euler_class) pairs for geometric analysis.
300    ///
301    /// # Contract
302    ///
303    /// ```text
304    /// ensures: result.len() == C(n, k)
305    /// ensures: forall (_, euler) in result. euler != 0 (for distinct weights)
306    /// ```
307    pub fn fixed_point_analysis(&mut self) -> Vec<(&FixedPoint, Rational64)> {
308        self.ensure_fixed_points();
309        let weights = &self.weights;
310        self.fixed_points
311            .as_ref()
312            .unwrap()
313            .iter()
314            .map(|fp| {
315                let euler = fp.tangent_euler_class(weights);
316                (fp, euler)
317            })
318            .collect()
319    }
320
321    /// Parallel version of localized intersection.
322    #[cfg(feature = "parallel")]
323    pub fn localized_intersection_parallel(&mut self, classes: &[SchubertClass]) -> Rational64 {
324        // Same as sequential — both use LR coefficients
325        self.localized_intersection(classes)
326    }
327}
328
329/// Generate all k-element subsets of {0, ..., n-1}.
330fn k_subsets(n: usize, k: usize) -> Vec<Vec<usize>> {
331    let mut result = Vec::new();
332    let mut current = Vec::with_capacity(k);
333    generate_subsets(n, k, 0, &mut current, &mut result);
334    result
335}
336
337fn generate_subsets(
338    n: usize,
339    k: usize,
340    start: usize,
341    current: &mut Vec<usize>,
342    result: &mut Vec<Vec<usize>>,
343) {
344    if current.len() == k {
345        result.push(current.clone());
346        return;
347    }
348    let remaining = k - current.len();
349    for i in start..=(n - remaining) {
350        current.push(i);
351        generate_subsets(n, k, i + 1, current, result);
352        current.pop();
353    }
354}
355
356/// Binomial coefficient C(n, k) for small values.
357fn binomial_usize(n: usize, k: usize) -> usize {
358    if k > n {
359        return 0;
360    }
361    if k == 0 || k == n {
362        return 1;
363    }
364    let k = k.min(n - k);
365    let mut result: usize = 1;
366    for i in 0..k {
367        result = result * (n - i) / (i + 1);
368    }
369    result
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_fixed_point_count() {
378        let loc = EquivariantLocalizer::new((2, 4)).unwrap();
379        assert_eq!(loc.fixed_point_count(), 6); // C(4,2) = 6
380    }
381
382    #[test]
383    fn test_fixed_point_count_gr35() {
384        let loc = EquivariantLocalizer::new((3, 5)).unwrap();
385        assert_eq!(loc.fixed_point_count(), 10); // C(5,3) = 10
386    }
387
388    #[test]
389    fn test_tangent_euler_class_nonzero() {
390        let weights = TorusWeights::standard(4);
391        let fp = FixedPoint::new(vec![0, 1], (2, 4)).unwrap();
392        let euler = fp.tangent_euler_class(&weights);
393        assert_ne!(euler, Rational64::from(0));
394    }
395
396    #[test]
397    fn test_fixed_point_partition() {
398        // Fixed point {0, 2} in Gr(2,4): partition = [2-1, 0-0] = [1]
399        let fp = FixedPoint::new(vec![0, 2], (2, 4)).unwrap();
400        assert_eq!(fp.to_partition(), vec![1]);
401    }
402
403    #[test]
404    fn test_localization_four_lines() {
405        // Classic: how many lines meet 4 general lines in P³?
406        // Answer: 2 (computed as σ_1^4 on Gr(2,4))
407        let mut loc = EquivariantLocalizer::new((2, 4)).unwrap();
408
409        let sigma_1 = SchubertClass::new(vec![1], (2, 4)).unwrap();
410        let classes = vec![sigma_1.clone(), sigma_1.clone(), sigma_1.clone(), sigma_1];
411
412        let result = loc.localized_intersection(&classes);
413        assert_eq!(result, Rational64::from(2));
414    }
415
416    #[test]
417    fn test_localization_point_class() {
418        // σ_{2,2} is the point class on Gr(2,4), integral = 1
419        let mut loc = EquivariantLocalizer::new((2, 4)).unwrap();
420
421        let sigma_22 = SchubertClass::new(vec![2, 2], (2, 4)).unwrap();
422        let classes = vec![sigma_22];
423
424        let result = loc.localized_intersection(&classes);
425        assert_eq!(result, Rational64::from(1));
426    }
427
428    #[test]
429    fn test_localization_sigma1_squared_gr24() {
430        // σ_1^2 on Gr(2,4): codim = 2, dim(Gr) = 4, so underdetermined
431        let mut loc = EquivariantLocalizer::new((2, 4)).unwrap();
432
433        let sigma_1 = SchubertClass::new(vec![1], (2, 4)).unwrap();
434        let classes = vec![sigma_1.clone(), sigma_1];
435
436        let result = loc.intersection_result(&classes);
437        assert!(matches!(
438            result,
439            IntersectionResult::PositiveDimensional { dimension: 2, .. }
440        ));
441    }
442
443    #[test]
444    fn test_localization_overdetermined() {
445        let mut loc = EquivariantLocalizer::new((2, 4)).unwrap();
446
447        let sigma_1 = SchubertClass::new(vec![1], (2, 4)).unwrap();
448        // 5 copies of σ_1: codim 5 > dim 4
449        let classes = vec![
450            sigma_1.clone(),
451            sigma_1.clone(),
452            sigma_1.clone(),
453            sigma_1.clone(),
454            sigma_1,
455        ];
456
457        let result = loc.intersection_result(&classes);
458        assert_eq!(result, IntersectionResult::Empty);
459    }
460
461    #[test]
462    fn test_localization_sigma1_cubed_sigma1() {
463        // σ_1^3 · σ_1 on Gr(2,4): same as σ_1^4 = 2
464        let mut loc = EquivariantLocalizer::new((2, 4)).unwrap();
465
466        let sigma_1 = SchubertClass::new(vec![1], (2, 4)).unwrap();
467        let classes = vec![sigma_1.clone(), sigma_1.clone(), sigma_1.clone(), sigma_1];
468
469        let result = loc.intersection_result(&classes);
470        assert_eq!(result, IntersectionResult::Finite(2));
471    }
472
473    #[test]
474    fn test_custom_weights() {
475        // Custom weights should still give the same intersection number
476        let weights = TorusWeights::custom(vec![1, 3, 7, 11]).unwrap();
477        let mut loc = EquivariantLocalizer::with_weights((2, 4), weights).unwrap();
478
479        let sigma_1 = SchubertClass::new(vec![1], (2, 4)).unwrap();
480        let classes = vec![sigma_1.clone(), sigma_1.clone(), sigma_1.clone(), sigma_1];
481
482        let result = loc.localized_intersection(&classes);
483        assert_eq!(result, Rational64::from(2));
484    }
485
486    #[test]
487    fn test_invalid_weights_zero() {
488        let result = TorusWeights::custom(vec![0, 1, 2, 3]);
489        assert!(result.is_err());
490    }
491
492    #[test]
493    fn test_invalid_weights_duplicate() {
494        let result = TorusWeights::custom(vec![1, 2, 2, 3]);
495        assert!(result.is_err());
496    }
497
498    #[test]
499    fn test_k_subsets() {
500        let subs = k_subsets(4, 2);
501        assert_eq!(subs.len(), 6);
502        assert_eq!(subs[0], vec![0, 1]);
503        assert_eq!(subs[5], vec![2, 3]);
504    }
505
506    #[test]
507    fn test_fixed_point_analysis() {
508        let mut loc = EquivariantLocalizer::new((2, 4)).unwrap();
509        let analysis = loc.fixed_point_analysis();
510        assert_eq!(analysis.len(), 6);
511        // All Euler classes should be nonzero
512        for (_, euler) in &analysis {
513            assert_ne!(*euler, Rational64::from(0));
514        }
515    }
516
517    #[cfg(feature = "parallel")]
518    #[test]
519    fn test_parallel_agrees() {
520        let mut loc = EquivariantLocalizer::new((2, 4)).unwrap();
521
522        let sigma_1 = SchubertClass::new(vec![1], (2, 4)).unwrap();
523        let classes = vec![sigma_1.clone(), sigma_1.clone(), sigma_1.clone(), sigma_1];
524
525        let sequential = loc.localized_intersection(&classes);
526        let parallel = loc.localized_intersection_parallel(&classes);
527        assert_eq!(sequential, parallel);
528    }
529}