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

⎇ 9de0cc601fd2eb7b9cf9b8a26ef8ed1ac1c9cd8d tests/properties.rs Rust · 126 líneas · 5.0 KB Historia Quién tocó qué crudo

T7: veredicto por difusión de Tarski y cierre de discrepancias 67c1eb776 · 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 nuevo de la pista de retículos (Addendum §F.3), el que el MVP
92    /// lineal NO podía satisfacer: **monótono sobre grafo arbitrario (cíclico
93    /// incluido) ⟹ corre libre** por la ruta de Tarski.
94    #[test]
95    fn monotono_sobre_grafo_arbitrario_corre_libre_via_tarski((n, edges) in grafo()) {
96        let shares = edges.iter().map(|&(a, b)| Share::monotona(a, b)).collect();
97        let config = Config::new("mono-arbitrario", n, shares);
98        prop_assert_eq!(config.oracle_verdict(), OracleVerdict::CoordinationFree);
99        prop_assert_eq!(config.tarski_verdict(), OracleVerdict::CoordinationFree);
100    }
101
102    /// Invariante 1 (MVP lineal): solo-suma sobre un bosque ⟹ `H¹ = 0` y el
103    /// oráculo y el haz lineal coinciden en "corre libre".
104    #[test]
105    fn monotono_sobre_bosque_corre_libre((n, edges) in grafo()) {
106        // Quedarnos solo con las aristas que conectan componentes nuevas → bosque.
107        let mut parent: Vec<usize> = (0..n).collect();
108        fn find(parent: &mut [usize], x: usize) -> usize {
109            let mut r = x;
110            while parent[r] != r { r = parent[r]; }
111            r
112        }
113        let mut bosque = Vec::new();
114        for (a, b) in edges {
115            let ra = find(&mut parent, a);
116            let rb = find(&mut parent, b);
117            if ra != rb {
118                parent[ra] = rb;
119                bosque.push(Share::monotona(a, b));
120            }
121        }
122        let config = Config::new("bosque-aleatorio", n, bosque);
123        prop_assert_eq!(config.oracle_verdict(), OracleVerdict::CoordinationFree);
124        prop_assert_eq!(config.sheaf_verdict(), OracleVerdict::CoordinationFree);
125    }
126}