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.
T7: veredicto por difusión de Tarski y cierre de discrepancias
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::lattice::{GaloisRestriction, LatticeCell};
10use crate::nerve::ReplicaId;
11use crate::sheaf::Sheaf;
12use crate::tarski::{LatticeSheaf, TarskiLaplacian};
13
14/// Umbral para considerar no nula una componente del vector de obstrucción.
15const SUPPORT_EPS: f64 = 1e-9;
16
17/// Un nudo: una obstrucción localizada entre réplicas/aristas concretas.
18#[derive(Debug, Clone, PartialEq)]
19pub struct Knot {
20 /// Réplicas implicadas en el nudo.
21 pub replicas: Vec<ReplicaId>,
22 /// Aristas del ciclo obstruido (índices en el nervio).
23 pub edges: Vec<usize>,
24}
25
26/// El veredicto final en lenguaje de sistemas.
27#[derive(Debug, Clone, PartialEq)]
28pub enum Verdict {
29 /// `H¹ = 0`: fusión libre de coordinación.
30 RunsFree,
31 /// `H¹ ≠ 0`: hace falta coordinar, con los nudos localizados.
32 NeedsCoordination {
33 /// Los nudos detectados (uno por generador del cokernel).
34 knots: Vec<Knot>,
35 },
36}
37
38impl Verdict {
39 /// Diagnostica la configuración a partir del haz y su cohomología.
40 ///
41 /// Si `H¹ = 0` → `RunsFree`. Si no, cada vector de la base del cokernel es un
42 /// nudo independiente: las aristas con soporte no nulo lo forman, y sus
43 /// extremos son las réplicas implicadas (§7 paso 5).
44 pub fn diagnose(sheaf: &Sheaf, coho: &Cohomology) -> Verdict {
45 if coho.dim_h1 == 0 {
46 return Verdict::RunsFree;
47 }
48
49 // Offsets de cada arista dentro de C¹ (donde vive el vector de obstrucción).
50 let mut edge_offsets = Vec::with_capacity(sheaf.edge_stalks.len());
51 let mut acc = 0;
52 for &d in &sheaf.edge_stalks {
53 edge_offsets.push(acc);
54 acc += d;
55 }
56
57 let mut knots = Vec::new();
58 for basis_vec in &coho.obstruction_basis {
59 let mut edges = Vec::new();
60 let mut replicas = BTreeSet::new();
61 for (e, (&off, &d)) in edge_offsets.iter().zip(&sheaf.edge_stalks).enumerate() {
62 let touches = basis_vec[off..off + d].iter().any(|x| x.abs() > SUPPORT_EPS);
63 if touches {
64 edges.push(e);
65 let (u, v) = sheaf.edge_endpoints[e];
66 replicas.insert(u);
67 replicas.insert(v);
68 }
69 }
70 knots.push(Knot {
71 replicas: replicas.into_iter().map(ReplicaId).collect(),
72 edges,
73 });
74 }
75
76 Verdict::NeedsCoordination { knots }
77 }
78
79 /// Diagnostica por **difusión de Tarski** sobre un haz de retículos
80 /// (Addendum §C, T7): se harmoniza la siembra a punto fijo y se lee.
81 ///
82 /// - **RunsFree** si el punto fijo reconcilia sin colapsar: ninguna arista
83 /// queda en ⊤ y ningún vértice colapsa. La difusión preserva lo local.
84 /// - **NeedsCoordination** si hay colapso a ⊤: obstrucción semántica real.
85 /// La localización (R-L1, difusa) son las aristas cuyo desacuerdo
86 /// *sobrevive* al punto fijo, y sus réplicas.
87 pub fn via_tarski<L, R>(sheaf: &LatticeSheaf<L, R>, seed: &[L]) -> Verdict
88 where
89 L: LatticeCell + PartialEq,
90 R: GaloisRestriction<L, L>,
91 {
92 let harm = TarskiLaplacian::new(sheaf).harmonize(seed);
93 let top = L::top();
94 let r = &sheaf.restriction;
95
96 let mut edges = Vec::new();
97 let mut replicas = BTreeSet::new();
98 // Aristas cuyo desacuerdo sobrevive al punto fijo (colapsan a ⊤).
99 for (i, &(a, b)) in sheaf.edges.iter().enumerate() {
100 if r.lower(&harm[a]).join(&r.lower(&harm[b])) == top {
101 edges.push(i);
102 replicas.insert(a);
103 replicas.insert(b);
104 }
105 }
106 // Vértices colapsados a ⊤ (por si un colapso no dejara arista superviviente).
107 for (v, h) in harm.iter().enumerate() {
108 if *h == top {
109 replicas.insert(v);
110 }
111 }
112
113 if replicas.is_empty() {
114 Verdict::RunsFree
115 } else {
116 Verdict::NeedsCoordination {
117 knots: vec![Knot {
118 replicas: replicas.into_iter().map(ReplicaId).collect(),
119 edges,
120 }],
121 }
122 }
123 }
124}
125
126impl fmt::Display for Verdict {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Verdict::RunsFree => write!(f, "H¹ = 0 → corre libre (sin jefe)"),
130 Verdict::NeedsCoordination { knots } => {
131 write!(f, "H¹ ≠ 0 → coordina aquí")?;
132 for knot in knots {
133 let reps: Vec<usize> = knot.replicas.iter().map(|r| r.0).collect();
134 write!(f, "\n · nudo entre réplicas {reps:?} (aristas {:?})", knot.edges)?;
135 }
136 Ok(())
137 }
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::cohomology::compute_with;
146 use crate::gf2::Gf2Backend;
147
148 #[test]
149 fn arbol_corre_libre() {
150 let sheaf = Sheaf::constant(3, &[(0, 1), (1, 2)], 1);
151 let coho = compute_with(&sheaf, &Gf2Backend).unwrap();
152 assert_eq!(Verdict::diagnose(&sheaf, &coho), Verdict::RunsFree);
153 }
154
155 #[test]
156 fn dos_triangulos_reportan_dos_nudos() {
157 // Dos triángulos que comparten la arista (1,2): b₁ = 2 → dos ciclos
158 // obstruidos, luego dos nudos (uno por generador del cokernel).
159 let edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)];
160 let sheaf = Sheaf::constant(4, &edges, 1);
161 let coho = compute_with(&sheaf, &Gf2Backend).unwrap();
162 match Verdict::diagnose(&sheaf, &coho) {
163 Verdict::NeedsCoordination { knots } => {
164 assert_eq!(knots.len(), 2, "todos los ciclos obstruidos, no solo uno");
165 // Cada nudo es un ciclo real: al menos 3 aristas y 3 réplicas.
166 for k in &knots {
167 assert!(k.edges.len() >= 3);
168 assert!(k.replicas.len() >= 3);
169 }
170 }
171 Verdict::RunsFree => panic!("dos ciclos deberían obstruir"),
172 }
173 }
174}