//! `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::nerve::ReplicaId; use crate::sheaf::Sheaf; /// 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 } } } 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(()) } } } }