1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2#![allow(dead_code)]
22#![allow(clippy::too_many_arguments)]
23
24use std::collections::{HashMap, VecDeque};
25use std::f64::consts::PI;
26
27struct Lcg {
32 state: u64,
33}
34
35impl Lcg {
36 fn new(seed: u64) -> Self {
37 Self {
38 state: seed.wrapping_add(1),
39 }
40 }
41
42 fn next_u64(&mut self) -> u64 {
43 self.state = self
44 .state
45 .wrapping_mul(6_364_136_223_846_793_005)
46 .wrapping_add(1_442_695_040_888_963_407);
47 self.state
48 }
49
50 fn next_f64(&mut self) -> f64 {
51 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
52 }
53
54 fn next_range(&mut self, lo: usize, hi: usize) -> usize {
55 lo + (self.next_u64() as usize % (hi - lo))
56 }
57}
58
59#[derive(Clone)]
68pub struct NetworkGraph {
69 pub n: usize,
71 pub directed: bool,
73 pub adj: Vec<Vec<(usize, f64)>>,
75}
76
77impl NetworkGraph {
78 pub fn new(n: usize, directed: bool) -> Self {
80 Self {
81 n,
82 directed,
83 adj: vec![Vec::new(); n],
84 }
85 }
86
87 pub fn add_edge(&mut self, u: usize, v: usize, w: f64) {
89 self.adj[u].push((v, w));
90 if !self.directed {
91 self.adj[v].push((u, w));
92 }
93 }
94
95 pub fn degree(&self, u: usize) -> usize {
97 self.adj[u].len()
98 }
99
100 pub fn edge_count(&self) -> usize {
102 let total: usize = self.adj.iter().map(|a| a.len()).sum();
103 if self.directed { total } else { total / 2 }
104 }
105
106 pub fn complete(n: usize) -> Self {
108 let mut g = Self::new(n, false);
109 for i in 0..n {
110 for j in (i + 1)..n {
111 g.add_edge(i, j, 1.0);
112 }
113 }
114 g
115 }
116
117 pub fn ring(n: usize) -> Self {
119 let mut g = Self::new(n, false);
120 for i in 0..n {
121 g.add_edge(i, (i + 1) % n, 1.0);
122 }
123 g
124 }
125
126 pub fn erdos_renyi(n: usize, p: f64, seed: u64) -> Self {
128 let mut g = Self::new(n, false);
129 let mut rng = Lcg::new(seed);
130 for i in 0..n {
131 for j in (i + 1)..n {
132 if rng.next_f64() < p {
133 g.add_edge(i, j, 1.0);
134 }
135 }
136 }
137 g
138 }
139
140 pub fn bfs_distances(&self, s: usize) -> Vec<i64> {
142 let mut dist = vec![-1i64; self.n];
143 dist[s] = 0;
144 let mut queue = VecDeque::new();
145 queue.push_back(s);
146 while let Some(u) = queue.pop_front() {
147 for &(v, _) in &self.adj[u] {
148 if dist[v] < 0 {
149 dist[v] = dist[u] + 1;
150 queue.push_back(v);
151 }
152 }
153 }
154 dist
155 }
156
157 pub fn average_path_length(&self) -> f64 {
159 let mut total = 0.0f64;
160 let mut count = 0usize;
161 for s in 0..self.n {
162 let dists = self.bfs_distances(s);
163 for d in &dists {
164 if *d > 0 {
165 total += *d as f64;
166 count += 1;
167 }
168 }
169 }
170 if count == 0 {
171 f64::INFINITY
172 } else {
173 total / count as f64
174 }
175 }
176
177 pub fn clustering_coefficient(&self, u: usize) -> f64 {
179 let neighbors: Vec<usize> = self.adj[u].iter().map(|&(v, _)| v).collect();
180 let k = neighbors.len();
181 if k < 2 {
182 return 0.0;
183 }
184 let nbr_set: std::collections::HashSet<usize> = neighbors.iter().cloned().collect();
185 let mut triangles = 0usize;
186 for &v in &neighbors {
187 for &(w, _) in &self.adj[v] {
188 if w != u && nbr_set.contains(&w) {
189 triangles += 1;
190 }
191 }
192 }
193 triangles as f64 / (k * (k - 1)) as f64
194 }
195
196 pub fn mean_clustering(&self) -> f64 {
198 let s: f64 = (0..self.n).map(|u| self.clustering_coefficient(u)).sum();
199 s / self.n as f64
200 }
201}
202
203pub struct SirModel {
214 pub beta: f64,
216 pub gamma: f64,
218 pub n: f64,
220}
221
222#[derive(Clone, Debug)]
224pub struct SirState {
225 pub s: f64,
227 pub i: f64,
229 pub r: f64,
231}
232
233impl SirModel {
234 pub fn new(beta: f64, gamma: f64, n: f64) -> Self {
236 Self { beta, gamma, n }
237 }
238
239 pub fn r0(&self) -> f64 {
241 self.beta / self.gamma
242 }
243
244 pub fn herd_immunity_threshold(&self) -> f64 {
246 let r0 = self.r0();
247 if r0 <= 1.0 { 0.0 } else { 1.0 - 1.0 / r0 }
248 }
249
250 pub fn integrate(&self, s0: f64, i0: f64, r0_val: f64, dt: f64, steps: usize) -> Vec<SirState> {
252 let mut traj = Vec::with_capacity(steps + 1);
253 let (mut s, mut i, mut r) = (s0, i0, r0_val);
254 traj.push(SirState { s, i, r });
255 for _ in 0..steps {
256 let ds = -self.beta * s * i / self.n;
257 let di = self.beta * s * i / self.n - self.gamma * i;
258 let dr = self.gamma * i;
259 s += dt * ds;
260 i += dt * di;
261 r += dt * dr;
262 s = s.max(0.0);
263 i = i.max(0.0);
264 r = r.max(0.0);
265 traj.push(SirState { s, i, r });
266 }
267 traj
268 }
269
270 pub fn simulate_on_graph(
274 &self,
275 graph: &NetworkGraph,
276 initially_infected: &[usize],
277 dt: f64,
278 steps: usize,
279 seed: u64,
280 ) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
281 let n = graph.n;
282 let mut state = vec![0u8; n];
284 for &idx in initially_infected {
285 if idx < n {
286 state[idx] = 1;
287 }
288 }
289 let mut rng = Lcg::new(seed);
290 let mut s_ts = Vec::with_capacity(steps + 1);
291 let mut i_ts = Vec::with_capacity(steps + 1);
292 let mut r_ts = Vec::with_capacity(steps + 1);
293 let count_state = |state: &[u8], val: u8| state.iter().filter(|&&x| x == val).count();
294 s_ts.push(count_state(&state, 0));
295 i_ts.push(count_state(&state, 1));
296 r_ts.push(count_state(&state, 2));
297 for _ in 0..steps {
298 let mut next = state.clone();
299 for u in 0..n {
300 if state[u] == 1 {
301 if rng.next_f64() < self.gamma * dt {
303 next[u] = 2;
304 }
305 } else if state[u] == 0 {
306 let inf_neighbors =
308 graph.adj[u].iter().filter(|&&(v, _)| state[v] == 1).count();
309 let p_inf = 1.0 - (1.0 - self.beta * dt).powi(inf_neighbors as i32);
310 if rng.next_f64() < p_inf {
311 next[u] = 1;
312 }
313 }
314 }
315 state = next;
316 s_ts.push(count_state(&state, 0));
317 i_ts.push(count_state(&state, 1));
318 r_ts.push(count_state(&state, 2));
319 }
320 (s_ts, i_ts, r_ts)
321 }
322}
323
324pub struct SeirdModel {
336 pub beta: f64,
338 pub sigma: f64,
340 pub gamma: f64,
342 pub mu: f64,
344 pub n: f64,
346}
347
348#[derive(Clone, Debug)]
350pub struct SeirdState {
351 pub s: f64,
353 pub e: f64,
355 pub i: f64,
357 pub r: f64,
359 pub d: f64,
361}
362
363impl SeirdModel {
364 pub fn new(beta: f64, sigma: f64, gamma: f64, mu: f64, n: f64) -> Self {
366 Self {
367 beta,
368 sigma,
369 gamma,
370 mu,
371 n,
372 }
373 }
374
375 pub fn r0(&self) -> f64 {
377 self.beta / (self.gamma + self.mu)
378 }
379
380 pub fn case_fatality_ratio(&self) -> f64 {
382 self.mu / (self.gamma + self.mu)
383 }
384
385 pub fn integrate(
387 &self,
388 s0: f64,
389 e0: f64,
390 i0: f64,
391 r0: f64,
392 d0: f64,
393 dt: f64,
394 steps: usize,
395 ) -> Vec<SeirdState> {
396 let mut traj = Vec::with_capacity(steps + 1);
397 let (mut s, mut e, mut i, mut r, mut d) = (s0, e0, i0, r0, d0);
398 traj.push(SeirdState { s, e, i, r, d });
399 for _ in 0..steps {
400 let ds = -self.beta * s * i / self.n;
401 let de = self.beta * s * i / self.n - self.sigma * e;
402 let di = self.sigma * e - self.gamma * i - self.mu * i;
403 let dr = self.gamma * i;
404 let dd = self.mu * i;
405 s = (s + dt * ds).max(0.0);
406 e = (e + dt * de).max(0.0);
407 i = (i + dt * di).max(0.0);
408 r += dt * dr;
409 d += dt * dd;
410 traj.push(SeirdState { s, e, i, r, d });
411 }
412 traj
413 }
414}
415
416pub struct OpinionDynamics {
425 pub epsilon: f64,
427 pub mu: f64,
429}
430
431impl OpinionDynamics {
432 pub fn new(epsilon: f64, mu: f64) -> Self {
434 Self { epsilon, mu }
435 }
436
437 pub fn run(&self, opinions: &mut Vec<f64>, steps: usize, seed: u64) {
441 let n = opinions.len();
442 if n < 2 {
443 return;
444 }
445 let mut rng = Lcg::new(seed);
446 for _ in 0..steps {
447 let i = rng.next_range(0, n);
448 let j_raw = rng.next_range(0, n - 1);
449 let j = if j_raw >= i { j_raw + 1 } else { j_raw };
450 let diff = opinions[j] - opinions[i];
451 if diff.abs() < self.epsilon {
452 opinions[i] += self.mu * diff;
453 opinions[j] -= self.mu * diff;
454 }
455 }
456 }
457
458 pub fn count_clusters(&self, opinions: &[f64]) -> usize {
460 let mut sorted = opinions.to_vec();
461 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
462 if sorted.is_empty() {
463 return 0;
464 }
465 let mut clusters = 1usize;
466 for w in sorted.windows(2) {
467 if w[1] - w[0] >= self.epsilon / 2.0 {
468 clusters += 1;
469 }
470 }
471 clusters
472 }
473}
474
475pub struct DeGrootModel {
484 pub trust: Vec<Vec<f64>>,
486}
487
488impl DeGrootModel {
489 pub fn new(trust: Vec<Vec<f64>>) -> Self {
493 Self { trust }
494 }
495
496 pub fn from_graph(graph: &NetworkGraph) -> Self {
498 let n = graph.n;
499 let mut trust = vec![vec![0.0f64; n]; n];
500 for u in 0..n {
501 let deg = graph.adj[u].len();
502 if deg == 0 {
503 trust[u][u] = 1.0;
504 } else {
505 for &(v, _) in &graph.adj[u] {
506 trust[u][v] = 1.0 / deg as f64;
507 }
508 }
509 }
510 Self { trust }
511 }
512
513 pub fn n(&self) -> usize {
515 self.trust.len()
516 }
517
518 pub fn step(&self, opinions: &[f64]) -> Vec<f64> {
520 let n = opinions.len();
521 let mut next = vec![0.0; n];
522 for i in 0..n {
523 for j in 0..n {
524 next[i] += self.trust[i][j] * opinions[j];
525 }
526 }
527 next
528 }
529
530 pub fn run(&self, opinions: &[f64], steps: usize) -> Vec<f64> {
532 let mut ops = opinions.to_vec();
533 for _ in 0..steps {
534 ops = self.step(&ops);
535 }
536 ops
537 }
538
539 pub fn has_consensus(&self, opinions: &[f64], tol: f64) -> bool {
541 let min = opinions.iter().cloned().fold(f64::INFINITY, f64::min);
542 let max = opinions.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
543 max - min < tol
544 }
545}
546
547pub struct VoterModel {
553 pub n_states: usize,
555}
556
557impl VoterModel {
558 pub fn new(n_states: usize) -> Self {
560 Self { n_states }
561 }
562
563 pub fn run(&self, graph: &NetworkGraph, opinions: &mut Vec<usize>, steps: usize, seed: u64) {
565 let n = graph.n;
566 if n == 0 {
567 return;
568 }
569 let mut rng = Lcg::new(seed);
570 for _ in 0..steps {
571 let i = rng.next_range(0, n);
572 if graph.adj[i].is_empty() {
573 continue;
574 }
575 let nb_idx = rng.next_range(0, graph.adj[i].len());
576 let j = graph.adj[i][nb_idx].0;
577 opinions[i] = opinions[j];
578 }
579 }
580
581 pub fn fraction(&self, opinions: &[usize], state: usize) -> f64 {
583 let count = opinions.iter().filter(|&&x| x == state).count();
584 count as f64 / opinions.len() as f64
585 }
586
587 pub fn is_consensus(&self, opinions: &[usize]) -> bool {
589 opinions.windows(2).all(|w| w[0] == w[1])
590 }
591}
592
593pub struct KuramotoModel {
603 pub omega: Vec<f64>,
605 pub k: f64,
607}
608
609impl KuramotoModel {
610 pub fn new(omega: Vec<f64>, k: f64) -> Self {
612 Self { omega, k }
613 }
614
615 pub fn lorentzian(n: usize, omega0: f64, gamma_width: f64, seed: u64) -> Self {
619 let mut rng = Lcg::new(seed);
620 let omega: Vec<f64> = (0..n)
621 .map(|_| {
622 let u = rng.next_f64();
624 omega0 + gamma_width * (PI * (u - 0.5)).tan()
625 })
626 .collect();
627 Self {
628 omega,
629 k: 2.0 * gamma_width,
630 }
631 }
632
633 pub fn n(&self) -> usize {
635 self.omega.len()
636 }
637
638 pub fn order_parameter(phases: &[f64]) -> f64 {
642 let n = phases.len() as f64;
643 let re: f64 = phases.iter().map(|&t| t.cos()).sum::<f64>() / n;
644 let im: f64 = phases.iter().map(|&t| t.sin()).sum::<f64>() / n;
645 (re * re + im * im).sqrt()
646 }
647
648 pub fn mean_phase(phases: &[f64]) -> f64 {
650 let re: f64 = phases.iter().map(|&t| t.cos()).sum::<f64>();
651 let im: f64 = phases.iter().map(|&t| t.sin()).sum::<f64>();
652 im.atan2(re)
653 }
654
655 pub fn integrate(&self, phases0: &[f64], dt: f64, steps: usize) -> (Vec<f64>, Vec<f64>) {
659 let n = self.n();
660 let mut phases = phases0.to_vec();
661 let mut times = Vec::with_capacity(steps + 1);
662 let mut r_vals = Vec::with_capacity(steps + 1);
663 times.push(0.0);
664 r_vals.push(Self::order_parameter(&phases));
665
666 let deriv = |ph: &[f64]| -> Vec<f64> {
667 (0..n)
668 .map(|i| {
669 let coupling: f64 = ph.iter().map(|&t| (t - ph[i]).sin()).sum::<f64>();
670 self.omega[i] + self.k / n as f64 * coupling
671 })
672 .collect()
673 };
674
675 for step in 0..steps {
676 let k1 = deriv(&phases);
677 let p2: Vec<f64> = phases
678 .iter()
679 .zip(&k1)
680 .map(|(&p, &k)| p + 0.5 * dt * k)
681 .collect();
682 let k2 = deriv(&p2);
683 let p3: Vec<f64> = phases
684 .iter()
685 .zip(&k2)
686 .map(|(&p, &k)| p + 0.5 * dt * k)
687 .collect();
688 let k3 = deriv(&p3);
689 let p4: Vec<f64> = phases.iter().zip(&k3).map(|(&p, &k)| p + dt * k).collect();
690 let k4 = deriv(&p4);
691 for i in 0..n {
692 phases[i] += dt / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
693 }
694 times.push((step + 1) as f64 * dt);
695 r_vals.push(Self::order_parameter(&phases));
696 }
697 (times, r_vals)
698 }
699
700 pub fn critical_coupling_estimate(&self) -> f64 {
704 let mean = self.omega.iter().sum::<f64>() / self.n() as f64;
705 let var: f64 =
706 self.omega.iter().map(|&w| (w - mean).powi(2)).sum::<f64>() / self.n() as f64;
707 2.0 * var.sqrt() / PI.sqrt()
708 }
709}
710
711pub struct RandomWalkGraph {
717 graph: NetworkGraph,
718}
719
720impl RandomWalkGraph {
721 pub fn new(graph: NetworkGraph) -> Self {
723 Self { graph }
724 }
725
726 pub fn walk(&self, start: usize, max_steps: usize, seed: u64) -> Vec<usize> {
730 let mut rng = Lcg::new(seed);
731 let mut path = Vec::with_capacity(max_steps + 1);
732 let mut current = start;
733 path.push(current);
734 for _ in 0..max_steps {
735 let neighbors = &self.graph.adj[current];
736 if neighbors.is_empty() {
737 break;
738 }
739 let idx = rng.next_range(0, neighbors.len());
740 current = neighbors[idx].0;
741 path.push(current);
742 }
743 path
744 }
745
746 pub fn hitting_time_estimate(
748 &self,
749 start: usize,
750 target: usize,
751 trials: usize,
752 seed: u64,
753 ) -> f64 {
754 let mut rng = Lcg::new(seed);
755 let mut total = 0usize;
756 let mut successes = 0usize;
757 for t in 0..trials {
758 let mut current = start;
759 let walk_seed = seed.wrapping_add(t as u64 * 1_000_003);
760 let _ = walk_seed; let mut steps = 0usize;
762 let max_steps = self.graph.n * self.graph.n * 10;
763 loop {
764 if current == target {
765 break;
766 }
767 let neighbors = &self.graph.adj[current];
768 if neighbors.is_empty() || steps >= max_steps {
769 break;
770 }
771 let idx = rng.next_range(0, neighbors.len());
772 current = neighbors[idx].0;
773 steps += 1;
774 }
775 if current == target {
776 total += steps;
777 successes += 1;
778 }
779 }
780 if successes == 0 {
781 f64::INFINITY
782 } else {
783 total as f64 / successes as f64
784 }
785 }
786
787 pub fn cover_time_estimate(&self, start: usize, trials: usize, seed: u64) -> f64 {
789 let n = self.graph.n;
790 let mut rng = Lcg::new(seed);
791 let mut total_steps = 0usize;
792 for _ in 0..trials {
793 let mut visited = vec![false; n];
794 let mut current = start;
795 visited[current] = true;
796 let mut steps = 0usize;
797 let max_steps = n * n * 100;
798 while visited.iter().any(|&v| !v) && steps < max_steps {
799 let neighbors = &self.graph.adj[current];
800 if neighbors.is_empty() {
801 break;
802 }
803 let idx = rng.next_range(0, neighbors.len());
804 current = neighbors[idx].0;
805 visited[current] = true;
806 steps += 1;
807 }
808 total_steps += steps;
809 }
810 total_steps as f64 / trials as f64
811 }
812
813 pub fn stationary_distribution(&self, tol: f64, max_iter: usize) -> Vec<f64> {
815 let n = self.graph.n;
816 let mut pi = vec![1.0 / n as f64; n];
817 for _ in 0..max_iter {
818 let mut next = vec![0.0f64; n];
819 for u in 0..n {
820 let deg = self.graph.adj[u].len();
821 if deg == 0 {
822 continue;
823 }
824 let w = pi[u] / deg as f64;
825 for &(v, _) in &self.graph.adj[u] {
826 next[v] += w;
827 }
828 }
829 let sum: f64 = next.iter().sum();
831 if sum > 0.0 {
832 for x in &mut next {
833 *x /= sum;
834 }
835 }
836 let diff: f64 = pi.iter().zip(&next).map(|(a, b)| (a - b).abs()).sum();
837 pi = next;
838 if diff < tol {
839 break;
840 }
841 }
842 pi
843 }
844}
845
846pub struct PageRank {
852 pub damping: f64,
854 pub tol: f64,
856 pub max_iter: usize,
858}
859
860impl PageRank {
861 pub fn new(damping: f64, tol: f64, max_iter: usize) -> Self {
863 Self {
864 damping,
865 tol,
866 max_iter,
867 }
868 }
869
870 pub fn compute(&self, graph: &NetworkGraph) -> Vec<f64> {
874 let n = graph.n;
875 let inv_n = 1.0 / n as f64;
876 let mut pr = vec![inv_n; n];
877 let out_deg: Vec<usize> = (0..n).map(|u| graph.adj[u].len()).collect();
879
880 for _ in 0..self.max_iter {
881 let dangling_sum: f64 = (0..n).filter(|&u| out_deg[u] == 0).map(|u| pr[u]).sum();
883 let mut next = vec![0.0f64; n];
884 for u in 0..n {
885 if out_deg[u] == 0 {
886 continue;
887 }
888 let contrib = self.damping * pr[u] / out_deg[u] as f64;
889 for &(v, _) in &graph.adj[u] {
890 next[v] += contrib;
891 }
892 }
893 let teleport = (1.0 - self.damping + self.damping * dangling_sum) * inv_n;
894 for x in &mut next {
895 *x += teleport;
896 }
897 let sum: f64 = next.iter().sum();
899 if sum > 0.0 {
900 for x in &mut next {
901 *x /= sum;
902 }
903 }
904 let diff: f64 = pr.iter().zip(&next).map(|(a, b)| (a - b).abs()).sum();
905 pr = next;
906 if diff < self.tol {
907 break;
908 }
909 }
910 pr
911 }
912}
913
914pub struct CommunityDetection;
922
923impl CommunityDetection {
924 pub fn modularity(graph: &NetworkGraph, communities: &[usize]) -> f64 {
926 let m = graph.edge_count() as f64;
927 if m == 0.0 {
928 return 0.0;
929 }
930 let n = graph.n;
931 let degree: Vec<f64> = (0..n).map(|u| graph.adj[u].len() as f64).collect();
932 let mut q = 0.0f64;
933 for u in 0..n {
934 for &(v, _) in &graph.adj[u] {
935 if communities[u] == communities[v] {
936 q += 1.0 - degree[u] * degree[v] / (2.0 * m);
937 }
938 }
939 }
940 q / (2.0 * m)
941 }
942
943 pub fn greedy_modularity(graph: &NetworkGraph) -> Vec<usize> {
948 let n = graph.n;
949 let mut communities: Vec<usize> = (0..n).collect();
950 let mut best_q = Self::modularity(graph, &communities);
951 let mut improved = true;
952 while improved {
953 improved = false;
954 let mut edge_pairs: Vec<(usize, usize)> = Vec::new();
956 for u in 0..n {
957 for &(v, _) in &graph.adj[u] {
958 let cu = communities[u];
959 let cv = communities[v];
960 if cu != cv {
961 let pair = if cu < cv { (cu, cv) } else { (cv, cu) };
962 edge_pairs.push(pair);
963 }
964 }
965 }
966 edge_pairs.sort();
967 edge_pairs.dedup();
968 for (ca, cb) in edge_pairs {
969 let mut trial = communities.clone();
970 for x in &mut trial {
971 if *x == cb {
972 *x = ca;
973 }
974 }
975 let q = Self::modularity(graph, &trial);
976 if q > best_q + 1e-10 {
977 best_q = q;
978 communities = trial;
979 improved = true;
980 }
981 }
982 }
983 let mut id_map: HashMap<usize, usize> = HashMap::new();
985 let mut next_id = 0usize;
986 for c in &mut communities {
987 let entry = id_map.entry(*c).or_insert_with(|| {
988 let id = next_id;
989 next_id += 1;
990 id
991 });
992 *c = *entry;
993 }
994 communities
995 }
996
997 pub fn n_communities(communities: &[usize]) -> usize {
999 let max = communities.iter().cloned().max().unwrap_or(0);
1000 max + 1
1001 }
1002}
1003
1004pub struct NetworkRobustness;
1010
1011impl NetworkRobustness {
1012 pub fn giant_component_fraction(graph: &NetworkGraph) -> f64 {
1014 let n = graph.n;
1015 if n == 0 {
1016 return 0.0;
1017 }
1018 let comp_id = Self::connected_components(graph);
1019 let mut sizes: HashMap<usize, usize> = HashMap::new();
1020 for &c in &comp_id {
1021 *sizes.entry(c).or_insert(0) += 1;
1022 }
1023 *sizes.values().max().unwrap_or(&0) as f64 / n as f64
1024 }
1025
1026 pub fn connected_components(graph: &NetworkGraph) -> Vec<usize> {
1028 let n = graph.n;
1029 let mut comp = vec![usize::MAX; n];
1030 let mut comp_id = 0usize;
1031 for start in 0..n {
1032 if comp[start] != usize::MAX {
1033 continue;
1034 }
1035 let mut queue = VecDeque::new();
1036 queue.push_back(start);
1037 comp[start] = comp_id;
1038 while let Some(u) = queue.pop_front() {
1039 for &(v, _) in &graph.adj[u] {
1040 if comp[v] == usize::MAX {
1041 comp[v] = comp_id;
1042 queue.push_back(v);
1043 }
1044 }
1045 }
1046 comp_id += 1;
1047 }
1048 comp
1049 }
1050
1051 pub fn percolation_curve(graph: &NetworkGraph, seed: u64) -> Vec<(f64, f64)> {
1053 let n = graph.n;
1054 let mut rng = Lcg::new(seed);
1055 let mut order: Vec<usize> = (0..n).collect();
1057 for i in (1..n).rev() {
1058 let j = rng.next_range(0, i + 1);
1059 order.swap(i, j);
1060 }
1061 let mut curve = Vec::with_capacity(n + 1);
1062 let gc0 = Self::giant_component_fraction(graph);
1063 curve.push((0.0, gc0));
1064 let mut active = vec![true; n];
1065 for (step, &node) in order.iter().enumerate() {
1066 active[node] = false;
1067 let active_nodes: Vec<usize> = (0..n).filter(|&i| active[i]).collect();
1069 let an = active_nodes.len();
1070 if an == 0 {
1071 curve.push(((step + 1) as f64 / n as f64, 0.0));
1072 continue;
1073 }
1074 let mut idx_map = vec![usize::MAX; n];
1075 for (new_i, &old_i) in active_nodes.iter().enumerate() {
1076 idx_map[old_i] = new_i;
1077 }
1078 let mut sub = NetworkGraph::new(an, graph.directed);
1079 for &u in &active_nodes {
1080 for &(v, w) in &graph.adj[u] {
1081 if active[v]
1082 && idx_map[u] < an
1083 && idx_map[v] < an
1084 && (graph.directed || idx_map[u] < idx_map[v])
1085 {
1086 sub.add_edge(idx_map[u], idx_map[v], w);
1087 }
1088 }
1089 }
1090 let gc = Self::giant_component_fraction(&sub);
1091 curve.push(((step + 1) as f64 / n as f64, gc));
1092 }
1093 curve
1094 }
1095
1096 pub fn bond_percolation_threshold(mean_degree: f64) -> f64 {
1098 if mean_degree <= 0.0 {
1099 return 1.0;
1100 }
1101 1.0 / mean_degree
1102 }
1103}
1104
1105pub struct SmallWorld;
1111
1112impl SmallWorld {
1113 pub fn watts_strogatz(n: usize, k: usize, p: f64, seed: u64) -> NetworkGraph {
1116 let mut g = NetworkGraph::new(n, false);
1117 let mut rng = Lcg::new(seed);
1118 for i in 0..n {
1120 for j in 1..=k {
1121 let neighbor = (i + j) % n;
1122 g.add_edge(i, neighbor, 1.0);
1123 }
1124 }
1125 let mut adj_new: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
1128 let mut visited_edges: std::collections::HashSet<(usize, usize)> =
1129 std::collections::HashSet::new();
1130 for i in 0..n {
1131 for j in 1..=k {
1132 let neighbor = (i + j) % n;
1133 if rng.next_f64() < p {
1134 let mut new_nb = rng.next_range(0, n);
1136 let mut attempts = 0;
1137 while (new_nb == i || visited_edges.contains(&(i.min(new_nb), i.max(new_nb))))
1138 && attempts < n * 2
1139 {
1140 new_nb = rng.next_range(0, n);
1141 attempts += 1;
1142 }
1143 if new_nb != i && !visited_edges.contains(&(i.min(new_nb), i.max(new_nb))) {
1144 let e = (i.min(new_nb), i.max(new_nb));
1145 visited_edges.insert(e);
1146 adj_new[i].push((new_nb, 1.0));
1147 adj_new[new_nb].push((i, 1.0));
1148 }
1149 } else {
1150 let e = (i.min(neighbor), i.max(neighbor));
1151 if !visited_edges.contains(&e) {
1152 visited_edges.insert(e);
1153 adj_new[i].push((neighbor, 1.0));
1154 adj_new[neighbor].push((i, 1.0));
1155 }
1156 }
1157 }
1158 }
1159 NetworkGraph {
1160 n,
1161 directed: false,
1162 adj: adj_new,
1163 }
1164 }
1165
1166 pub fn small_world_coefficient(graph: &NetworkGraph) -> f64 {
1170 let n = graph.n;
1171 if n == 0 {
1172 return 0.0;
1173 }
1174 let c = graph.mean_clustering();
1175 let l = graph.average_path_length();
1176 let mean_k = graph.adj.iter().map(|a| a.len()).sum::<usize>() as f64 / n as f64;
1178 if mean_k <= 1.0 || n <= 1 {
1179 return 0.0;
1180 }
1181 let c_rand = mean_k / n as f64;
1182 let l_rand = (n as f64).ln() / mean_k.ln();
1183 if c_rand == 0.0 || l_rand == 0.0 || l == 0.0 {
1184 return 0.0;
1185 }
1186 (c / c_rand) / (l / l_rand)
1187 }
1188}
1189
1190#[cfg(test)]
1195mod tests {
1196 use super::*;
1197
1198 #[test]
1201 fn test_network_graph_basic() {
1202 let mut g = NetworkGraph::new(4, false);
1203 g.add_edge(0, 1, 1.0);
1204 g.add_edge(1, 2, 1.0);
1205 g.add_edge(2, 3, 1.0);
1206 assert_eq!(g.edge_count(), 3);
1207 assert_eq!(g.degree(1), 2);
1208 }
1209
1210 #[test]
1211 fn test_network_graph_directed() {
1212 let mut g = NetworkGraph::new(3, true);
1213 g.add_edge(0, 1, 1.0);
1214 g.add_edge(1, 2, 1.0);
1215 assert_eq!(g.degree(0), 1);
1216 assert_eq!(g.degree(1), 1);
1217 assert_eq!(g.degree(2), 0);
1218 assert_eq!(g.edge_count(), 2);
1219 }
1220
1221 #[test]
1222 fn test_complete_graph() {
1223 let g = NetworkGraph::complete(4);
1224 assert_eq!(g.edge_count(), 6);
1226 assert_eq!(g.degree(0), 3);
1227 }
1228
1229 #[test]
1230 fn test_ring_graph_bfs() {
1231 let g = NetworkGraph::ring(5);
1232 let dist = g.bfs_distances(0);
1233 assert_eq!(dist[0], 0);
1234 assert_eq!(dist[1], 1);
1235 assert_eq!(dist[4], 1); assert_eq!(dist[2], 2);
1237 }
1238
1239 #[test]
1240 fn test_clustering_coefficient_complete() {
1241 let g = NetworkGraph::complete(5);
1242 let c = g.clustering_coefficient(0);
1244 assert!(
1245 (c - 1.0).abs() < 1e-10,
1246 "clustering should be 1 for complete graph, got {}",
1247 c
1248 );
1249 }
1250
1251 #[test]
1252 fn test_mean_clustering_ring() {
1253 let g = NetworkGraph::ring(6);
1254 let c = g.mean_clustering();
1255 assert!(c < 1e-10, "ring k=1 clustering should be 0, got {}", c);
1257 }
1258
1259 #[test]
1260 fn test_average_path_length_complete() {
1261 let g = NetworkGraph::complete(4);
1262 let apl = g.average_path_length();
1263 assert!(
1264 (apl - 1.0).abs() < 1e-10,
1265 "complete graph APL should be 1, got {}",
1266 apl
1267 );
1268 }
1269
1270 #[test]
1273 fn test_sir_population_conserved() {
1274 let sir = SirModel::new(0.3, 0.1, 1000.0);
1275 let traj = sir.integrate(990.0, 10.0, 0.0, 0.1, 200);
1276 for state in &traj {
1277 let total = state.s + state.i + state.r;
1278 assert!(
1279 (total - 1000.0).abs() < 1.0,
1280 "SIR population drift: {}",
1281 total
1282 );
1283 }
1284 }
1285
1286 #[test]
1287 fn test_sir_r0() {
1288 let sir = SirModel::new(0.3, 0.1, 1000.0);
1289 assert!((sir.r0() - 3.0).abs() < 1e-10);
1290 }
1291
1292 #[test]
1293 fn test_sir_herd_immunity_threshold() {
1294 let sir = SirModel::new(0.3, 0.1, 1000.0);
1295 let hit = sir.herd_immunity_threshold();
1297 assert!(
1298 (hit - 2.0 / 3.0).abs() < 1e-10,
1299 "HIT should be 2/3 for R0=3, got {}",
1300 hit
1301 );
1302 }
1303
1304 #[test]
1305 fn test_sir_herd_immunity_threshold_r0_lt1() {
1306 let sir = SirModel::new(0.05, 0.1, 1000.0);
1307 assert_eq!(sir.herd_immunity_threshold(), 0.0);
1309 }
1310
1311 #[test]
1312 fn test_sir_epidemic_grows_initially() {
1313 let sir = SirModel::new(0.5, 0.1, 1000.0);
1314 let traj = sir.integrate(999.0, 1.0, 0.0, 0.01, 10);
1315 assert!(traj[5].i > traj[0].i, "infected should grow when R0 > 1");
1317 }
1318
1319 #[test]
1320 fn test_sir_network_population_conserved() {
1321 let g = NetworkGraph::complete(20);
1322 let sir = SirModel::new(0.3, 0.1, 20.0);
1323 let (s_ts, i_ts, r_ts) = sir.simulate_on_graph(&g, &[0], 0.1, 50, 42);
1324 for ((&s, &i), &r) in s_ts.iter().zip(&i_ts).zip(&r_ts) {
1325 assert_eq!(
1326 s + i + r,
1327 20,
1328 "population not conserved: {}+{}+{}={}",
1329 s,
1330 i,
1331 r,
1332 s + i + r
1333 );
1334 }
1335 }
1336
1337 #[test]
1340 fn test_seird_population_conserved() {
1341 let m = SeirdModel::new(0.4, 0.2, 0.15, 0.01, 1000.0);
1342 let traj = m.integrate(990.0, 5.0, 5.0, 0.0, 0.0, 0.1, 100);
1343 for st in &traj {
1344 let total = st.s + st.e + st.i + st.r + st.d;
1345 assert!((total - 1000.0).abs() < 2.0, "SEIRD total drift: {}", total);
1346 }
1347 }
1348
1349 #[test]
1350 fn test_seird_r0() {
1351 let m = SeirdModel::new(0.4, 0.2, 0.15, 0.05, 1000.0);
1352 let r0 = m.r0();
1353 assert!((r0 - 0.4 / 0.2).abs() < 1e-10);
1354 }
1355
1356 #[test]
1357 fn test_seird_deaths_nonnegative() {
1358 let m = SeirdModel::new(0.3, 0.2, 0.1, 0.02, 500.0);
1359 let traj = m.integrate(495.0, 3.0, 2.0, 0.0, 0.0, 0.1, 50);
1360 for st in &traj {
1361 assert!(st.d >= -1e-10, "deaths should be non-negative");
1362 }
1363 }
1364
1365 #[test]
1368 fn test_opinion_dynamics_opinions_in_range() {
1369 let od = OpinionDynamics::new(0.3, 0.5);
1370 let mut ops: Vec<f64> = (0..20).map(|i| i as f64 / 20.0).collect();
1371 od.run(&mut ops, 5000, 123);
1372 for &o in &ops {
1373 assert!((0.0..=1.0).contains(&o), "opinion out of range: {}", o);
1374 }
1375 }
1376
1377 #[test]
1378 fn test_opinion_dynamics_convergence() {
1379 let od = OpinionDynamics::new(1.0, 0.5); let mut ops: Vec<f64> = (0..10).map(|i| i as f64 / 10.0).collect();
1381 od.run(&mut ops, 50000, 77);
1382 assert!(od.count_clusters(&ops) <= 2);
1384 }
1385
1386 #[test]
1387 fn test_opinion_dynamics_fragmentation() {
1388 let od = OpinionDynamics::new(0.1, 0.5); let mut ops: Vec<f64> = (0..20).map(|i| i as f64 / 20.0).collect();
1390 od.run(&mut ops, 10000, 99);
1391 let clusters = od.count_clusters(&ops);
1393 assert!(clusters >= 1);
1394 }
1395
1396 #[test]
1399 fn test_degroot_consensus_complete_graph() {
1400 let g = NetworkGraph::complete(5);
1401 let dg = DeGrootModel::from_graph(&g);
1402 let ops = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1403 let final_ops = dg.run(&ops, 100);
1404 let mean = ops.iter().sum::<f64>() / ops.len() as f64;
1405 for &o in &final_ops {
1406 assert!(
1407 (o - mean).abs() < 1e-6,
1408 "consensus failed: {} vs {}",
1409 o,
1410 mean
1411 );
1412 }
1413 }
1414
1415 #[test]
1416 fn test_degroot_opinion_preserves_mean() {
1417 let g = NetworkGraph::ring(6);
1418 let dg = DeGrootModel::from_graph(&g);
1419 let ops = vec![0.1, 0.4, 0.6, 0.2, 0.9, 0.3];
1420 let mean0: f64 = ops.iter().sum::<f64>() / ops.len() as f64;
1421 let final_ops = dg.run(&ops, 50);
1422 let mean1: f64 = final_ops.iter().sum::<f64>() / final_ops.len() as f64;
1423 assert!(
1424 (mean0 - mean1).abs() < 1e-10,
1425 "mean opinion should be preserved"
1426 );
1427 }
1428
1429 #[test]
1432 fn test_voter_model_fractions_sum_to_one() {
1433 let g = NetworkGraph::complete(10);
1434 let vm = VoterModel::new(2);
1435 let mut ops: Vec<usize> = (0..10).map(|i| i % 2).collect();
1436 vm.run(&g, &mut ops, 1000, 42);
1437 let f0 = vm.fraction(&ops, 0);
1438 let f1 = vm.fraction(&ops, 1);
1439 assert!((f0 + f1 - 1.0).abs() < 1e-10);
1440 }
1441
1442 #[test]
1443 fn test_voter_model_consensus_reached() {
1444 let g = NetworkGraph::complete(8);
1445 let vm = VoterModel::new(2);
1446 let mut ops = vec![0usize; 8];
1448 vm.run(&g, &mut ops, 1000, 5);
1449 assert!(vm.is_consensus(&ops));
1450 }
1451
1452 #[test]
1455 fn test_kuramoto_order_parameter_range() {
1456 let omegas = vec![1.0, 1.1, 0.9, 1.05, 0.95];
1457 let km = KuramotoModel::new(omegas, 5.0);
1458 let phases0: Vec<f64> = (0..5).map(|i| i as f64 * 0.1).collect();
1459 let (_, r_vals) = km.integrate(&phases0, 0.01, 100);
1460 for r in &r_vals {
1461 assert!(
1462 *r >= 0.0 && *r <= 1.0 + 1e-10,
1463 "order parameter out of range: {}",
1464 r
1465 );
1466 }
1467 }
1468
1469 #[test]
1470 fn test_kuramoto_fully_synchronized_stays_synchronized() {
1471 let omegas = vec![1.0f64; 5];
1473 let km = KuramotoModel::new(omegas, 1.0);
1474 let phases0 = vec![0.0f64; 5]; let (_, r_vals) = km.integrate(&phases0, 0.01, 100);
1476 let r_final = *r_vals.last().unwrap();
1477 assert!(
1478 r_final > 0.99,
1479 "fully synchronized should stay at r≈1, got {}",
1480 r_final
1481 );
1482 }
1483
1484 #[test]
1485 fn test_kuramoto_order_parameter_formula() {
1486 let phases = vec![0.0f64; 4]; let r = KuramotoModel::order_parameter(&phases);
1488 assert!((r - 1.0).abs() < 1e-10);
1489 }
1490
1491 #[test]
1492 fn test_kuramoto_order_parameter_opposite_phases() {
1493 let phases = vec![0.0, PI, 0.0, PI];
1495 let r = KuramotoModel::order_parameter(&phases);
1496 assert!(r < 1e-10, "opposite phases should give r≈0, got {}", r);
1497 }
1498
1499 #[test]
1500 fn test_kuramoto_critical_coupling_positive() {
1501 let omegas = vec![0.5, 1.0, 1.5, 2.0, 0.8];
1502 let km = KuramotoModel::new(omegas, 1.0);
1503 let kc = km.critical_coupling_estimate();
1504 assert!(kc > 0.0, "critical coupling should be positive");
1505 }
1506
1507 #[test]
1510 fn test_random_walk_path_length() {
1511 let g = NetworkGraph::ring(10);
1512 let rw = RandomWalkGraph::new(g);
1513 let path = rw.walk(0, 50, 1234);
1514 assert_eq!(path.len(), 51); }
1516
1517 #[test]
1518 fn test_random_walk_stays_in_graph() {
1519 let g = NetworkGraph::complete(8);
1520 let n = g.n;
1521 let rw = RandomWalkGraph::new(g);
1522 let path = rw.walk(0, 100, 7);
1523 for &node in &path {
1524 assert!(node < n, "walker left graph: {}", node);
1525 }
1526 }
1527
1528 #[test]
1529 fn test_stationary_distribution_sums_to_one() {
1530 let g = NetworkGraph::complete(5);
1531 let rw = RandomWalkGraph::new(g);
1532 let pi = rw.stationary_distribution(1e-9, 1000);
1533 let sum: f64 = pi.iter().sum();
1534 assert!(
1535 (sum - 1.0).abs() < 1e-6,
1536 "stationary dist should sum to 1, got {}",
1537 sum
1538 );
1539 }
1540
1541 #[test]
1542 fn test_stationary_distribution_uniform_regular() {
1543 let g = NetworkGraph::complete(4);
1544 let rw = RandomWalkGraph::new(g);
1545 let pi = rw.stationary_distribution(1e-9, 1000);
1546 let expected = 0.25f64;
1547 for &p in &pi {
1548 assert!(
1549 (p - expected).abs() < 1e-6,
1550 "regular graph stationary dist should be uniform, got {}",
1551 p
1552 );
1553 }
1554 }
1555
1556 #[test]
1559 fn test_pagerank_sums_to_one() {
1560 let mut g = NetworkGraph::new(4, true);
1561 g.add_edge(0, 1, 1.0);
1562 g.add_edge(1, 2, 1.0);
1563 g.add_edge(2, 0, 1.0);
1564 g.add_edge(3, 0, 1.0);
1565 let pr_solver = PageRank::new(0.85, 1e-8, 100);
1566 let pr = pr_solver.compute(&g);
1567 let sum: f64 = pr.iter().sum();
1568 assert!(
1569 (sum - 1.0).abs() < 1e-6,
1570 "PageRank should sum to 1, got {}",
1571 sum
1572 );
1573 }
1574
1575 #[test]
1576 fn test_pagerank_dangling_nodes() {
1577 let mut g = NetworkGraph::new(3, true);
1578 g.add_edge(0, 1, 1.0);
1579 let pr_solver = PageRank::new(0.85, 1e-8, 200);
1581 let pr = pr_solver.compute(&g);
1582 let sum: f64 = pr.iter().sum();
1583 assert!(
1584 (sum - 1.0).abs() < 1e-6,
1585 "PageRank with dangling node should sum to 1"
1586 );
1587 }
1588
1589 #[test]
1590 fn test_pagerank_all_positive() {
1591 let g = NetworkGraph::complete(5);
1592 let mut gd = NetworkGraph::new(5, true);
1594 for u in 0..5 {
1595 for &(v, w) in &g.adj[u] {
1596 gd.add_edge(u, v, w);
1597 }
1598 }
1599 let pr_solver = PageRank::new(0.85, 1e-8, 100);
1600 let pr = pr_solver.compute(&gd);
1601 for &p in &pr {
1602 assert!(p > 0.0, "PageRank should be positive");
1603 }
1604 }
1605
1606 #[test]
1609 fn test_modularity_trivial_all_same_community() {
1610 let g = NetworkGraph::ring(6);
1611 let communities = vec![0usize; 6]; let q = CommunityDetection::modularity(&g, &communities);
1613 assert!(q <= 1.0, "Q should be <= 1, got {}", q);
1614 }
1615
1616 #[test]
1617 fn test_modularity_two_cliques() {
1618 let mut g = NetworkGraph::new(8, false);
1621 for i in 0..4 {
1622 for j in (i + 1)..4 {
1623 g.add_edge(i, j, 1.0);
1624 }
1625 }
1626 for i in 4..8 {
1627 for j in (i + 1)..8 {
1628 g.add_edge(i, j, 1.0);
1629 }
1630 }
1631 g.add_edge(3, 4, 1.0); let com_two = vec![0, 0, 0, 0, 1, 1, 1, 1];
1634 let q_two = CommunityDetection::modularity(&g, &com_two);
1635 assert!(
1636 q_two > 0.0,
1637 "two-clique true partition Q should be positive, got {}",
1638 q_two
1639 );
1640 let com_individual: Vec<usize> = (0..8).collect();
1642 let q_individual = CommunityDetection::modularity(&g, &com_individual);
1643 assert!(
1644 q_two > q_individual,
1645 "true 2-community partition Q={} should exceed individual Q={}",
1646 q_two,
1647 q_individual
1648 );
1649 }
1650
1651 #[test]
1652 fn test_greedy_modularity_returns_valid_partition() {
1653 let g = NetworkGraph::complete(6);
1654 let communities = CommunityDetection::greedy_modularity(&g);
1655 assert_eq!(communities.len(), 6);
1656 }
1657
1658 #[test]
1661 fn test_giant_component_complete_graph() {
1662 let g = NetworkGraph::complete(10);
1663 let gc = NetworkRobustness::giant_component_fraction(&g);
1664 assert!(
1665 (gc - 1.0).abs() < 1e-10,
1666 "complete graph should have GC=1, got {}",
1667 gc
1668 );
1669 }
1670
1671 #[test]
1672 fn test_giant_component_disconnected() {
1673 let g = NetworkGraph::new(4, false); let gc = NetworkRobustness::giant_component_fraction(&g);
1675 assert!(
1676 (gc - 0.25).abs() < 1e-10,
1677 "isolated nodes GC=0.25 (each own component), got {}",
1678 gc
1679 );
1680 }
1681
1682 #[test]
1683 fn test_percolation_curve_monotone() {
1684 let g = NetworkGraph::erdos_renyi(20, 0.4, 1);
1685 let curve = NetworkRobustness::percolation_curve(&g, 42);
1686 assert!(!curve.is_empty(), "percolation curve should not be empty");
1688 assert!(curve[0].1 > 0.0, "initial GC should be positive");
1690 assert!(
1692 curve.last().unwrap().1 == 0.0,
1693 "final GC should be 0 when all nodes removed"
1694 );
1695 let early = curve[curve.len() / 5].1;
1697 let late = curve[4 * curve.len() / 5].1;
1698 assert!(
1699 late <= early + 0.2,
1700 "GC should trend downward: early={} late={}",
1701 early,
1702 late
1703 );
1704 }
1705
1706 #[test]
1707 fn test_bond_percolation_threshold() {
1708 let pct = NetworkRobustness::bond_percolation_threshold(4.0);
1709 assert!((pct - 0.25).abs() < 1e-10);
1710 }
1711
1712 #[test]
1715 fn test_watts_strogatz_node_count() {
1716 let g = SmallWorld::watts_strogatz(20, 2, 0.1, 42);
1717 assert_eq!(g.n, 20);
1718 }
1719
1720 #[test]
1721 fn test_small_world_low_p_high_clustering() {
1722 let g = SmallWorld::watts_strogatz(30, 3, 0.0, 1);
1724 let c = g.mean_clustering();
1725 assert!(
1726 c > 0.3,
1727 "regular WS lattice should have non-trivial clustering, got {}",
1728 c
1729 );
1730 }
1731
1732 #[test]
1733 fn test_erdos_renyi_mean_degree() {
1734 let n = 100;
1735 let p = 0.1;
1736 let g = NetworkGraph::erdos_renyi(n, p, 999);
1737 let mean_k = g.adj.iter().map(|a| a.len()).sum::<usize>() as f64 / n as f64;
1738 let expected = (n as f64 - 1.0) * p;
1739 assert!(
1741 (mean_k - expected).abs() < 3.0 * expected.sqrt() + 2.0,
1742 "ER mean degree {} far from expected {}",
1743 mean_k,
1744 expected
1745 );
1746 }
1747}