Skip to main content

sheaf_coherence/
lib.rs

1//! # sheaf-coherence
2//!
3//! Cellular sheaf coherence for multi-agent belief alignment.
4//!
5//! A **cellular sheaf** assigns vector spaces (stalks) to nodes and linear maps
6//! (restriction maps) to edges of a graph. The **sheaf Laplacian** `L_F` measures
7//! how much a section (belief assignment) disagrees across edges:
8//!
9//! - `L_F x = 0` ⟹ **global section** (perfect agreement)
10//! - `||L_F x|| / ||x||` ⟹ **disagreement level**
11//! - `1 - ||L_F x|| / ||x||` ⟹ **alignment score**
12//!
13//! # Quick Start
14//!
15//! ```
16//! use sheaf_coherence::{CellularSheaf, SheafLaplacian, CoherenceMeasure, AgentSheaf, AgentBelief};
17//!
18//! // Build a complete sheaf on 3 nodes with 2D stalks
19//! let sheaf = CellularSheaf::complete(3, 2).unwrap();
20//! let lap = SheafLaplacian::from_sheaf(&sheaf).unwrap();
21//!
22//! // Perfect agreement → alignment = 1.0
23//! let beliefs = vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0];
24//! let coherence = CoherenceMeasure::from_flat(&sheaf, &beliefs, 100, 1e-10).unwrap();
25//! assert!(coherence.alignment > 0.99);
26//!
27//! // Agent-based interface
28//! let agents = vec![
29//!     AgentBelief::new("alice", vec![1.0, 0.0], 0.9),
30//!     AgentBelief::new("bob",   vec![1.0, 0.0], 0.8),
31//!     AgentBelief::new("carol", vec![0.0, 1.0], 0.7),
32//! ];
33//! let asheaf = AgentSheaf::complete(agents).unwrap();
34//! let coh = asheaf.coherence(100, 1e-10).unwrap();
35//! ```
36
37pub mod agent;
38pub mod coherence;
39pub mod error;
40pub mod laplacian;
41pub mod section;
42pub mod sheaf;
43
44pub use agent::{AgentBelief, AgentSheaf};
45pub use coherence::CoherenceMeasure;
46pub use error::SheafError;
47pub use laplacian::SheafLaplacian;
48pub use section::GlobalSection;
49pub use sheaf::{CellularSheaf, SheafBuilder};
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    // ── CellularSheaf construction ──────────────────────────────
56
57    #[test]
58    fn test_constant_sheaf() {
59        let s = CellularSheaf::constant(4, 3).unwrap();
60        assert_eq!(s.node_count(), 4);
61        assert_eq!(s.total_dim(), 12);
62        assert!(s.restriction_maps.is_empty());
63    }
64
65    #[test]
66    fn test_path_sheaf() {
67        let s = CellularSheaf::path(3, 2).unwrap();
68        assert_eq!(s.node_count(), 3);
69        assert_eq!(s.restriction_maps.len(), 2); // edges: 0-1, 1-2
70    }
71
72    #[test]
73    fn test_cycle_sheaf() {
74        let s = CellularSheaf::cycle(4, 2).unwrap();
75        assert_eq!(s.restriction_maps.len(), 4); // 3 path edges + 1 cycle-closing
76    }
77
78    #[test]
79    fn test_complete_sheaf() {
80        let s = CellularSheaf::complete(4, 2).unwrap();
81        // Complete graph K4 has 6 edges
82        assert_eq!(s.restriction_maps.len(), 6);
83    }
84
85    #[test]
86    fn test_empty_sheaf_rejected() {
87        assert!(CellularSheaf::constant(0, 2).is_err());
88    }
89
90    #[test]
91    fn test_cycle_too_small() {
92        assert!(CellularSheaf::cycle(2, 1).is_err());
93    }
94
95    #[test]
96    fn test_builder_custom_sheaf() {
97        let s = CellularSheaf::builder()
98            .add_node(2)
99            .add_node(2)
100            .add_node(3)
101            .add_edge(0, 1, vec![vec![1.0, 0.0], vec![0.0, 1.0]])
102            .build()
103            .unwrap();
104        assert_eq!(s.node_count(), 3);
105        assert_eq!(s.total_dim(), 7);
106        assert_eq!(s.restriction_maps.len(), 1);
107    }
108
109    #[test]
110    fn test_dimension_mismatch_detected() {
111        // Edge (0,1): map is 2x2 but stalk[1]=3
112        let res = CellularSheaf::builder()
113            .add_node(2)
114            .add_node(3)
115            .add_edge(0, 1, vec![vec![1.0, 0.0], vec![0.0, 1.0]])
116            .build();
117        assert!(matches!(res, Err(SheafError::DimensionMismatch { .. })));
118    }
119
120    #[test]
121    fn test_get_restriction_map() {
122        let s = CellularSheaf::path(3, 2).unwrap();
123        let m = s.get_restriction_map(0, 1);
124        assert!(m.is_some());
125        let m = s.get_restriction_map(1, 0);
126        assert!(m.is_some()); // undirected lookup
127        let m = s.get_restriction_map(0, 2);
128        assert!(m.is_none());
129    }
130
131    // ── SheafLaplacian ──────────────────────────────────────────
132
133    #[test]
134    fn test_laplacian_single_node() {
135        let s = CellularSheaf::constant(1, 2).unwrap();
136        let lap = SheafLaplacian::from_sheaf(&s).unwrap();
137        assert_eq!(lap.n, 2);
138        // Zero matrix (no edges)
139        assert_eq!(lap.matrix, vec![vec![0.0; 2]; 2]);
140    }
141
142    #[test]
143    fn test_laplacian_path_2_nodes() {
144        let s = CellularSheaf::path(2, 1).unwrap();
145        let lap = SheafLaplacian::from_sheaf(&s).unwrap();
146        // Identity restriction map on 1D stalks: F^T F = [[1]]
147        // L = [[1, -1], [-1, 1]]
148        assert_eq!(lap.matrix, vec![vec![1.0, -1.0], vec![-1.0, 1.0]]);
149    }
150
151    #[test]
152    fn test_laplacian_complete_3_nodes_1d() {
153        let s = CellularSheaf::complete(3, 1).unwrap();
154        let lap = SheafLaplacian::from_sheaf(&s).unwrap();
155        // 3 edges, each contributes [[1]] to diag and -[[1]] to off-diag
156        // Each node has degree 2: diag = [2, 2, 2], off-diag = -1
157        assert!((lap.matrix[0][0] - 2.0).abs() < 1e-10);
158        assert!((lap.matrix[0][1] - (-1.0)).abs() < 1e-10);
159    }
160
161    #[test]
162    fn test_laplacian_quadratic_form_constant_section() {
163        // Constant section on path should have zero quadratic form
164        let s = CellularSheaf::path(3, 2).unwrap();
165        let lap = SheafLaplacian::from_sheaf(&s).unwrap();
166        let x = vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0]; // same value at each node
167        let q = lap.quadratic_form(&x);
168        assert!(q.abs() < 1e-10, "constant section should have zero energy, got {q}");
169    }
170
171    #[test]
172    fn test_laplacian_apply_zero_for_global_section() {
173        let s = CellularSheaf::complete(3, 2).unwrap();
174        let lap = SheafLaplacian::from_sheaf(&s).unwrap();
175        // All nodes same value → global section → L_F x = 0
176        let x = vec![2.0, 3.0, 2.0, 3.0, 2.0, 3.0];
177        let lx = lap.apply(&x);
178        for v in &lx {
179            assert!(v.abs() < 1e-10, "L_F x should be zero for global section");
180        }
181    }
182
183    #[test]
184    fn test_laplacian_eigenvalues() {
185        let s = CellularSheaf::path(2, 1).unwrap();
186        let lap = SheafLaplacian::from_sheaf(&s).unwrap();
187        // [[1,-1],[-1,1]] has eigenvalues 0 and 2
188        let (largest, _) = lap.power_iteration(500, 1e-10);
189        assert!((largest - 2.0).abs() < 0.05, "largest eigenvalue should be ~2, got {largest}");
190        // Smallest should be near 0
191        let (smallest, _) = lap.smallest_eigenvalue(500, 1e-10);
192        assert!(smallest.abs() < 0.05, "smallest eigenvalue should be ~0, got {smallest}");
193    }
194
195    // ── GlobalSection ───────────────────────────────────────────
196
197    #[test]
198    fn test_global_section_exact() {
199        let s = CellularSheaf::complete(3, 1).unwrap();
200        let values = vec![vec![1.0], vec![1.0], vec![1.0]];
201        let gs = GlobalSection::new(&s, values, 1e-8).unwrap();
202        assert!(gs.is_exact);
203        assert!(gs.residual < 1e-8);
204    }
205
206    #[test]
207    fn test_global_section_not_exact() {
208        let s = CellularSheaf::complete(3, 1).unwrap();
209        let values = vec![vec![1.0], vec![0.0], vec![0.0]];
210        let gs = GlobalSection::new(&s, values, 1e-8).unwrap();
211        assert!(!gs.is_exact);
212        assert!(gs.residual > 0.1);
213    }
214
215    #[test]
216    fn test_find_global_section() {
217        let s = CellularSheaf::complete(3, 1).unwrap();
218        let gs = GlobalSection::find(&s, 200, 1e-10).unwrap();
219        assert!(gs.is_exact); // constant sheaf on connected graph has global section
220    }
221
222    #[test]
223    fn test_section_flatten() {
224        let s = CellularSheaf::path(2, 2).unwrap();
225        let values = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
226        let gs = GlobalSection::new(&s, values, 1e-8).unwrap();
227        assert_eq!(gs.flatten(), vec![1.0, 2.0, 3.0, 4.0]);
228    }
229
230    // ── CoherenceMeasure ────────────────────────────────────────
231
232    #[test]
233    fn test_perfect_coherence() {
234        let s = CellularSheaf::complete(3, 2).unwrap();
235        let x = vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0];
236        let c = CoherenceMeasure::from_flat(&s, &x, 100, 1e-10).unwrap();
237        assert!(c.alignment > 0.99, "alignment = {}", c.alignment);
238        for d in &c.disagreement {
239            assert!(d < &0.01, "per-edge disagreement should be ~0");
240        }
241    }
242
243    #[test]
244    fn test_zero_coherence() {
245        let s = CellularSheaf::path(2, 1).unwrap();
246        let x = vec![1.0, -1.0]; // opposite beliefs
247        let c = CoherenceMeasure::from_flat(&s, &x, 100, 1e-10).unwrap();
248        assert!(c.alignment < 0.01, "alignment should be near 0, got {}", c.alignment);
249    }
250
251    #[test]
252    fn test_partial_coherence() {
253        let s = CellularSheaf::complete(3, 2).unwrap();
254        // Two agents agree, one partially disagrees
255        let x = vec![1.0, 0.0, 1.0, 0.0, 0.8, 0.6];
256        let c = CoherenceMeasure::from_flat(&s, &x, 100, 1e-10).unwrap();
257        // Not perfectly aligned (third agent differs), not maximally misaligned
258        assert!(c.alignment > 0.0 && c.alignment < 1.0, "alignment = {}", c.alignment);
259    }
260
261    #[test]
262    fn test_disagreement_per_edge() {
263        let s = CellularSheaf::path(3, 1).unwrap();
264        let x = vec![0.0, 1.0, 2.0];
265        let c = CoherenceMeasure::from_flat(&s, &x, 100, 1e-10).unwrap();
266        assert_eq!(c.disagreement.len(), 2); // 2 edges in path of 3
267    }
268
269    #[test]
270    fn test_avg_and_max_disagreement() {
271        let s = CellularSheaf::path(3, 1).unwrap();
272        let x = vec![0.0, 1.0, 2.0];
273        let c = CoherenceMeasure::from_flat(&s, &x, 100, 1e-10).unwrap();
274        assert!(c.max_disagreement() >= c.avg_disagreement());
275    }
276
277    #[test]
278    fn test_is_aligned() {
279        let s = CellularSheaf::complete(3, 1).unwrap();
280        let x = vec![1.0, 1.0, 1.0];
281        let c = CoherenceMeasure::from_flat(&s, &x, 100, 1e-10).unwrap();
282        assert!(c.is_aligned(0.95));
283    }
284
285    #[test]
286    fn test_dominant_mode_nonempty() {
287        let s = CellularSheaf::complete(3, 2).unwrap();
288        let x = vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0];
289        let c = CoherenceMeasure::from_flat(&s, &x, 100, 1e-10).unwrap();
290        assert_eq!(c.dominant_mode.len(), 6);
291    }
292
293    // ── AgentSheaf ──────────────────────────────────────────────
294
295    #[test]
296    fn test_agent_sheaf_complete() {
297        let agents = vec![
298            AgentBelief::new("a", vec![1.0, 0.0], 0.9),
299            AgentBelief::new("b", vec![1.0, 0.0], 0.8),
300        ];
301        let asheaf = AgentSheaf::complete(agents).unwrap();
302        assert_eq!(asheaf.len(), 2);
303        assert_eq!(asheaf.sheaf.restriction_maps.len(), 1);
304    }
305
306    #[test]
307    fn test_agent_sheaf_path() {
308        let agents = vec![
309            AgentBelief::new("a", vec![1.0], 1.0),
310            AgentBelief::new("b", vec![1.0], 1.0),
311            AgentBelief::new("c", vec![1.0], 1.0),
312        ];
313        let asheaf = AgentSheaf::path(agents).unwrap();
314        assert_eq!(asheaf.sheaf.restriction_maps.len(), 2);
315    }
316
317    #[test]
318    fn test_agent_sheaf_custom_edges() {
319        let agents = vec![
320            AgentBelief::new("a", vec![1.0], 1.0),
321            AgentBelief::new("b", vec![1.0], 1.0),
322            AgentBelief::new("c", vec![1.0], 1.0),
323        ];
324        let asheaf = AgentSheaf::with_edges(agents, &[(0, 1), (1, 2)]).unwrap();
325        assert_eq!(asheaf.sheaf.restriction_maps.len(), 2);
326    }
327
328    #[test]
329    fn test_agent_coherence_perfect() {
330        let agents = vec![
331            AgentBelief::new("a", vec![5.0, 3.0], 1.0),
332            AgentBelief::new("b", vec![5.0, 3.0], 1.0),
333            AgentBelief::new("c", vec![5.0, 3.0], 1.0),
334        ];
335        let asheaf = AgentSheaf::complete(agents).unwrap();
336        let coh = asheaf.coherence(200, 1e-10).unwrap();
337        assert!(coh.alignment > 0.99);
338    }
339
340    #[test]
341    fn test_agent_global_section() {
342        let agents = vec![
343            AgentBelief::new("a", vec![1.0, 0.0], 1.0),
344            AgentBelief::new("b", vec![1.0, 0.0], 1.0),
345        ];
346        let asheaf = AgentSheaf::complete(agents).unwrap();
347        let gs = asheaf.global_section(1e-8).unwrap();
348        assert!(gs.is_exact);
349    }
350
351    #[test]
352    fn test_agent_dimension_mismatch() {
353        let agents = vec![
354            AgentBelief::new("a", vec![1.0, 0.0], 1.0),
355            AgentBelief::new("b", vec![1.0], 1.0), // wrong dim
356        ];
357        assert!(AgentSheaf::complete(agents).is_err());
358    }
359
360    #[test]
361    fn test_agent_flat_beliefs() {
362        let agents = vec![
363            AgentBelief::new("a", vec![1.0, 2.0], 1.0),
364            AgentBelief::new("b", vec![3.0, 4.0], 1.0),
365        ];
366        let asheaf = AgentSheaf::complete(agents).unwrap();
367        assert_eq!(asheaf.flat_beliefs(), vec![1.0, 2.0, 3.0, 4.0]);
368    }
369
370    #[test]
371    fn test_agent_confidence_clamped() {
372        let a = AgentBelief::new("x", vec![1.0], 1.5);
373        assert!((a.confidence - 1.0).abs() < 1e-10);
374        let a = AgentBelief::new("x", vec![1.0], -0.5);
375        assert!(a.confidence.abs() < 1e-10);
376    }
377
378    #[test]
379    fn test_agent_empty_rejected() {
380        let agents: Vec<AgentBelief> = vec![];
381        assert!(AgentSheaf::complete(agents).is_err());
382    }
383
384    // ── Serialization ───────────────────────────────────────────
385
386    #[test]
387    fn test_serde_roundtrip() {
388        let s = CellularSheaf::complete(3, 2).unwrap();
389        let json = serde_json::to_string(&s).unwrap();
390        let s2: CellularSheaf = serde_json::from_str(&json).unwrap();
391        assert_eq!(s.node_count(), s2.node_count());
392        assert_eq!(s.restriction_maps.len(), s2.restriction_maps.len());
393    }
394}