//! `sheaf` — el haz celular y el ensamblado del coboundary `δ` (§6.3, §7). use crate::error::{Result, SheafError}; use crate::linalg::Matrix; /// Mapa de restricción `F(v ⊴ e): F(v) → F(e)`: proyección lineal del estado /// de una réplica sobre el dato de una arista. Matriz `edge_dim × vertex_dim`. pub type RestrictionMap = Matrix; /// Un haz celular sobre el nervio. #[derive(Debug, Default, Clone)] pub struct Sheaf { /// Para cada réplica: la dimensión de su stalk `F(v)`. pub vertex_stalks: Vec, /// Para cada arista: la dimensión de su stalk `F(e)`. pub edge_stalks: Vec, /// Mapas de restricción: por arista, dos matrices (una por extremo). pub restrictions: Vec<(RestrictionMap, RestrictionMap)>, /// Extremos `(u, v)` de cada arista, como índices en `vertex_stalks`. pub edge_endpoints: Vec<(usize, usize)>, } impl Sheaf { /// Dimensión de `C⁰ = ⊕_v F(v)` (espacio de cocadenas de vértice). pub fn dim_c0(&self) -> usize { self.vertex_stalks.iter().sum() } /// Dimensión de `C¹ = ⊕_e F(e)` (espacio de cocadenas de arista). pub fn dim_c1(&self) -> usize { self.edge_stalks.iter().sum() } /// Desplazamiento de la columna donde empieza el bloque del vértice `v` en `C⁰`. fn vertex_offset(&self, v: usize) -> usize { self.vertex_stalks[..v].iter().sum() } /// Desplazamiento de la fila donde empieza el bloque de la arista `e` en `C¹`. fn edge_offset(&self, e: usize) -> usize { self.edge_stalks[..e].iter().sum() } /// Comprueba que los tamaños de las matrices de restricción cuadran con las /// dimensiones declaradas de stalks (fila = arista, columna = vértice). fn validate(&self) -> Result<()> { let ne = self.edge_stalks.len(); if self.restrictions.len() != ne || self.edge_endpoints.len() != ne { return Err(SheafError::DimensionMismatch(format!( "aristas: {ne} stalks, {} restricciones, {} extremos", self.restrictions.len(), self.edge_endpoints.len() ))); } let nv = self.vertex_stalks.len(); for (e, ((ru, rv), &(u, v))) in self .restrictions .iter() .zip(self.edge_endpoints.iter()) .enumerate() { if u >= nv || v >= nv { return Err(SheafError::MissingEntity(format!( "arista {e} referencia vértice inexistente ({u},{v}) de {nv}" ))); } let ed = self.edge_stalks[e]; if ru.rows != ed || ru.cols != self.vertex_stalks[u] { return Err(SheafError::DimensionMismatch(format!( "arista {e}: R_u es {}×{}, se esperaba {ed}×{}", ru.rows, ru.cols, self.vertex_stalks[u] ))); } if rv.rows != ed || rv.cols != self.vertex_stalks[v] { return Err(SheafError::DimensionMismatch(format!( "arista {e}: R_v es {}×{}, se esperaba {ed}×{}", rv.rows, rv.cols, self.vertex_stalks[v] ))); } } Ok(()) } /// Ensambla la matriz global de coboundary `δ: C⁰ → C¹`. /// /// Convención (§6.3): para arista `e=(u,v)`, /// `(δx)_e = R_{u⊴e}·x_u − R_{v⊴e}·x_v`. /// /// Bloque de la arista `e`: `+R_u` en las columnas de `u`, `−R_v` en las de /// `v`. Se acumula (`+=`) para tolerar lazos o multiaristas. pub fn coboundary(&self) -> Result { self.validate()?; let mut delta = Matrix::zeros(self.dim_c1(), self.dim_c0()); for (e, ((ru, rv), &(u, v))) in self .restrictions .iter() .zip(self.edge_endpoints.iter()) .enumerate() { let row0 = self.edge_offset(e); let ed = self.edge_stalks[e]; let ucol0 = self.vertex_offset(u); for i in 0..ed { for j in 0..ru.cols { delta.add_to(row0 + i, ucol0 + j, ru.get(i, j)); } } let vcol0 = self.vertex_offset(v); for i in 0..ed { for j in 0..rv.cols { delta.add_to(row0 + i, vcol0 + j, -rv.get(i, j)); } } } Ok(delta) } /// Construye el **haz constante** de dimensión de stalk `d` sobre un grafo: /// todas las stalks (vértice y arista) valen `d`, y toda restricción es la /// identidad. Es el andamio de la prueba de cordura topológica (§7.2). /// /// `num_vertices` réplicas y `edges` como pares `(u, v)` de índices. pub fn constant(num_vertices: usize, edges: &[(usize, usize)], d: usize) -> Self { let id = Matrix::identity(d); Self { vertex_stalks: vec![d; num_vertices], edge_stalks: vec![d; edges.len()], restrictions: edges.iter().map(|_| (id.clone(), id.clone())).collect(), edge_endpoints: edges.to_vec(), } } }