gioser

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 24Markdown 15TOML 1358 Definiciones

⎇ main src/sheaf.rs Rust · 136 líneas · 5.1 KB Historia Quién tocó qué crudo

T1: cohomología f64 + prueba de cordura del haz constante dbd6875b0 · Sergio ·

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}