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.

⎇ c4920fdc5e1224aee2e931a7c3d421b42f4c9e9e tests/properties.rs Rust · 115 siq'ikuna · 4.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

T4/M3: oráculo CRDT/CALM, batería de validación y DISCREPANCIES c4920fdc5 · Sergio ·

1//! Invariantes basadas en propiedades (§9), que `proptest` intenta romper.
2//!
3//! Tres invariantes:
4//! 1. Solo-suma (monótono) sobre un bosque ⟹ `H¹ = 0`.
5//! 2. Añadir una arista: `H¹` no decrece y `H⁰` no crece.
6//! 3. El haz constante reproduce los números de Betti del grafo.
7//!
8//! La invariante 1 se acota a **bosques** a propósito: sobre grafos cíclicos la
9//! codificación lineal ve la obstrucción topológica aunque el dato sea monótono
10//! (§11 R1) — esa discrepancia está documentada, no es un bug.
11
12use proptest::prelude::*;
13use sheafsync::cohomology::compute_with;
14use sheafsync::gf2::Gf2Backend;
15use sheafsync::oracle::{Config, OracleVerdict, Share};
16use sheafsync::sheaf::Sheaf;
17
18/// `(dim H⁰, dim H¹)` del haz constante `d=1` sobre `(n, edges)`.
19fn coho(n: usize, edges: &[(usize, usize)]) -> (usize, usize) {
20    let sheaf = Sheaf::constant(n, edges, 1);
21    let c = compute_with(&sheaf, &Gf2Backend).unwrap();
22    (c.dim_h0, c.dim_h1)
23}
24
25/// Número de componentes conexas por union-find.
26fn componentes(n: usize, edges: &[(usize, usize)]) -> usize {
27    let mut parent: Vec<usize> = (0..n).collect();
28    fn find(parent: &mut [usize], x: usize) -> usize {
29        let mut r = x;
30        while parent[r] != r {
31            r = parent[r];
32        }
33        // compresión de caminos
34        let mut c = x;
35        while parent[c] != c {
36            let next = parent[c];
37            parent[c] = r;
38            c = next;
39        }
40        r
41    }
42    for &(a, b) in edges {
43        let ra = find(&mut parent, a);
44        let rb = find(&mut parent, b);
45        if ra != rb {
46            parent[ra] = rb;
47        }
48    }
49    (0..n).filter(|&x| find(&mut parent, x) == x).count()
50}
51
52/// Grafo aleatorio: `n` vértices y una lista de aristas (posibles multiaristas).
53fn grafo() -> impl Strategy<Value = (usize, Vec<(usize, usize)>)> {
54    (2usize..=6).prop_flat_map(|n| {
55        let arista = (0..n, 0..n).prop_filter("sin lazos", |(a, b)| a != b);
56        (Just(n), prop::collection::vec(arista, 0..10))
57    })
58}
59
60/// Grafo aleatorio más una arista extra dentro del mismo rango de vértices.
61fn grafo_y_arista() -> impl Strategy<Value = (usize, Vec<(usize, usize)>, (usize, usize))> {
62    (2usize..=6).prop_flat_map(|n| {
63        let arista = (0..n, 0..n).prop_filter("sin lazos", |(a, b)| a != b);
64        (Just(n), prop::collection::vec(arista.clone(), 0..10), arista)
65    })
66}
67
68proptest! {
69    /// Invariante 3: haz constante ⟹ `H⁰ = #componentes`, `H¹ = E − V + C`.
70    #[test]
71    fn haz_constante_reproduce_betti((n, edges) in grafo()) {
72        let (h0, h1) = coho(n, &edges);
73        let c = componentes(n, &edges);
74        let e = edges.len();
75        prop_assert_eq!(h0, c, "H⁰ debe contar componentes");
76        // E − V + C ≥ 0 siempre; lo calculamos sin subdesbordar.
77        prop_assert_eq!(h1 + n, e + c, "H¹ debe ser el primer número de Betti");
78    }
79
80    /// Invariante 2: añadir una arista no reduce `H¹` ni aumenta `H⁰`.
81    #[test]
82    fn anadir_arista_no_reduce_h1_ni_aumenta_h0((n, edges, extra) in grafo_y_arista()) {
83        let (h0, h1) = coho(n, &edges);
84        let mut mas = edges.clone();
85        mas.push(extra);
86        let (h0b, h1b) = coho(n, &mas);
87        prop_assert!(h1b >= h1, "H¹ no debe decrecer al añadir una arista");
88        prop_assert!(h0b <= h0, "H⁰ no debe crecer al añadir una arista");
89    }
90
91    /// Invariante 1: solo-suma (monótono) sobre un bosque ⟹ `H¹ = 0` y el
92    /// oráculo y el haz coinciden en "corre libre".
93    #[test]
94    fn monotono_sobre_bosque_corre_libre((n, edges) in grafo()) {
95        // Quedarnos solo con las aristas que conectan componentes nuevas → bosque.
96        let mut parent: Vec<usize> = (0..n).collect();
97        fn find(parent: &mut [usize], x: usize) -> usize {
98            let mut r = x;
99            while parent[r] != r { r = parent[r]; }
100            r
101        }
102        let mut bosque = Vec::new();
103        for (a, b) in edges {
104            let ra = find(&mut parent, a);
105            let rb = find(&mut parent, b);
106            if ra != rb {
107                parent[ra] = rb;
108                bosque.push(Share::monotona(a, b));
109            }
110        }
111        let config = Config::new("bosque-aleatorio", n, bosque);
112        prop_assert_eq!(config.oracle_verdict(), OracleVerdict::CoordinationFree);
113        prop_assert_eq!(config.sheaf_verdict(), OracleVerdict::CoordinationFree);
114    }
115}