Skip to main content

amari_enumerative/
stability.rs

1//! Wall-Crossing and Bridgeland Stability
2//!
3//! Implements Bridgeland stability conditions on namespace capabilities,
4//! modeling how capability counts change as trust levels vary.
5//!
6//! # Key Concepts
7//!
8//! - **Stability condition**: Z_t(σ_λ) = -codim(λ) + i·t·dim(λ) assigns a central
9//!   charge to each Schubert class. A capability is stable if its phase is in (0, 1).
10//! - **Wall**: A trust level where a capability transitions between stable/unstable.
11//! - **Wall-crossing formula**: Tracks how the count of stable capabilities changes.
12//!
13//! # Contracts
14//!
15//! - Central charge is linear in the class
16//! - Phase is in [0, 1] for geometric objects
17//! - Walls are finitely many and computable
18
19use crate::namespace::{Capability, CapabilityId, Namespace};
20use crate::schubert::SchubertClass;
21
22/// A Bridgeland-type stability condition parameterized by trust level.
23///
24/// The central charge is:
25/// ```text
26/// Z_t(σ_λ) = -codim(σ_λ) + i · t · dim(σ_λ)
27/// ```
28///
29/// A Schubert class is stable at trust level t if its phase φ = (1/π)·arg(Z_t) ∈ (0, 1).
30#[derive(Debug, Clone)]
31pub struct StabilityCondition {
32    /// Grassmannian parameters
33    pub grassmannian: (usize, usize),
34    /// Trust level parameter
35    pub trust_level: f64,
36}
37
38impl StabilityCondition {
39    /// Create the standard stability condition for a Grassmannian.
40    ///
41    /// # Contract
42    ///
43    /// ```text
44    /// requires: trust_level > 0
45    /// ensures: result.is_stable(σ_λ) depends on codim/dim ratio
46    /// ```
47    pub fn standard(grassmannian: (usize, usize), trust_level: f64) -> Self {
48        Self {
49            grassmannian,
50            trust_level,
51        }
52    }
53
54    /// Compute the central charge Z_t(σ_λ).
55    ///
56    /// Returns (real_part, imaginary_part).
57    ///
58    /// # Contract
59    ///
60    /// ```text
61    /// ensures: result.0 == -codim(class)
62    /// ensures: result.1 == trust_level * dim(class)
63    /// ```
64    #[must_use]
65    pub fn central_charge(&self, class: &SchubertClass) -> (f64, f64) {
66        let codim = class.codimension() as f64;
67        let dim = class.dimension() as f64;
68        (-codim, self.trust_level * dim)
69    }
70
71    /// Compute the phase φ = (1/π) · arg(Z_t(σ_λ)).
72    ///
73    /// Phase in (0, 1) means the class is stable.
74    /// Phase = 0 or 1 means semistable (on a wall).
75    ///
76    /// # Contract
77    ///
78    /// ```text
79    /// ensures: 0.0 <= result <= 1.0
80    /// ```
81    #[must_use]
82    pub fn phase(&self, class: &SchubertClass) -> f64 {
83        let (re, im) = self.central_charge(class);
84
85        if re == 0.0 && im == 0.0 {
86            return 0.0;
87        }
88
89        let angle = im.atan2(re); // in (-π, π]
90        let normalized = angle / std::f64::consts::PI; // in (-1, 1]
91
92        // Map to [0, 1]: phase in (0, 1) means stable
93        if normalized < 0.0 {
94            normalized + 1.0
95        } else {
96            normalized
97        }
98    }
99
100    /// Check if a capability is stable at this trust level.
101    ///
102    /// # Contract
103    ///
104    /// ```text
105    /// ensures: result == (0 < phase(class) < 1)
106    /// ```
107    #[must_use]
108    pub fn is_stable(&self, capability: &Capability) -> bool {
109        let phase = self.phase(&capability.schubert_class);
110        phase > 0.0 && phase < 1.0
111    }
112
113    /// Find all stable capabilities in a namespace at this trust level.
114    ///
115    /// # Contract
116    ///
117    /// ```text
118    /// ensures: forall cap in result. self.is_stable(cap) == true
119    /// ensures: result.len() <= namespace.capabilities.len()
120    /// ```
121    #[must_use]
122    pub fn stable_capabilities<'a>(&self, namespace: &'a Namespace) -> Vec<&'a Capability> {
123        namespace
124            .capabilities
125            .iter()
126            .filter(|cap| self.is_stable(cap))
127            .collect()
128    }
129
130    /// Count stable capabilities.
131    ///
132    /// # Contract
133    ///
134    /// ```text
135    /// ensures: result == self.stable_capabilities(namespace).len()
136    /// ```
137    #[must_use]
138    pub fn stable_count(&self, namespace: &Namespace) -> usize {
139        self.stable_capabilities(namespace).len()
140    }
141}
142
143/// A wall in the stability space: a trust level where stability changes.
144#[derive(Debug, Clone)]
145pub struct Wall {
146    /// The trust level at which the wall occurs
147    pub trust_level: f64,
148    /// The capability that changes stability
149    pub destabilized_class: CapabilityId,
150    /// Direction: +1 if becoming stable, -1 if becoming unstable
151    pub direction: i32,
152    /// Change in stable count when crossing this wall
153    pub count_change: i32,
154}
155
156impl Wall {
157    /// The trust level at which the wall occurs.
158    #[must_use]
159    pub fn trust_level(&self) -> f64 {
160        self.trust_level
161    }
162}
163
164/// Engine for computing wall-crossing phenomena.
165///
166/// Analyzes how the set of stable capabilities changes as the trust level varies.
167#[derive(Debug, Clone)]
168pub struct WallCrossingEngine {
169    /// Grassmannian parameters
170    pub grassmannian: (usize, usize),
171}
172
173impl WallCrossingEngine {
174    /// Create a new wall-crossing engine.
175    #[must_use]
176    pub fn new(grassmannian: (usize, usize)) -> Self {
177        Self { grassmannian }
178    }
179
180    /// Compute all walls for the capabilities in a namespace.
181    ///
182    /// A wall occurs at trust level t where the phase of some capability
183    /// equals 0 or 1, causing a stability transition.
184    ///
185    /// The wall for σ_λ occurs where:
186    /// ```text
187    /// arg(Z_t(σ_λ)) = 0 or π
188    /// ```
189    /// i.e., where t · dim(σ_λ) / codim(σ_λ) reaches critical values.
190    ///
191    /// # Contract
192    ///
193    /// ```text
194    /// ensures: walls are sorted by trust_level
195    /// ensures: each wall corresponds to exactly one capability
196    /// ```
197    pub fn compute_walls(&self, namespace: &Namespace) -> Vec<Wall> {
198        let mut walls = Vec::new();
199
200        for cap in &namespace.capabilities {
201            let codim = cap.codimension() as f64;
202            let dim = cap.schubert_class.dimension() as f64;
203
204            if dim == 0.0 {
205                // Point class: always has phase 1 (purely real, negative)
206                // No wall crossing possible
207                continue;
208            }
209
210            if codim == 0.0 {
211                // Identity class: always stable for t > 0
212                // Wall at t = 0
213                walls.push(Wall {
214                    trust_level: 0.0,
215                    destabilized_class: cap.id.clone(),
216                    direction: 1,
217                    count_change: 1,
218                });
219                continue;
220            }
221
222            // The phase φ(t) = (1/π) · arctan(t · dim / codim) + 1/2
223            // (since re < 0 for codim > 0)
224            //
225            // Phase = 1 when Z is purely real negative: impossible (im ≥ 0 for t ≥ 0)
226            // Phase approaches 1/2 as t → ∞
227            // Phase = 1 only when im = 0 and re < 0: at t = 0
228            //
229            // Wall: transition from unstable (t near 0, phase near 1) to stable
230            // occurs when phase drops below 1:
231            // arg = π - ε means phase = 1 - ε/π
232            //
233            // For the stability condition, the critical trust level is where
234            // the ratio codim/dim determines the wall:
235            let critical_t = codim / dim;
236
237            // Below critical_t: the phase is close to 1 (barely stable or unstable)
238            // Above critical_t: the phase moves toward 1/2 (more stable)
239            walls.push(Wall {
240                trust_level: critical_t,
241                destabilized_class: cap.id.clone(),
242                direction: 1,
243                count_change: 1,
244            });
245        }
246
247        walls.sort_by(|a, b| {
248            a.trust_level
249                .partial_cmp(&b.trust_level)
250                .unwrap_or(std::cmp::Ordering::Equal)
251        });
252
253        walls
254    }
255
256    /// Compute the number of stable capabilities at a given trust level.
257    ///
258    /// # Contract
259    ///
260    /// ```text
261    /// ensures: result <= namespace.capabilities.len()
262    /// ```
263    #[must_use]
264    pub fn stable_count_at(&self, namespace: &Namespace, trust_level: f64) -> usize {
265        let condition = StabilityCondition::standard(self.grassmannian, trust_level);
266        condition.stable_count(namespace)
267    }
268
269    /// Compute the phase diagram: piecewise-constant function
270    /// trust_level → count of stable capabilities.
271    ///
272    /// Returns sorted (trust_level, count) breakpoints.
273    ///
274    /// # Contract
275    ///
276    /// ```text
277    /// ensures: result is sorted by trust_level (first component)
278    /// ensures: result.len() >= 1
279    /// ```
280    #[must_use]
281    pub fn phase_diagram(&self, namespace: &Namespace) -> Vec<(f64, usize)> {
282        let walls = self.compute_walls(namespace);
283
284        if walls.is_empty() {
285            return vec![(0.0, 0)];
286        }
287
288        let mut breakpoints = Vec::new();
289        let mut trust_levels: Vec<f64> = vec![0.001]; // start just above 0
290
291        for wall in &walls {
292            if wall.trust_level > 0.0 {
293                // Sample just before and just after the wall
294                trust_levels.push(wall.trust_level - 0.001);
295                trust_levels.push(wall.trust_level + 0.001);
296            }
297        }
298
299        // Add a high trust level
300        if let Some(last_wall) = walls.last() {
301            trust_levels.push(last_wall.trust_level + 1.0);
302        }
303
304        trust_levels.sort_by(|a, b| a.partial_cmp(b).unwrap());
305        trust_levels.dedup();
306
307        let mut prev_count = None;
308        for &t in &trust_levels {
309            let count = self.stable_count_at(namespace, t);
310            if prev_count != Some(count) {
311                breakpoints.push((t, count));
312                prev_count = Some(count);
313            }
314        }
315
316        breakpoints
317    }
318}
319
320/// Batch compute stable capability counts at multiple trust levels in parallel.
321#[cfg(feature = "parallel")]
322#[must_use]
323pub fn stable_count_batch(
324    grassmannian: (usize, usize),
325    namespace: &Namespace,
326    trust_levels: &[f64],
327) -> Vec<usize> {
328    use rayon::prelude::*;
329    trust_levels
330        .par_iter()
331        .map(|&t| {
332            let cond = StabilityCondition::standard(grassmannian, t);
333            cond.stable_count(namespace)
334        })
335        .collect()
336}
337
338/// Batch compute walls for multiple namespaces in parallel.
339#[cfg(feature = "parallel")]
340#[must_use]
341pub fn compute_walls_batch(
342    grassmannian: (usize, usize),
343    namespaces: &[Namespace],
344) -> Vec<Vec<Wall>> {
345    use rayon::prelude::*;
346    let engine = WallCrossingEngine::new(grassmannian);
347    namespaces
348        .par_iter()
349        .map(|ns| engine.compute_walls(ns))
350        .collect()
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::namespace::Capability;
357    use crate::schubert::SchubertClass;
358
359    fn make_test_namespace() -> Namespace {
360        let pos = SchubertClass::new(vec![], (2, 4)).unwrap();
361        let mut ns = Namespace::new("test", pos);
362
363        // σ_1: codim=1, dim=3
364        let cap1 = Capability::new("c1", "Cap1", vec![1], (2, 4)).unwrap();
365        // σ_2: codim=2, dim=2
366        let cap2 = Capability::new("c2", "Cap2", vec![2], (2, 4)).unwrap();
367        // σ_{1,1}: codim=2, dim=2
368        let cap3 = Capability::new("c3", "Cap3", vec![1, 1], (2, 4)).unwrap();
369
370        ns.grant(cap1).unwrap();
371        ns.grant(cap2).unwrap();
372        ns.grant(cap3).unwrap();
373        ns
374    }
375
376    #[test]
377    fn test_stability_standard() {
378        let ns = make_test_namespace();
379        let cond = StabilityCondition::standard((2, 4), 1.0);
380
381        // At trust level 1.0, capabilities with sufficient dimension should be stable
382        let stable = cond.stable_capabilities(&ns);
383        assert!(!stable.is_empty());
384    }
385
386    #[test]
387    fn test_central_charge() {
388        let cond = StabilityCondition::standard((2, 4), 1.0);
389        let class = SchubertClass::new(vec![1], (2, 4)).unwrap();
390
391        let (re, im) = cond.central_charge(&class);
392        assert_eq!(re, -1.0); // codim = 1
393        assert_eq!(im, 3.0); // trust * dim = 1.0 * 3
394    }
395
396    #[test]
397    fn test_phase_range() {
398        let cond = StabilityCondition::standard((2, 4), 1.0);
399
400        for partition in &[vec![1], vec![2], vec![1, 1], vec![2, 2]] {
401            if let Ok(class) = SchubertClass::new(partition.clone(), (2, 4)) {
402                let phase = cond.phase(&class);
403                assert!(
404                    (0.0..=1.0).contains(&phase),
405                    "Phase {} out of range for {:?}",
406                    phase,
407                    partition
408                );
409            }
410        }
411    }
412
413    #[test]
414    fn test_stability_low_trust() {
415        let ns = make_test_namespace();
416        // At very low trust, high-codim capabilities may not be stable
417        let cond = StabilityCondition::standard((2, 4), 0.01);
418        let stable_count = cond.stable_count(&ns);
419        // With very low trust, fewer capabilities should be stable
420        assert!(stable_count <= ns.capabilities.len());
421    }
422
423    #[test]
424    fn test_stability_high_trust() {
425        let ns = make_test_namespace();
426        // At high trust, most capabilities with nonzero dimension should be stable
427        let cond = StabilityCondition::standard((2, 4), 100.0);
428        let stable = cond.stable_capabilities(&ns);
429        // High trust should stabilize capabilities with dim > 0
430        assert!(!stable.is_empty());
431    }
432
433    #[test]
434    fn test_wall_computation() {
435        let ns = make_test_namespace();
436        let engine = WallCrossingEngine::new((2, 4));
437        let walls = engine.compute_walls(&ns);
438
439        // Should find walls for each capability
440        assert!(!walls.is_empty());
441
442        // Walls should be sorted by trust level
443        for w in walls.windows(2) {
444            assert!(w[0].trust_level <= w[1].trust_level);
445        }
446    }
447
448    #[test]
449    fn test_phase_diagram() {
450        let ns = make_test_namespace();
451        let engine = WallCrossingEngine::new((2, 4));
452        let diagram = engine.phase_diagram(&ns);
453
454        // Diagram should have at least one breakpoint
455        assert!(!diagram.is_empty());
456
457        // Trust levels should be increasing
458        for d in diagram.windows(2) {
459            assert!(d[0].0 < d[1].0);
460        }
461    }
462
463    #[test]
464    fn test_stable_count_monotone() {
465        let ns = make_test_namespace();
466        let engine = WallCrossingEngine::new((2, 4));
467
468        // Stable count should generally increase with trust level
469        // (not strictly, but at high enough trust level it should be maximal)
470        let low = engine.stable_count_at(&ns, 0.01);
471        let high = engine.stable_count_at(&ns, 100.0);
472        assert!(high >= low);
473    }
474
475    #[test]
476    fn test_point_class_stability() {
477        // Point class σ_{2,2} on Gr(2,4): codim=4, dim=0
478        let pos = SchubertClass::new(vec![], (2, 4)).unwrap();
479        let mut ns = Namespace::new("test", pos);
480        let point_cap = Capability::new("pt", "Point", vec![2, 2], (2, 4)).unwrap();
481        ns.grant(point_cap).unwrap();
482
483        let cond = StabilityCondition::standard((2, 4), 1.0);
484        // Point class has dim=0, so Z = (-4, 0) → phase = 1 → not stable (boundary)
485        let stable = cond.stable_count(&ns);
486        assert_eq!(stable, 0);
487    }
488
489    #[cfg(feature = "parallel")]
490    #[test]
491    fn test_stable_count_batch() {
492        let ns = make_test_namespace();
493        let trust_levels = vec![0.01, 0.5, 1.0, 10.0, 100.0];
494        let results = super::stable_count_batch((2, 4), &ns, &trust_levels);
495        assert_eq!(results.len(), 5);
496        // Higher trust should have >= lower trust stable count
497        assert!(results[4] >= results[0]);
498    }
499
500    #[cfg(feature = "parallel")]
501    #[test]
502    fn test_compute_walls_batch() {
503        let ns1 = make_test_namespace();
504        let ns2 = make_test_namespace();
505        let results = super::compute_walls_batch((2, 4), &[ns1, ns2]);
506        assert_eq!(results.len(), 2);
507        // Both should have the same walls
508        assert_eq!(results[0].len(), results[1].len());
509    }
510}