tawasuyu/hifas
Motor en Rust para decidir, con demostración y no a ojo, cuándo un conjunto de réplicas local-first puede operar sin coordinación y cuándo no — y, cuando la respuesta es "sí, dentro de límites", proveer el mecanismo que lo garantiza.
T3: backend GF(2) exacto + migración a aritmética sin epsilon
1//! `verdict` — traducción del resultado topológico a lenguaje de sistemas (§6.5).
2//!
3//! `H¹ = 0 → corre libre` vs `H¹ ≠ 0 → coordina aquí`, con localización.
4
5use std::collections::BTreeSet;
6use std::fmt;
7
8use crate::cohomology::Cohomology;
9use crate::nerve::ReplicaId;
10use crate::sheaf::Sheaf;
11
12/// Umbral para considerar no nula una componente del vector de obstrucción.
13const SUPPORT_EPS: f64 = 1e-9;
14
15/// Un nudo: una obstrucción localizada entre réplicas/aristas concretas.
16#[derive(Debug, Clone, PartialEq)]
17pub struct Knot {
18 /// Réplicas implicadas en el nudo.
19 pub replicas: Vec<ReplicaId>,
20 /// Aristas del ciclo obstruido (índices en el nervio).
21 pub edges: Vec<usize>,
22}
23
24/// El veredicto final en lenguaje de sistemas.
25#[derive(Debug, Clone, PartialEq)]
26pub enum Verdict {
27 /// `H¹ = 0`: fusión libre de coordinación.
28 RunsFree,
29 /// `H¹ ≠ 0`: hace falta coordinar, con los nudos localizados.
30 NeedsCoordination {
31 /// Los nudos detectados (uno por generador del cokernel).
32 knots: Vec<Knot>,
33 },
34}
35
36impl Verdict {
37 /// Diagnostica la configuración a partir del haz y su cohomología.
38 ///
39 /// Si `H¹ = 0` → `RunsFree`. Si no, cada vector de la base del cokernel es un
40 /// nudo independiente: las aristas con soporte no nulo lo forman, y sus
41 /// extremos son las réplicas implicadas (§7 paso 5).
42 pub fn diagnose(sheaf: &Sheaf, coho: &Cohomology) -> Verdict {
43 if coho.dim_h1 == 0 {
44 return Verdict::RunsFree;
45 }
46
47 // Offsets de cada arista dentro de C¹ (donde vive el vector de obstrucción).
48 let mut edge_offsets = Vec::with_capacity(sheaf.edge_stalks.len());
49 let mut acc = 0;
50 for &d in &sheaf.edge_stalks {
51 edge_offsets.push(acc);
52 acc += d;
53 }
54
55 let mut knots = Vec::new();
56 for basis_vec in &coho.obstruction_basis {
57 let mut edges = Vec::new();
58 let mut replicas = BTreeSet::new();
59 for (e, (&off, &d)) in edge_offsets.iter().zip(&sheaf.edge_stalks).enumerate() {
60 let touches = basis_vec[off..off + d].iter().any(|x| x.abs() > SUPPORT_EPS);
61 if touches {
62 edges.push(e);
63 let (u, v) = sheaf.edge_endpoints[e];
64 replicas.insert(u);
65 replicas.insert(v);
66 }
67 }
68 knots.push(Knot {
69 replicas: replicas.into_iter().map(ReplicaId).collect(),
70 edges,
71 });
72 }
73
74 Verdict::NeedsCoordination { knots }
75 }
76}
77
78impl fmt::Display for Verdict {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Verdict::RunsFree => write!(f, "H¹ = 0 → corre libre (sin jefe)"),
82 Verdict::NeedsCoordination { knots } => {
83 write!(f, "H¹ ≠ 0 → coordina aquí")?;
84 for knot in knots {
85 let reps: Vec<usize> = knot.replicas.iter().map(|r| r.0).collect();
86 write!(f, "\n · nudo entre réplicas {reps:?} (aristas {:?})", knot.edges)?;
87 }
88 Ok(())
89 }
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::cohomology::compute_with;
98 use crate::gf2::Gf2Backend;
99
100 #[test]
101 fn arbol_corre_libre() {
102 let sheaf = Sheaf::constant(3, &[(0, 1), (1, 2)], 1);
103 let coho = compute_with(&sheaf, &Gf2Backend).unwrap();
104 assert_eq!(Verdict::diagnose(&sheaf, &coho), Verdict::RunsFree);
105 }
106
107 #[test]
108 fn dos_triangulos_reportan_dos_nudos() {
109 // Dos triángulos que comparten la arista (1,2): b₁ = 2 → dos ciclos
110 // obstruidos, luego dos nudos (uno por generador del cokernel).
111 let edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)];
112 let sheaf = Sheaf::constant(4, &edges, 1);
113 let coho = compute_with(&sheaf, &Gf2Backend).unwrap();
114 match Verdict::diagnose(&sheaf, &coho) {
115 Verdict::NeedsCoordination { knots } => {
116 assert_eq!(knots.len(), 2, "todos los ciclos obstruidos, no solo uno");
117 // Cada nudo es un ciclo real: al menos 3 aristas y 3 réplicas.
118 for k in &knots {
119 assert!(k.edges.len() >= 3);
120 assert!(k.replicas.len() >= 3);
121 }
122 }
123 Verdict::RunsFree => panic!("dos ciclos deberían obstruir"),
124 }
125 }
126}