1use super::brownian::Rng;
6use glam::Vec2;
7
8pub struct PercolationGrid {
14 pub width: usize,
15 pub height: usize,
16 pub sites: Vec<bool>,
18}
19
20impl PercolationGrid {
21 pub fn new(width: usize, height: usize) -> Self {
23 Self {
24 width,
25 height,
26 sites: vec![false; width * height],
27 }
28 }
29
30 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 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 pub fn open_count(&self) -> usize {
48 self.sites.iter().filter(|&&s| s).count()
49 }
50
51 pub fn open_fraction(&self) -> f64 {
53 self.open_count() as f64 / (self.width * self.height) as f64
54 }
55
56 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
75pub 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
85pub fn bond_percolation(width: usize, height: usize, p: f64, rng: &mut Rng) -> PercolationGrid {
96 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 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 let mut uf = UnionFind::new(width * height);
132 for y in 0..height {
133 for x in 0..width {
134 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 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
152struct 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]]; 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
199pub 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 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 if x + 1 < w && grid.is_open(x + 1, y) {
219 uf.union(idx, y * w + x + 1);
220 }
221 if y + 1 < h && grid.is_open(x, y + 1) {
223 uf.union(idx, (y + 1) * w + x);
224 }
225 }
226 }
227
228 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())); clusters
243}
244
245pub 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); let virtual_top = w * h;
255 let virtual_bottom = w * h + 1;
256
257 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 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
286pub 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
300pub 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
337pub 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
354pub 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
371pub 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 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 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 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 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 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#[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 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 assert_eq!(clusters[0].len(), 3);
508 assert_eq!(clusters[1].len(), 2);
509 }
510
511 #[test]
512 fn test_percolates_simple() {
513 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 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 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 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); }
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 assert!(curve[0].1 < 0.1);
601 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}