1#![allow(
36 unknown_lints,
37 clippy::cast_possible_truncation,
38 clippy::cast_precision_loss,
39 clippy::cast_sign_loss,
40 clippy::float_cmp,
41 clippy::too_many_arguments,
42 clippy::similar_names,
43 clippy::many_single_char_names,
44 clippy::needless_range_loop
45)]
46
47use std::collections::HashSet;
48
49use crate::core::rng::SplitMix64;
50use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
51
52fn validate(
53 nodes: u32,
54 types: u32,
55 type_dist: Option<&[f64]>,
56 pref_matrix: &[Vec<f64>],
57 directed: bool,
58) -> IgraphResult<()> {
59 if types < 1 {
60 return Err(IgraphError::InvalidArgument(
61 "The number of vertex types must be at least 1.".into(),
62 ));
63 }
64 let k = types as usize;
65 if let Some(td) = type_dist {
66 if td.len() != k {
67 return Err(IgraphError::InvalidArgument(format!(
68 "type_dist length ({}) does not match number of types ({k})",
69 td.len()
70 )));
71 }
72 for (i, &v) in td.iter().enumerate() {
73 if v.is_nan() {
74 return Err(IgraphError::InvalidArgument(format!(
75 "type_dist[{i}] must not be NaN"
76 )));
77 }
78 if v < 0.0 {
79 return Err(IgraphError::InvalidArgument(format!(
80 "type_dist[{i}] = {v} must be non-negative"
81 )));
82 }
83 }
84 }
85 if pref_matrix.len() != k {
86 return Err(IgraphError::InvalidArgument(format!(
87 "preference matrix has {} rows (expected {k})",
88 pref_matrix.len()
89 )));
90 }
91 for (i, row) in pref_matrix.iter().enumerate() {
92 if row.len() != k {
93 return Err(IgraphError::InvalidArgument(format!(
94 "preference matrix row {i} has length {} (expected {k})",
95 row.len()
96 )));
97 }
98 for (j, &p) in row.iter().enumerate() {
99 if p.is_nan() {
100 return Err(IgraphError::InvalidArgument(format!(
101 "preference matrix entry [{i}][{j}] must not be NaN"
102 )));
103 }
104 if !(0.0..=1.0).contains(&p) {
105 return Err(IgraphError::InvalidArgument(format!(
106 "preference matrix entry [{i}][{j}] = {p} must lie in [0, 1]"
107 )));
108 }
109 }
110 }
111 if !directed {
112 for (i, row_i) in pref_matrix.iter().enumerate() {
113 for (j, row_j) in pref_matrix.iter().enumerate().skip(i + 1) {
114 if row_i[j] != row_j[i] {
115 return Err(IgraphError::InvalidArgument(format!(
116 "preference matrix must be symmetric for undirected graphs: \
117 pref[{i}][{j}] = {} but pref[{j}][{i}] = {}",
118 row_i[j], row_j[i],
119 )));
120 }
121 }
122 }
123 }
124 let _ = nodes;
128 Ok(())
129}
130
131fn cumdist_lookup(cumdist: &[f64], u: f64) -> usize {
134 let mut lo = 1usize;
135 let mut hi = cumdist.len();
136 while lo < hi {
137 let mid = lo + (hi - lo) / 2;
138 if cumdist[mid] > u {
139 hi = mid;
140 } else {
141 lo = mid + 1;
142 }
143 }
144 lo.min(cumdist.len() - 1).max(1)
145}
146
147fn floyd_distinct_sample(rng: &mut SplitMix64, n: u32, m: u32) -> Vec<u32> {
150 debug_assert!(m <= n);
151 let cap = m as usize;
152 let mut chosen: HashSet<u32> = HashSet::with_capacity(cap);
153 let mut out: Vec<u32> = Vec::with_capacity(cap);
154 for j in (n - m)..n {
155 let span = (j as usize) + 1;
156 let t = rng.gen_index(span) as u32;
157 let pick = if chosen.insert(t) {
158 t
159 } else {
160 chosen.insert(j);
161 j
162 };
163 out.push(pick);
164 }
165 out.sort_unstable();
166 out
167}
168
169pub fn establishment_game(
213 nodes: u32,
214 types: u32,
215 k: u32,
216 type_dist: Option<&[f64]>,
217 pref_matrix: &[Vec<f64>],
218 directed: bool,
219 seed: u64,
220) -> IgraphResult<(Graph, Vec<u32>)> {
221 validate(nodes, types, type_dist, pref_matrix, directed)?;
222
223 let n_types = types as usize;
224 let mut rng = SplitMix64::new(seed);
225
226 let mut cumdist = vec![0.0f64; n_types + 1];
227 if let Some(td) = type_dist {
228 for i in 0..n_types {
229 cumdist[i + 1] = cumdist[i] + td[i];
230 }
231 } else {
232 for i in 0..n_types {
233 cumdist[i + 1] = (i + 1) as f64;
234 }
235 }
236 let maxcum = cumdist[n_types];
237 if maxcum <= 0.0 {
238 return Err(IgraphError::InvalidArgument(
239 "type_dist must contain at least one positive value".into(),
240 ));
241 }
242
243 let mut node_types = vec![0u32; nodes as usize];
244 for v in 0..(nodes as usize) {
245 let u = rng.gen_unit() * maxcum;
246 let pos = cumdist_lookup(&cumdist, u);
247 let t = (pos - 1).min(n_types - 1);
248 node_types[v] = t as u32;
249 }
250
251 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
252 if k > 0 && nodes > k {
253 for i in k..nodes {
254 let type1 = node_types[i as usize] as usize;
255 let picks = floyd_distinct_sample(&mut rng, i, k);
256 for &j in &picks {
257 let type2 = node_types[j as usize] as usize;
258 let p = pref_matrix[type1][type2];
259 if p > 0.0 && rng.gen_unit() < p {
260 edges.push((i, j));
261 }
262 }
263 }
264 }
265
266 let mut g = Graph::new(nodes, directed)?;
267 g.add_edges(edges)?;
268 Ok((g, node_types))
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use std::collections::HashSet as Set;
275
276 fn diag_pref(types: usize, p: f64) -> Vec<Vec<f64>> {
277 (0..types)
278 .map(|i| (0..types).map(|j| if i == j { p } else { 0.0 }).collect())
279 .collect()
280 }
281
282 fn full_pref(types: usize, p: f64) -> Vec<Vec<f64>> {
283 vec![vec![p; types]; types]
284 }
285
286 #[test]
287 fn nodes_zero_returns_empty_graph() {
288 let pref = full_pref(2, 0.5);
289 let (g, types) = establishment_game(0, 2, 5, None, &pref, false, 42).unwrap();
290 assert_eq!(g.vcount(), 0);
291 assert_eq!(g.ecount(), 0);
292 assert_eq!(types.len(), 0);
293 assert!(!g.is_directed());
294 }
295
296 #[test]
297 fn nodes_le_k_yields_no_edges() {
298 let pref = full_pref(2, 1.0);
299 let (g, types) = establishment_game(5, 2, 5, None, &pref, false, 7).unwrap();
300 assert_eq!(g.vcount(), 5);
301 assert_eq!(g.ecount(), 0);
302 assert_eq!(types.len(), 5);
303 }
304
305 #[test]
306 fn k_zero_yields_no_edges() {
307 let pref = full_pref(2, 1.0);
308 let (g, _) = establishment_game(20, 2, 0, None, &pref, false, 7).unwrap();
309 assert_eq!(g.ecount(), 0);
310 }
311
312 #[test]
313 fn zero_pref_means_no_edges() {
314 let pref = vec![vec![0.0]];
316 let (g, types) = establishment_game(20, 1, 5, Some(&[1.0]), &pref, false, 11).unwrap();
317 assert_eq!(g.vcount(), 20);
318 assert_eq!(g.ecount(), 0);
319 assert!(types.iter().all(|&t| t == 0));
320 }
321
322 #[test]
323 fn full_pref_saturates_edges() {
324 let pref = full_pref(2, 1.0);
326 let (g, _) = establishment_game(30, 2, 5, None, &pref, false, 13).unwrap();
327 let n: usize = 30;
329 let k: usize = 5;
330 assert_eq!(g.ecount(), (n - k) * k);
331 }
332
333 #[test]
334 fn cross_only_pref_yields_bipartite_directed() {
335 let pref = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
337 let (g, types) = establishment_game(40, 2, 5, Some(&[1.0, 1.0]), &pref, true, 19).unwrap();
338 assert_eq!(g.vcount(), 40);
339 assert!(g.is_directed());
340 let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
342 for eid in 0..m {
343 let (u, v) = g.edge(eid).expect("edge id in bounds");
344 assert_ne!(types[u as usize], types[v as usize]);
345 }
346 }
347
348 #[test]
349 fn deterministic_for_fixed_seed() {
350 let pref = diag_pref(3, 0.4);
351 let (g1, t1) =
352 establishment_game(50, 3, 4, Some(&[1.0, 2.0, 3.0]), &pref, false, 0xABCDu64).unwrap();
353 let (g2, t2) =
354 establishment_game(50, 3, 4, Some(&[1.0, 2.0, 3.0]), &pref, false, 0xABCDu64).unwrap();
355 assert_eq!(g1.ecount(), g2.ecount());
356 assert_eq!(t1, t2);
357 let m = u32::try_from(g1.ecount()).expect("fits");
358 for eid in 0..m {
359 assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
360 }
361 }
362
363 #[test]
364 fn distinct_seeds_differ() {
365 let pref = full_pref(2, 0.4);
366 let (g1, _) = establishment_game(50, 2, 4, None, &pref, false, 1).unwrap();
367 let (g2, _) = establishment_game(50, 2, 4, None, &pref, false, 2).unwrap();
368 let m1 = u32::try_from(g1.ecount()).unwrap();
370 let m2 = u32::try_from(g2.ecount()).unwrap();
371 let same_edges = m1 == m2 && {
372 let mut all_eq = true;
373 for eid in 0..m1 {
374 if g1.edge(eid).unwrap() != g2.edge(eid).unwrap() {
375 all_eq = false;
376 break;
377 }
378 }
379 all_eq
380 };
381 assert!(!same_edges);
382 }
383
384 #[test]
385 fn diagonal_pref_keeps_edges_within_types() {
386 let pref = diag_pref(3, 0.6);
389 let (g, types) =
390 establishment_game(80, 3, 4, Some(&[1.0, 1.0, 1.0]), &pref, false, 23).unwrap();
391 let m = u32::try_from(g.ecount()).unwrap();
392 for eid in 0..m {
393 let (u, v) = g.edge(eid).unwrap();
394 assert_eq!(types[u as usize], types[v as usize]);
395 }
396 }
397
398 #[test]
399 fn graph_is_simple_no_loops_no_multi() {
400 let pref = full_pref(3, 0.7);
403 let (g, _) = establishment_game(60, 3, 4, None, &pref, false, 31).unwrap();
404 let m = u32::try_from(g.ecount()).unwrap();
405 let mut seen: Set<(VertexId, VertexId)> = Set::with_capacity(m as usize);
406 for eid in 0..m {
407 let (a, b) = g.edge(eid).unwrap();
408 assert_ne!(a, b, "no self-loops");
409 let key = if a <= b { (a, b) } else { (b, a) };
410 assert!(seen.insert(key), "edge {key:?} appears twice");
411 }
412 }
413
414 #[test]
415 fn types_within_range_uniform() {
416 let pref = full_pref(4, 0.0);
417 let (_g, types) = establishment_game(200, 4, 1, None, &pref, false, 99).unwrap();
418 assert!(types.iter().all(|&t| t < 4));
419 let mut seen = [false; 4];
421 for &t in &types {
422 seen[t as usize] = true;
423 }
424 assert!(seen.iter().all(|&x| x));
425 }
426
427 #[test]
428 fn types_zero_rejected() {
429 let pref: Vec<Vec<f64>> = vec![];
430 let err = establishment_game(5, 0, 1, None, &pref, false, 1).unwrap_err();
431 assert!(matches!(err, IgraphError::InvalidArgument(_)));
432 }
433
434 #[test]
435 fn type_dist_wrong_length_rejected() {
436 let pref = full_pref(2, 0.1);
437 let err = establishment_game(10, 2, 1, Some(&[1.0]), &pref, false, 1).unwrap_err();
438 assert!(matches!(err, IgraphError::InvalidArgument(_)));
439 }
440
441 #[test]
442 fn type_dist_negative_rejected() {
443 let pref = full_pref(2, 0.1);
444 let err = establishment_game(10, 2, 1, Some(&[1.0, -0.1]), &pref, false, 1).unwrap_err();
445 assert!(matches!(err, IgraphError::InvalidArgument(_)));
446 }
447
448 #[test]
449 fn type_dist_all_zero_rejected() {
450 let pref = full_pref(2, 0.1);
451 let err = establishment_game(10, 2, 1, Some(&[0.0, 0.0]), &pref, false, 1).unwrap_err();
452 assert!(matches!(err, IgraphError::InvalidArgument(_)));
453 }
454
455 #[test]
456 fn pref_out_of_range_rejected() {
457 let pref = vec![vec![1.5, 0.1], vec![0.1, 0.5]];
458 let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
459 assert!(matches!(err, IgraphError::InvalidArgument(_)));
460 let pref = vec![vec![-0.1, 0.1], vec![0.1, 0.5]];
461 let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
462 assert!(matches!(err, IgraphError::InvalidArgument(_)));
463 }
464
465 #[test]
466 fn pref_nan_rejected() {
467 let pref = vec![vec![f64::NAN, 0.1], vec![0.1, 0.5]];
468 let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
469 assert!(matches!(err, IgraphError::InvalidArgument(_)));
470 }
471
472 #[test]
473 fn pref_asymmetric_undirected_rejected() {
474 let pref = vec![vec![0.5, 0.1], vec![0.2, 0.5]];
475 let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
476 assert!(matches!(err, IgraphError::InvalidArgument(_)));
477 }
478
479 #[test]
480 fn pref_asymmetric_directed_ok() {
481 let pref = vec![vec![0.5, 0.1], vec![0.2, 0.5]];
482 let (g, _) = establishment_game(10, 2, 1, None, &pref, true, 1).unwrap();
483 assert_eq!(g.vcount(), 10);
484 assert!(g.is_directed());
485 }
486
487 #[test]
488 fn pref_wrong_shape_rejected() {
489 let pref = vec![vec![0.5, 0.1, 0.1], vec![0.1, 0.5, 0.1]];
490 let err = establishment_game(10, 2, 1, None, &pref, true, 1).unwrap_err();
491 assert!(matches!(err, IgraphError::InvalidArgument(_)));
492 }
493
494 #[test]
495 fn directed_no_loops_construction_invariant() {
496 let pref = full_pref(2, 1.0);
499 let (g, _) = establishment_game(30, 2, 3, None, &pref, true, 5).unwrap();
500 let m = u32::try_from(g.ecount()).unwrap();
501 for eid in 0..m {
502 let (a, b) = g.edge(eid).unwrap();
503 assert_ne!(a, b);
504 assert!(a > b, "growth direction always points back in time");
505 }
506 }
507}
508
509#[cfg(all(test, feature = "proptest-harness"))]
510mod proptest_harness {
511 use super::*;
512 use proptest::prelude::*;
513
514 proptest! {
515 #![proptest_config(ProptestConfig::with_cases(40))]
516
517 #[test]
518 fn types_in_range(
519 seed in any::<u64>(),
520 n in 0u32..120,
521 t in 1u32..5,
522 k in 0u32..6,
523 directed in any::<bool>(),
524 ) {
525 let types_usize = t as usize;
526 let pref = vec![vec![0.5; types_usize]; types_usize];
527 let (g, types) = establishment_game(n, t, k, None, &pref, directed, seed).unwrap();
528 prop_assert_eq!(g.vcount(), n);
529 prop_assert_eq!(types.len() as u32, n);
530 for &tt in &types {
531 prop_assert!(tt < t);
532 }
533 }
534
535 #[test]
536 fn ecount_band(
537 seed in any::<u64>(),
538 n in 5u32..80,
539 t in 1u32..4,
540 k in 0u32..5,
541 p in 0.0f64..=1.0f64,
542 ) {
543 let types_usize = t as usize;
544 let pref = vec![vec![p; types_usize]; types_usize];
545 let (g, _) = establishment_game(n, t, k, None, &pref, false, seed).unwrap();
546 if n > k {
548 let bound = u64::from(n - k) * u64::from(k);
549 prop_assert!(g.ecount() as u64 <= bound);
550 } else {
551 prop_assert_eq!(g.ecount(), 0);
552 }
553 }
554
555 #[test]
556 fn deterministic(
557 seed in any::<u64>(),
558 n in 0u32..60,
559 t in 1u32..4,
560 k in 0u32..5,
561 ) {
562 let types_usize = t as usize;
563 let pref = vec![vec![0.3; types_usize]; types_usize];
564 let (g1, types1) = establishment_game(n, t, k, None, &pref, false, seed).unwrap();
565 let (g2, types2) = establishment_game(n, t, k, None, &pref, false, seed).unwrap();
566 prop_assert_eq!(types1, types2);
567 prop_assert_eq!(g1.ecount(), g2.ecount());
568 let m = u32::try_from(g1.ecount()).unwrap();
569 for eid in 0..m {
570 prop_assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
571 }
572 }
573
574 #[test]
575 fn diagonal_pref_stays_within_types(
576 seed in any::<u64>(),
577 n in 10u32..80,
578 t in 2u32..5,
579 k in 1u32..5,
580 ) {
581 let types_usize = t as usize;
582 let pref: Vec<Vec<f64>> = (0..types_usize)
583 .map(|i| {
584 (0..types_usize)
585 .map(|j| if i == j { 0.6 } else { 0.0 })
586 .collect()
587 })
588 .collect();
589 let (g, types) =
590 establishment_game(n, t, k, None, &pref, false, seed).unwrap();
591 let m = u32::try_from(g.ecount()).unwrap();
592 for eid in 0..m {
593 let (u, v) = g.edge(eid).unwrap();
594 prop_assert_eq!(types[u as usize], types[v as usize]);
595 }
596 }
597
598 #[test]
599 fn cross_only_pref_yields_cross_edges(
600 seed in any::<u64>(),
601 n in 10u32..80,
602 k in 1u32..5,
603 ) {
604 let pref = vec![vec![0.0, 0.7], vec![0.7, 0.0]];
606 let (g, types) =
607 establishment_game(n, 2, k, Some(&[1.0, 1.0]), &pref, false, seed).unwrap();
608 let m = u32::try_from(g.ecount()).unwrap();
609 for eid in 0..m {
610 let (u, v) = g.edge(eid).unwrap();
611 prop_assert_ne!(types[u as usize], types[v as usize]);
612 }
613 }
614 }
615}