tawasuyu

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.

Rust 15Markdown 3TOML 1175 Definitions

⎇ 67c1eb776f64dd43ab986286053d01f4fa8e7780 src/verdict.rs Rust · 174 lines · 6.3 KB History Who touched what raw

T7: veredicto por difusión de Tarski y cierre de discrepancias 67c1eb776 · Sergio ·

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}