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.
T1: cohomología f64 + prueba de cordura del haz constante
1//! `sheaf` — el haz celular y el ensamblado del coboundary `δ` (§6.3, §7).
2
3use crate::error::{Result, SheafError};
4use crate::linalg::Matrix;
5
6/// Mapa de restricción `F(v ⊴ e): F(v) → F(e)`: proyección lineal del estado
7/// de una réplica sobre el dato de una arista. Matriz `edge_dim × vertex_dim`.
8pub type RestrictionMap = Matrix;
9
10/// Un haz celular sobre el nervio.
11#[derive(Debug, Default, Clone)]
12pub struct Sheaf {
13 /// Para cada réplica: la dimensión de su stalk `F(v)`.
14 pub vertex_stalks: Vec<usize>,
15 /// Para cada arista: la dimensión de su stalk `F(e)`.
16 pub edge_stalks: Vec<usize>,
17 /// Mapas de restricción: por arista, dos matrices (una por extremo).
18 pub restrictions: Vec<(RestrictionMap, RestrictionMap)>,
19 /// Extremos `(u, v)` de cada arista, como índices en `vertex_stalks`.
20 pub edge_endpoints: Vec<(usize, usize)>,
21}
22
23impl Sheaf {
24 /// Dimensión de `C⁰ = ⊕_v F(v)` (espacio de cocadenas de vértice).
25 pub fn dim_c0(&self) -> usize {
26 self.vertex_stalks.iter().sum()
27 }
28
29 /// Dimensión de `C¹ = ⊕_e F(e)` (espacio de cocadenas de arista).
30 pub fn dim_c1(&self) -> usize {
31 self.edge_stalks.iter().sum()
32 }
33
34 /// Desplazamiento de la columna donde empieza el bloque del vértice `v` en `C⁰`.
35 fn vertex_offset(&self, v: usize) -> usize {
36 self.vertex_stalks[..v].iter().sum()
37 }
38
39 /// Desplazamiento de la fila donde empieza el bloque de la arista `e` en `C¹`.
40 fn edge_offset(&self, e: usize) -> usize {
41 self.edge_stalks[..e].iter().sum()
42 }
43
44 /// Comprueba que los tamaños de las matrices de restricción cuadran con las
45 /// dimensiones declaradas de stalks (fila = arista, columna = vértice).
46 fn validate(&self) -> Result<()> {
47 let ne = self.edge_stalks.len();
48 if self.restrictions.len() != ne || self.edge_endpoints.len() != ne {
49 return Err(SheafError::DimensionMismatch(format!(
50 "aristas: {ne} stalks, {} restricciones, {} extremos",
51 self.restrictions.len(),
52 self.edge_endpoints.len()
53 )));
54 }
55 let nv = self.vertex_stalks.len();
56 for (e, ((ru, rv), &(u, v))) in self
57 .restrictions
58 .iter()
59 .zip(self.edge_endpoints.iter())
60 .enumerate()
61 {
62 if u >= nv || v >= nv {
63 return Err(SheafError::MissingEntity(format!(
64 "arista {e} referencia vértice inexistente ({u},{v}) de {nv}"
65 )));
66 }
67 let ed = self.edge_stalks[e];
68 if ru.rows != ed || ru.cols != self.vertex_stalks[u] {
69 return Err(SheafError::DimensionMismatch(format!(
70 "arista {e}: R_u es {}×{}, se esperaba {ed}×{}",
71 ru.rows, ru.cols, self.vertex_stalks[u]
72 )));
73 }
74 if rv.rows != ed || rv.cols != self.vertex_stalks[v] {
75 return Err(SheafError::DimensionMismatch(format!(
76 "arista {e}: R_v es {}×{}, se esperaba {ed}×{}",
77 rv.rows, rv.cols, self.vertex_stalks[v]
78 )));
79 }
80 }
81 Ok(())
82 }
83
84 /// Ensambla la matriz global de coboundary `δ: C⁰ → C¹`.
85 ///
86 /// Convención (§6.3): para arista `e=(u,v)`,
87 /// `(δx)_e = R_{u⊴e}·x_u − R_{v⊴e}·x_v`.
88 ///
89 /// Bloque de la arista `e`: `+R_u` en las columnas de `u`, `−R_v` en las de
90 /// `v`. Se acumula (`+=`) para tolerar lazos o multiaristas.
91 pub fn coboundary(&self) -> Result<Matrix> {
92 self.validate()?;
93 let mut delta = Matrix::zeros(self.dim_c1(), self.dim_c0());
94
95 for (e, ((ru, rv), &(u, v))) in self
96 .restrictions
97 .iter()
98 .zip(self.edge_endpoints.iter())
99 .enumerate()
100 {
101 let row0 = self.edge_offset(e);
102 let ed = self.edge_stalks[e];
103
104 let ucol0 = self.vertex_offset(u);
105 for i in 0..ed {
106 for j in 0..ru.cols {
107 delta.add_to(row0 + i, ucol0 + j, ru.get(i, j));
108 }
109 }
110
111 let vcol0 = self.vertex_offset(v);
112 for i in 0..ed {
113 for j in 0..rv.cols {
114 delta.add_to(row0 + i, vcol0 + j, -rv.get(i, j));
115 }
116 }
117 }
118
119 Ok(delta)
120 }
121
122 /// Construye el **haz constante** de dimensión de stalk `d` sobre un grafo:
123 /// todas las stalks (vértice y arista) valen `d`, y toda restricción es la
124 /// identidad. Es el andamio de la prueba de cordura topológica (§7.2).
125 ///
126 /// `num_vertices` réplicas y `edges` como pares `(u, v)` de índices.
127 pub fn constant(num_vertices: usize, edges: &[(usize, usize)], d: usize) -> Self {
128 let id = Matrix::identity(d);
129 Self {
130 vertex_stalks: vec![d; num_vertices],
131 edge_stalks: vec![d; edges.len()],
132 restrictions: edges.iter().map(|_| (id.clone(), id.clone())).collect(),
133 edge_endpoints: edges.to_vec(),
134 }
135 }
136}