Skip to main content

proof_engine/stochastic/
percolation.rs

1//! Percolation theory: site and bond percolation on 2D grids,
2//! cluster detection via union-find, spanning cluster identification,
3//! and critical threshold estimation.
4
5use super::brownian::Rng;
6use glam::Vec2;
7
8// ---------------------------------------------------------------------------
9// PercolationGrid
10// ---------------------------------------------------------------------------
11
12/// A 2D percolation grid where each site is either open (true) or closed (false).
13pub struct PercolationGrid {
14    pub width: usize,
15    pub height: usize,
16    /// Flattened row-major grid. sites[y * width + x] = true if site (x,y) is open.
17    pub sites: Vec<bool>,
18}
19
20impl PercolationGrid {
21    /// Create an empty (all closed) grid.
22    pub fn new(width: usize, height: usize) -> Self {
23        Self {
24            width,
25            height,
26            sites: vec![false; width * height],
27        }
28    }
29
30    /// Check if site (x, y) is open.
31    pub fn is_open(&self, x: usize, y: usize) -> bool {
32        if x < self.width && y < self.height {
33            self.sites[y * self.width + x]
34        } else {
35            false
36        }
37    }
38
39    /// Set site (x, y).
40    pub fn set(&mut self, x: usize, y: usize, open: bool) {
41        if x < self.width && y < self.height {
42            self.sites[y * self.width + x] = open;
43        }
44    }
45
46    /// Count of open sites.
47    pub fn open_count(&self) -> usize {
48        self.sites.iter().filter(|&&s| s).count()
49    }
50
51    /// Fraction of open sites.
52    pub fn open_fraction(&self) -> f64 {
53        self.open_count() as f64 / (self.width * self.height) as f64
54    }
55
56    /// Get 4-connected neighbors of (x, y) that are within bounds.
57    fn neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize)> {
58        let mut n = Vec::with_capacity(4);
59        if x > 0 {
60            n.push((x - 1, y));
61        }
62        if x + 1 < self.width {
63            n.push((x + 1, y));
64        }
65        if y > 0 {
66            n.push((x, y - 1));
67        }
68        if y + 1 < self.height {
69            n.push((x, y + 1));
70        }
71        n
72    }
73}
74
75// ---------------------------------------------------------------------------
76// Grid generation
77// ---------------------------------------------------------------------------
78
79/// Generate a site percolation grid: each site is open with probability p.
80pub fn site_percolation(width: usize, height: usize, p: f64, rng: &mut Rng) -> PercolationGrid {
81    let sites: Vec<bool> = (0..width * height).map(|_| rng.uniform() < p).collect();
82    PercolationGrid { width, height, sites }
83}
84
85/// Generate a bond percolation grid.
86///
87/// In bond percolation, bonds (edges) are open with probability p.
88/// We represent this by opening a site if at least one of its bonds is open.
89/// More precisely, we track horizontal and vertical bonds, then a site is
90/// considered "connected" if it has an open bond to a neighbor. For simplicity,
91/// we convert to an equivalent site representation on a finer grid:
92/// the effective site at (x,y) is open if the bond connecting it to a neighbor is open.
93///
94/// For practical purposes, we return a grid where sites represent connectivity.
95pub fn bond_percolation(width: usize, height: usize, p: f64, rng: &mut Rng) -> PercolationGrid {
96    // Horizontal bonds: (width-1) * height
97    // Vertical bonds: width * (height-1)
98    let mut h_bonds = vec![vec![false; width.saturating_sub(1)]; height];
99    let mut v_bonds = vec![vec![false; width]; height.saturating_sub(1)];
100
101    for row in h_bonds.iter_mut() {
102        for bond in row.iter_mut() {
103            *bond = rng.uniform() < p;
104        }
105    }
106    for row in v_bonds.iter_mut() {
107        for bond in row.iter_mut() {
108            *bond = rng.uniform() < p;
109        }
110    }
111
112    // A site is "open" (reachable) if it has at least one open bond
113    // For percolation analysis, we use union-find on bonds directly.
114    // But to return a PercolationGrid, we mark sites as open if they
115    // are connected to at least one open bond.
116    let mut grid = PercolationGrid::new(width, height);
117    for y in 0..height {
118        for x in 0..width {
119            let has_bond =
120                (x > 0 && h_bonds[y][x - 1])
121                    || (x < width.saturating_sub(1) && h_bonds[y][x])
122                    || (y > 0 && v_bonds[y - 1][x])
123                    || (y < height.saturating_sub(1) && v_bonds[y][x]);
124            grid.set(x, y, has_bond);
125        }
126    }
127
128    // Override: use union-find on bond connectivity for proper percolation.
129    // We'll store the bond information in the grid for cluster detection.
130    // Actually, let's use a union-find on all sites connected by open bonds.
131    let mut uf = UnionFind::new(width * height);
132    for y in 0..height {
133        for x in 0..width {
134            // Horizontal bond to (x+1, y)
135            if x + 1 < width && x < h_bonds[y].len() && h_bonds[y][x] {
136                uf.union(y * width + x, y * width + x + 1);
137                grid.set(x, y, true);
138                grid.set(x + 1, y, true);
139            }
140            // Vertical bond to (x, y+1)
141            if y + 1 < height && y < v_bonds.len() && v_bonds[y][x] {
142                uf.union(y * width + x, (y + 1) * width + x);
143                grid.set(x, y, true);
144                grid.set(x, y + 1, true);
145            }
146        }
147    }
148
149    grid
150}
151
152// ---------------------------------------------------------------------------
153// Union-Find
154// ---------------------------------------------------------------------------
155
156/// Weighted union-find with path compression.
157struct UnionFind {
158    parent: Vec<usize>,
159    rank: Vec<usize>,
160}
161
162impl UnionFind {
163    fn new(n: usize) -> Self {
164        Self {
165            parent: (0..n).collect(),
166            rank: vec![0; n],
167        }
168    }
169
170    fn find(&mut self, mut x: usize) -> usize {
171        while self.parent[x] != x {
172            self.parent[x] = self.parent[self.parent[x]]; // path halving
173            x = self.parent[x];
174        }
175        x
176    }
177
178    fn union(&mut self, a: usize, b: usize) {
179        let ra = self.find(a);
180        let rb = self.find(b);
181        if ra == rb {
182            return;
183        }
184        if self.rank[ra] < self.rank[rb] {
185            self.parent[ra] = rb;
186        } else if self.rank[ra] > self.rank[rb] {
187            self.parent[rb] = ra;
188        } else {
189            self.parent[rb] = ra;
190            self.rank[ra] += 1;
191        }
192    }
193
194    fn connected(&mut self, a: usize, b: usize) -> bool {
195        self.find(a) == self.find(b)
196    }
197}
198
199// ---------------------------------------------------------------------------
200// Cluster detection
201// ---------------------------------------------------------------------------
202
203/// Find all connected clusters of open sites using union-find.
204/// Returns a vector of clusters, each cluster being a vector of (x, y) coordinates.
205pub fn find_clusters(grid: &PercolationGrid) -> Vec<Vec<(usize, usize)>> {
206    let w = grid.width;
207    let h = grid.height;
208    let mut uf = UnionFind::new(w * h);
209
210    // Union adjacent open sites
211    for y in 0..h {
212        for x in 0..w {
213            if !grid.is_open(x, y) {
214                continue;
215            }
216            let idx = y * w + x;
217            // Right neighbor
218            if x + 1 < w && grid.is_open(x + 1, y) {
219                uf.union(idx, y * w + x + 1);
220            }
221            // Down neighbor
222            if y + 1 < h && grid.is_open(x, y + 1) {
223                uf.union(idx, (y + 1) * w + x);
224            }
225        }
226    }
227
228    // Collect clusters
229    let mut cluster_map: std::collections::HashMap<usize, Vec<(usize, usize)>> =
230        std::collections::HashMap::new();
231    for y in 0..h {
232        for x in 0..w {
233            if grid.is_open(x, y) {
234                let root = uf.find(y * w + x);
235                cluster_map.entry(root).or_default().push((x, y));
236            }
237        }
238    }
239
240    let mut clusters: Vec<Vec<(usize, usize)>> = cluster_map.into_values().collect();
241    clusters.sort_by(|a, b| b.len().cmp(&a.len())); // largest first
242    clusters
243}
244
245/// Check if the grid percolates (there exists a cluster spanning from top to bottom).
246pub fn percolates(grid: &PercolationGrid) -> bool {
247    let w = grid.width;
248    let h = grid.height;
249    if h == 0 || w == 0 {
250        return false;
251    }
252
253    let mut uf = UnionFind::new(w * h + 2); // +2 for virtual top and bottom
254    let virtual_top = w * h;
255    let virtual_bottom = w * h + 1;
256
257    // Connect top row to virtual top, bottom row to virtual bottom
258    for x in 0..w {
259        if grid.is_open(x, 0) {
260            uf.union(x, virtual_top);
261        }
262        if grid.is_open(x, h - 1) {
263            uf.union((h - 1) * w + x, virtual_bottom);
264        }
265    }
266
267    // Union adjacent open sites
268    for y in 0..h {
269        for x in 0..w {
270            if !grid.is_open(x, y) {
271                continue;
272            }
273            let idx = y * w + x;
274            if x + 1 < w && grid.is_open(x + 1, y) {
275                uf.union(idx, y * w + x + 1);
276            }
277            if y + 1 < h && grid.is_open(x, y + 1) {
278                uf.union(idx, (y + 1) * w + x);
279            }
280        }
281    }
282
283    uf.connected(virtual_top, virtual_bottom)
284}
285
286/// Find the spanning cluster (if it exists).
287pub fn spanning_cluster(grid: &PercolationGrid) -> Option<Vec<(usize, usize)>> {
288    let clusters = find_clusters(grid);
289    let h = grid.height;
290    for cluster in clusters {
291        let has_top = cluster.iter().any(|&(_, y)| y == 0);
292        let has_bottom = cluster.iter().any(|&(_, y)| y == h - 1);
293        if has_top && has_bottom {
294            return Some(cluster);
295        }
296    }
297    None
298}
299
300// ---------------------------------------------------------------------------
301// Critical threshold estimation
302// ---------------------------------------------------------------------------
303
304/// Estimate the critical percolation probability p_c using binary search.
305///
306/// For 2D site percolation on a square lattice, p_c ≈ 0.5927.
307/// For bond percolation, p_c = 0.5 (exactly).
308pub fn find_critical_p(
309    width: usize,
310    height: usize,
311    trials: usize,
312    rng: &mut Rng,
313) -> f64 {
314    let mut lo = 0.0;
315    let mut hi = 1.0;
316
317    for _ in 0..30 {
318        let mid = (lo + hi) / 2.0;
319        let perc_count = (0..trials)
320            .filter(|_| {
321                let grid = site_percolation(width, height, mid, rng);
322                percolates(&grid)
323            })
324            .count();
325        let perc_fraction = perc_count as f64 / trials as f64;
326
327        if perc_fraction < 0.5 {
328            lo = mid;
329        } else {
330            hi = mid;
331        }
332    }
333
334    (lo + hi) / 2.0
335}
336
337/// Estimate percolation probability at a given p.
338pub fn percolation_probability(
339    width: usize,
340    height: usize,
341    p: f64,
342    trials: usize,
343    rng: &mut Rng,
344) -> f64 {
345    let count = (0..trials)
346        .filter(|_| {
347            let grid = site_percolation(width, height, p, rng);
348            percolates(&grid)
349        })
350        .count();
351    count as f64 / trials as f64
352}
353
354/// Compute the percolation curve: probability of percolation vs p.
355pub fn percolation_curve(
356    width: usize,
357    height: usize,
358    trials_per_point: usize,
359    p_points: usize,
360    rng: &mut Rng,
361) -> Vec<(f64, f64)> {
362    (0..=p_points)
363        .map(|i| {
364            let p = i as f64 / p_points as f64;
365            let prob = percolation_probability(width, height, p, trials_per_point, rng);
366            (p, prob)
367        })
368        .collect()
369}
370
371// ---------------------------------------------------------------------------
372// PercolationRenderer
373// ---------------------------------------------------------------------------
374
375/// Render a percolation grid with cluster colors and spanning cluster highlighted.
376pub struct PercolationRenderer {
377    pub closed_character: char,
378    pub open_character: char,
379    pub spanning_character: char,
380    pub closed_color: [f32; 4],
381    pub spanning_color: [f32; 4],
382    pub cell_size: f32,
383}
384
385impl PercolationRenderer {
386    pub fn new() -> Self {
387        Self {
388            closed_character: '░',
389            open_character: '█',
390            spanning_character: '▓',
391            closed_color: [0.2, 0.2, 0.2, 0.5],
392            spanning_color: [1.0, 0.3, 0.2, 1.0],
393            cell_size: 0.5,
394        }
395    }
396
397    /// Generate a deterministic color for a cluster index.
398    fn cluster_color(&self, index: usize) -> [f32; 4] {
399        let hues = [
400            [0.2, 0.7, 1.0, 0.9],
401            [0.3, 1.0, 0.4, 0.9],
402            [1.0, 0.8, 0.2, 0.9],
403            [0.8, 0.3, 1.0, 0.9],
404            [1.0, 0.5, 0.5, 0.9],
405            [0.5, 1.0, 0.8, 0.9],
406            [0.9, 0.9, 0.3, 0.9],
407            [0.4, 0.5, 1.0, 0.9],
408        ];
409        hues[index % hues.len()]
410    }
411
412    /// Render the full grid with clusters colored.
413    pub fn render(&self, grid: &PercolationGrid) -> Vec<(Vec2, char, [f32; 4])> {
414        let w = grid.width;
415        let h = grid.height;
416        let clusters = find_clusters(grid);
417        let span = spanning_cluster(grid);
418
419        // Build a lookup from (x,y) to cluster index
420        let mut site_cluster = std::collections::HashMap::new();
421        for (ci, cluster) in clusters.iter().enumerate() {
422            for &pos in cluster {
423                site_cluster.insert(pos, ci);
424            }
425        }
426
427        // Build spanning set
428        let spanning_set: std::collections::HashSet<(usize, usize)> = span
429            .map(|c| c.into_iter().collect())
430            .unwrap_or_default();
431
432        let mut glyphs = Vec::with_capacity(w * h);
433        for y in 0..h {
434            for x in 0..w {
435                let pos = Vec2::new(x as f32 * self.cell_size, (h - 1 - y) as f32 * self.cell_size);
436                if !grid.is_open(x, y) {
437                    glyphs.push((pos, self.closed_character, self.closed_color));
438                } else if spanning_set.contains(&(x, y)) {
439                    glyphs.push((pos, self.spanning_character, self.spanning_color));
440                } else if let Some(&ci) = site_cluster.get(&(x, y)) {
441                    let color = self.cluster_color(ci);
442                    glyphs.push((pos, self.open_character, color));
443                } else {
444                    glyphs.push((pos, self.open_character, [0.5, 0.5, 0.5, 0.7]));
445                }
446            }
447        }
448        glyphs
449    }
450
451    /// Render the percolation curve as a line chart.
452    pub fn render_curve(&self, curve: &[(f64, f64)]) -> Vec<(Vec2, char, [f32; 4])> {
453        let color = [0.2, 0.8, 1.0, 1.0];
454        curve
455            .iter()
456            .map(|&(p, prob)| {
457                let pos = Vec2::new(p as f32 * 10.0, prob as f32 * 10.0);
458                (pos, '·', color)
459            })
460            .collect()
461    }
462}
463
464impl Default for PercolationRenderer {
465    fn default() -> Self {
466        Self::new()
467    }
468}
469
470// ---------------------------------------------------------------------------
471// Tests
472// ---------------------------------------------------------------------------
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn test_site_percolation_p0() {
480        let mut rng = Rng::new(42);
481        let grid = site_percolation(20, 20, 0.0, &mut rng);
482        assert_eq!(grid.open_count(), 0);
483        assert!(!percolates(&grid));
484    }
485
486    #[test]
487    fn test_site_percolation_p1() {
488        let mut rng = Rng::new(42);
489        let grid = site_percolation(20, 20, 1.0, &mut rng);
490        assert_eq!(grid.open_count(), 400);
491        assert!(percolates(&grid));
492    }
493
494    #[test]
495    fn test_find_clusters() {
496        let mut grid = PercolationGrid::new(5, 5);
497        // Create two separate clusters
498        grid.set(0, 0, true);
499        grid.set(1, 0, true);
500        grid.set(3, 3, true);
501        grid.set(4, 3, true);
502        grid.set(4, 4, true);
503
504        let clusters = find_clusters(&grid);
505        assert_eq!(clusters.len(), 2);
506        // Largest cluster has 3 sites
507        assert_eq!(clusters[0].len(), 3);
508        assert_eq!(clusters[1].len(), 2);
509    }
510
511    #[test]
512    fn test_percolates_simple() {
513        // Create a grid that percolates (column of open sites)
514        let mut grid = PercolationGrid::new(5, 5);
515        for y in 0..5 {
516            grid.set(2, y, true);
517        }
518        assert!(percolates(&grid));
519
520        // Remove middle -> no longer percolates
521        grid.set(2, 2, false);
522        assert!(!percolates(&grid));
523    }
524
525    #[test]
526    fn test_spanning_cluster() {
527        let mut grid = PercolationGrid::new(5, 5);
528        for y in 0..5 {
529            grid.set(0, y, true);
530        }
531        let span = spanning_cluster(&grid);
532        assert!(span.is_some());
533        assert_eq!(span.unwrap().len(), 5);
534    }
535
536    #[test]
537    fn test_critical_p_approximate() {
538        // p_c for 2D site percolation ≈ 0.5927
539        // Due to finite size effects and limited trials, we just check it's in a reasonable range
540        let mut rng = Rng::new(42);
541        let pc = find_critical_p(20, 20, 50, &mut rng);
542        assert!(
543            pc > 0.4 && pc < 0.8,
544            "critical p should be roughly in [0.4, 0.8], got {}",
545            pc
546        );
547    }
548
549    #[test]
550    fn test_percolation_monotone() {
551        // Higher p should give higher percolation probability
552        let mut rng = Rng::new(42);
553        let prob_low = percolation_probability(20, 20, 0.3, 100, &mut rng);
554        let prob_high = percolation_probability(20, 20, 0.8, 100, &mut rng);
555        assert!(
556            prob_high >= prob_low,
557            "percolation probability should increase with p: low={}, high={}",
558            prob_low,
559            prob_high
560        );
561    }
562
563    #[test]
564    fn test_bond_percolation() {
565        let mut rng = Rng::new(42);
566        let grid = bond_percolation(20, 20, 0.0, &mut rng);
567        assert_eq!(grid.open_count(), 0);
568
569        let grid2 = bond_percolation(20, 20, 1.0, &mut rng);
570        assert!(grid2.open_count() > 0);
571    }
572
573    #[test]
574    fn test_renderer() {
575        let mut rng = Rng::new(42);
576        let grid = site_percolation(10, 10, 0.6, &mut rng);
577        let renderer = PercolationRenderer::new();
578        let glyphs = renderer.render(&grid);
579        assert_eq!(glyphs.len(), 100); // 10x10 grid
580    }
581
582    #[test]
583    fn test_union_find() {
584        let mut uf = UnionFind::new(10);
585        uf.union(0, 1);
586        uf.union(1, 2);
587        assert!(uf.connected(0, 2));
588        assert!(!uf.connected(0, 5));
589        uf.union(5, 6);
590        uf.union(2, 5);
591        assert!(uf.connected(0, 6));
592    }
593
594    #[test]
595    fn test_percolation_curve() {
596        let mut rng = Rng::new(42);
597        let curve = percolation_curve(10, 10, 20, 10, &mut rng);
598        assert_eq!(curve.len(), 11);
599        // First point (p=0) should have prob ≈ 0
600        assert!(curve[0].1 < 0.1);
601        // Last point (p=1) should have prob = 1
602        assert!((curve[10].1 - 1.0).abs() < 0.01);
603    }
604
605    #[test]
606    fn test_open_fraction() {
607        let mut rng = Rng::new(42);
608        let grid = site_percolation(100, 100, 0.5, &mut rng);
609        let frac = grid.open_fraction();
610        assert!(
611            (frac - 0.5).abs() < 0.1,
612            "open fraction should be ~0.5, got {}",
613            frac
614        );
615    }
616}