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}