//! `verdict` — traducción del resultado topológico a lenguaje de sistemas (§6.5). //! //! `H¹ = 0 → corre libre` vs `H¹ ≠ 0 → coordina aquí`, con localización. use std::collections::BTreeSet; use std::fmt; use crate::cohomology::Cohomology; use crate::lattice::{GaloisRestriction, LatticeCell}; use crate::nerve::ReplicaId; use crate::sheaf::Sheaf; use crate::tarski::{LatticeSheaf, TarskiLaplacian}; /// Umbral para considerar no nula una componente del vector de obstrucción. const SUPPORT_EPS: f64 = 1e-9; /// Un nudo: una obstrucción localizada entre réplicas/aristas concretas. #[derive(Debug, Clone, PartialEq)] pub struct Knot { /// Réplicas implicadas en el nudo. pub replicas: Vec, /// Aristas del ciclo obstruido (índices en el nervio). pub edges: Vec, } /// El veredicto final en lenguaje de sistemas. #[derive(Debug, Clone, PartialEq)] pub enum Verdict { /// `H¹ = 0`: fusión libre de coordinación. RunsFree, /// `H¹ ≠ 0`: hace falta coordinar, con los nudos localizados. NeedsCoordination { /// Los nudos detectados (uno por generador del cokernel). knots: Vec, }, } impl Verdict { /// Diagnostica la configuración a partir del haz y su cohomología. /// /// Si `H¹ = 0` → `RunsFree`. Si no, cada vector de la base del cokernel es un /// nudo independiente: las aristas con soporte no nulo lo forman, y sus /// extremos son las réplicas implicadas (§7 paso 5). pub fn diagnose(sheaf: &Sheaf, coho: &Cohomology) -> Verdict { if coho.dim_h1 == 0 { return Verdict::RunsFree; } // Offsets de cada arista dentro de C¹ (donde vive el vector de obstrucción). let mut edge_offsets = Vec::with_capacity(sheaf.edge_stalks.len()); let mut acc = 0; for &d in &sheaf.edge_stalks { edge_offsets.push(acc); acc += d; } let mut knots = Vec::new(); for basis_vec in &coho.obstruction_basis { let mut edges = Vec::new(); let mut replicas = BTreeSet::new(); for (e, (&off, &d)) in edge_offsets.iter().zip(&sheaf.edge_stalks).enumerate() { let touches = basis_vec[off..off + d].iter().any(|x| x.abs() > SUPPORT_EPS); if touches { edges.push(e); let (u, v) = sheaf.edge_endpoints[e]; replicas.insert(u); replicas.insert(v); } } knots.push(Knot { replicas: replicas.into_iter().map(ReplicaId).collect(), edges, }); } Verdict::NeedsCoordination { knots } } /// Diagnostica por **difusión de Tarski** sobre un haz de retículos /// (Addendum §C, T7): se harmoniza la siembra a punto fijo y se lee. /// /// - **RunsFree** si el punto fijo reconcilia sin colapsar: ninguna arista /// queda en ⊤ y ningún vértice colapsa. La difusión preserva lo local. /// - **NeedsCoordination** si hay colapso a ⊤: obstrucción semántica real. /// La localización (R-L1, difusa) son las aristas cuyo desacuerdo /// *sobrevive* al punto fijo, y sus réplicas. pub fn via_tarski(sheaf: &LatticeSheaf, seed: &[L]) -> Verdict where L: LatticeCell + PartialEq, R: GaloisRestriction, { let harm = TarskiLaplacian::new(sheaf).harmonize(seed); let top = L::top(); let r = &sheaf.restriction; let mut edges = Vec::new(); let mut replicas = BTreeSet::new(); // Aristas cuyo desacuerdo sobrevive al punto fijo (colapsan a ⊤). for (i, &(a, b)) in sheaf.edges.iter().enumerate() { if r.lower(&harm[a]).join(&r.lower(&harm[b])) == top { edges.push(i); replicas.insert(a); replicas.insert(b); } } // Vértices colapsados a ⊤ (por si un colapso no dejara arista superviviente). for (v, h) in harm.iter().enumerate() { if *h == top { replicas.insert(v); } } if replicas.is_empty() { Verdict::RunsFree } else { Verdict::NeedsCoordination { knots: vec![Knot { replicas: replicas.into_iter().map(ReplicaId).collect(), edges, }], } } } } impl fmt::Display for Verdict { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Verdict::RunsFree => write!(f, "H¹ = 0 → corre libre (sin jefe)"), Verdict::NeedsCoordination { knots } => { write!(f, "H¹ ≠ 0 → coordina aquí")?; for knot in knots { let reps: Vec = knot.replicas.iter().map(|r| r.0).collect(); write!(f, "\n · nudo entre réplicas {reps:?} (aristas {:?})", knot.edges)?; } Ok(()) } } } } #[cfg(test)] mod tests { use super::*; use crate::cohomology::compute_with; use crate::gf2::Gf2Backend; #[test] fn arbol_corre_libre() { let sheaf = Sheaf::constant(3, &[(0, 1), (1, 2)], 1); let coho = compute_with(&sheaf, &Gf2Backend).unwrap(); assert_eq!(Verdict::diagnose(&sheaf, &coho), Verdict::RunsFree); } #[test] fn dos_triangulos_reportan_dos_nudos() { // Dos triángulos que comparten la arista (1,2): b₁ = 2 → dos ciclos // obstruidos, luego dos nudos (uno por generador del cokernel). let edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]; let sheaf = Sheaf::constant(4, &edges, 1); let coho = compute_with(&sheaf, &Gf2Backend).unwrap(); match Verdict::diagnose(&sheaf, &coho) { Verdict::NeedsCoordination { knots } => { assert_eq!(knots.len(), 2, "todos los ciclos obstruidos, no solo uno"); // Cada nudo es un ciclo real: al menos 3 aristas y 3 réplicas. for k in &knots { assert!(k.edges.len() >= 3); assert!(k.replicas.len() >= 3); } } Verdict::RunsFree => panic!("dos ciclos deberían obstruir"), } } }