1#![allow(
51 unknown_lints,
52 clippy::cast_possible_truncation,
53 clippy::cast_precision_loss,
54 clippy::cast_sign_loss,
55 clippy::float_cmp,
56 clippy::too_many_arguments,
57 clippy::similar_names,
58 clippy::many_single_char_names,
59 clippy::needless_range_loop
60)]
61
62use crate::algorithms::games::sbm::PairShape;
63use crate::core::rng::SplitMix64;
64use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
65
66const MAX_NODES: u64 = 1u64 << 53;
70
71fn pair_shape(directed: bool, loops: bool, on_diagonal: bool) -> PairShape {
76 match (directed, loops, on_diagonal) {
77 (true, false, true) => PairShape::RectNoDiag,
78 (false, true, true) => PairShape::TriInclDiag,
79 (false, false, true) => PairShape::TriExclDiag,
80 _ => PairShape::Rect,
81 }
82}
83
84fn shape_maxedges(shape: PairShape, fromsize: u32, tosize: u32) -> u64 {
85 match shape {
86 PairShape::Rect => u64::from(fromsize) * u64::from(tosize),
87 PairShape::RectNoDiag => {
88 let fs = u64::from(fromsize);
89 fs * fs.saturating_sub(1)
90 }
91 PairShape::TriInclDiag => {
92 let fs = u64::from(fromsize);
93 fs * (fs + 1) / 2
94 }
95 PairShape::TriExclDiag => {
96 let fs = u64::from(fromsize);
97 fs * fs.saturating_sub(1) / 2
98 }
99 }
100}
101
102fn validate_symmetric(
107 nodes: u32,
108 types: u32,
109 type_dist: Option<&[f64]>,
110 fixed_sizes: bool,
111 pref_matrix: &[Vec<f64>],
112 directed: bool,
113) -> IgraphResult<()> {
114 if types < 1 {
115 return Err(IgraphError::InvalidArgument(
116 "The number of vertex types must be at least 1.".into(),
117 ));
118 }
119 let k = types as usize;
120 if let Some(td) = type_dist {
121 if td.len() != k {
122 return Err(IgraphError::InvalidArgument(format!(
123 "type_dist length ({}) does not match number of types ({k})",
124 td.len()
125 )));
126 }
127 for (i, &v) in td.iter().enumerate() {
128 if v.is_nan() {
129 return Err(IgraphError::InvalidArgument(format!(
130 "type_dist[{i}] must not be NaN"
131 )));
132 }
133 if v < 0.0 {
134 return Err(IgraphError::InvalidArgument(format!(
135 "type_dist[{i}] = {v} must be non-negative"
136 )));
137 }
138 }
139 }
140 if pref_matrix.len() != k {
141 return Err(IgraphError::InvalidArgument(format!(
142 "preference matrix has {} rows (expected {k})",
143 pref_matrix.len()
144 )));
145 }
146 for (i, row) in pref_matrix.iter().enumerate() {
147 if row.len() != k {
148 return Err(IgraphError::InvalidArgument(format!(
149 "preference matrix row {i} has length {} (expected {k})",
150 row.len()
151 )));
152 }
153 for (j, &p) in row.iter().enumerate() {
154 if p.is_nan() {
155 return Err(IgraphError::InvalidArgument(format!(
156 "preference matrix entry [{i}][{j}] must not be NaN"
157 )));
158 }
159 if !(0.0..=1.0).contains(&p) {
160 return Err(IgraphError::InvalidArgument(format!(
161 "preference matrix entry [{i}][{j}] = {p} must lie in [0, 1]"
162 )));
163 }
164 }
165 }
166 if !directed {
167 for (i, row_i) in pref_matrix.iter().enumerate() {
168 for (j, row_j) in pref_matrix.iter().enumerate().skip(i + 1) {
169 let pij = row_i[j];
170 let pji = row_j[i];
171 if pij != pji {
172 return Err(IgraphError::InvalidArgument(format!(
173 "preference matrix must be symmetric for undirected graphs: \
174 pref[{i}][{j}] = {pij} but pref[{j}][{i}] = {pji}"
175 )));
176 }
177 }
178 }
179 }
180 if fixed_sizes {
181 if let Some(td) = type_dist {
182 let mut sum: u64 = 0;
184 for (i, &v) in td.iter().enumerate() {
185 if v < 0.0 || !v.is_finite() {
186 return Err(IgraphError::InvalidArgument(format!(
187 "type_dist[{i}] = {v} must be a non-negative finite count when fixed_sizes is set"
188 )));
189 }
190 let c = v as u64;
191 #[allow(clippy::float_cmp)]
192 if (c as f64) != v {
193 return Err(IgraphError::InvalidArgument(format!(
194 "type_dist[{i}] = {v} must be an integer count when fixed_sizes is set"
195 )));
196 }
197 sum = sum.checked_add(c).ok_or_else(|| {
198 IgraphError::InvalidArgument("sum of type_dist overflows u64".into())
199 })?;
200 }
201 if sum != u64::from(nodes) {
202 return Err(IgraphError::InvalidArgument(format!(
203 "fixed_sizes requires sum(type_dist) = nodes; got {sum} vs {nodes}"
204 )));
205 }
206 }
207 }
208 if u64::from(nodes) > MAX_NODES {
209 return Err(IgraphError::InvalidArgument(format!(
210 "nodes = {nodes} exceeds f64-exact ceiling 2^53"
211 )));
212 }
213 Ok(())
214}
215
216fn cumdist_lookup(cumdist: &[f64], u: f64) -> usize {
219 let mut lo = 1usize;
223 let mut hi = cumdist.len();
224 while lo < hi {
225 let mid = lo + (hi - lo) / 2;
226 if cumdist[mid] > u {
227 hi = mid;
228 } else {
229 lo = mid + 1;
230 }
231 }
232 lo.min(cumdist.len() - 1).max(1)
233}
234
235fn assign_types_random(
236 rng: &mut SplitMix64,
237 nodes: u32,
238 types: u32,
239 type_dist: Option<&[f64]>,
240 node_types: &mut [u32],
241 vids_by_type: &mut [Vec<u32>],
242) -> IgraphResult<()> {
243 let k = types as usize;
244 let mut cumdist = vec![0.0f64; k + 1];
245 if let Some(td) = type_dist {
246 for i in 0..k {
247 cumdist[i + 1] = cumdist[i] + td[i];
248 }
249 } else {
250 for i in 0..k {
251 cumdist[i + 1] = (i + 1) as f64;
252 }
253 }
254 let maxcum = cumdist[k];
255 if maxcum <= 0.0 {
256 return Err(IgraphError::InvalidArgument(
257 "type_dist must have a positive total mass".into(),
258 ));
259 }
260 for v in 0..nodes {
261 let u = rng.gen_unit() * maxcum;
262 let pos = cumdist_lookup(&cumdist, u);
263 let t = (pos - 1).min(k - 1);
264 node_types[v as usize] = t as u32;
265 vids_by_type[t].push(v);
266 }
267 Ok(())
268}
269
270fn assign_types_fixed(
271 nodes: u32,
272 types: u32,
273 type_dist: Option<&[f64]>,
274 node_types: &mut [u32],
275 vids_by_type: &mut [Vec<u32>],
276) {
277 let mut an: u32 = 0;
278 if let Some(td) = type_dist {
279 for (i, &cnt_f) in td.iter().enumerate() {
280 let cnt = cnt_f as u32;
281 for _ in 0..cnt {
282 if an >= nodes {
283 break;
284 }
285 node_types[an as usize] = i as u32;
286 vids_by_type[i].push(an);
287 an += 1;
288 }
289 }
290 } else {
291 let size_one = nodes / types;
292 let extra = nodes - size_one * types;
293 for (i, slot) in vids_by_type.iter_mut().enumerate() {
294 for _ in 0..size_one {
295 node_types[an as usize] = i as u32;
296 slot.push(an);
297 an += 1;
298 }
299 if (i as u32) < extra {
300 node_types[an as usize] = i as u32;
301 slot.push(an);
302 an += 1;
303 }
304 }
305 }
306}
307
308fn sample_block_indirect(
312 rng: &mut SplitMix64,
313 edges: &mut Vec<(VertexId, VertexId)>,
314 v1: &[u32],
315 v2: &[u32],
316 shape: PairShape,
317 p: f64,
318 maxedges: u64,
319) {
320 if maxedges == 0 || p <= 0.0 {
321 return;
322 }
323 let v1size = v1.len() as u32;
324 let max_f = maxedges as f64;
325 let mut last = rng.gen_geom(p);
326 while last < max_f {
327 let idx = last.trunc() as u64;
328 if idx >= maxedges {
329 break;
330 }
331 let (vfrom, vto) = shape.decode(idx, v1size);
332 edges.push((v1[vfrom as usize], v2[vto as usize]));
333 last += rng.gen_geom(p);
334 last += 1.0;
335 }
336}
337
338pub fn preference_game(
394 nodes: u32,
395 types: u32,
396 type_dist: Option<&[f64]>,
397 fixed_sizes: bool,
398 pref_matrix: &[Vec<f64>],
399 directed: bool,
400 loops: bool,
401 seed: u64,
402) -> IgraphResult<(Graph, Vec<u32>)> {
403 validate_symmetric(nodes, types, type_dist, fixed_sizes, pref_matrix, directed)?;
404
405 let k = types as usize;
406 let mut node_types = vec![0u32; nodes as usize];
407 let mut vids_by_type: Vec<Vec<u32>> = vec![Vec::new(); k];
408 let mut rng = SplitMix64::new(seed);
409
410 if fixed_sizes {
411 assign_types_fixed(nodes, types, type_dist, &mut node_types, &mut vids_by_type);
412 } else {
413 assign_types_random(
414 &mut rng,
415 nodes,
416 types,
417 type_dist,
418 &mut node_types,
419 &mut vids_by_type,
420 )?;
421 }
422
423 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
424 for i in 0..k {
425 for j in 0..k {
426 if !directed && i > j {
427 continue;
428 }
429 let p = pref_matrix[i][j];
430 if p <= 0.0 {
431 continue;
432 }
433 let v1 = &vids_by_type[i];
434 let v2 = &vids_by_type[j];
435 let v1s = v1.len() as u32;
436 let v2s = v2.len() as u32;
437 if v1s == 0 || v2s == 0 {
438 continue;
439 }
440 let on_diag = i == j;
441 let shape = pair_shape(directed, loops, on_diag);
442 let maxedges = shape_maxedges(shape, v1s, v2s);
443 if maxedges as f64 > MAX_NODES as f64 {
444 return Err(IgraphError::InvalidArgument(
445 "block maxedges overflows f64-exact representation".into(),
446 ));
447 }
448 sample_block_indirect(&mut rng, &mut edges, v1, v2, shape, p, maxedges);
449 }
450 }
451
452 let mut g = Graph::new(nodes, directed)?;
453 g.add_edges(edges)?;
454 Ok((g, node_types))
455}
456
457fn validate_asymmetric(
462 nodes: u32,
463 no_out_types: u32,
464 no_in_types: u32,
465 type_dist_matrix: Option<&[Vec<f64>]>,
466 pref_matrix: &[Vec<f64>],
467) -> IgraphResult<()> {
468 if no_out_types < 1 {
469 return Err(IgraphError::InvalidArgument(
470 "no_out_types must be at least 1".into(),
471 ));
472 }
473 if no_in_types < 1 {
474 return Err(IgraphError::InvalidArgument(
475 "no_in_types must be at least 1".into(),
476 ));
477 }
478 let no_out = no_out_types as usize;
479 let no_in = no_in_types as usize;
480 if let Some(tdm) = type_dist_matrix {
481 if tdm.len() != no_out {
482 return Err(IgraphError::InvalidArgument(format!(
483 "type_dist_matrix has {} rows (expected {no_out})",
484 tdm.len()
485 )));
486 }
487 for (i, row) in tdm.iter().enumerate() {
488 if row.len() != no_in {
489 return Err(IgraphError::InvalidArgument(format!(
490 "type_dist_matrix row {i} has length {} (expected {no_in})",
491 row.len()
492 )));
493 }
494 for (j, &v) in row.iter().enumerate() {
495 if v.is_nan() {
496 return Err(IgraphError::InvalidArgument(format!(
497 "type_dist_matrix[{i}][{j}] must not be NaN"
498 )));
499 }
500 if v < 0.0 {
501 return Err(IgraphError::InvalidArgument(format!(
502 "type_dist_matrix[{i}][{j}] = {v} must be non-negative"
503 )));
504 }
505 }
506 }
507 }
508 if pref_matrix.len() != no_out {
509 return Err(IgraphError::InvalidArgument(format!(
510 "preference matrix has {} rows (expected {no_out})",
511 pref_matrix.len()
512 )));
513 }
514 for (i, row) in pref_matrix.iter().enumerate() {
515 if row.len() != no_in {
516 return Err(IgraphError::InvalidArgument(format!(
517 "preference matrix row {i} has length {} (expected {no_in})",
518 row.len()
519 )));
520 }
521 for (j, &p) in row.iter().enumerate() {
522 if p.is_nan() {
523 return Err(IgraphError::InvalidArgument(format!(
524 "preference matrix entry [{i}][{j}] must not be NaN"
525 )));
526 }
527 if !(0.0..=1.0).contains(&p) {
528 return Err(IgraphError::InvalidArgument(format!(
529 "preference matrix entry [{i}][{j}] = {p} must lie in [0, 1]"
530 )));
531 }
532 }
533 }
534 if u64::from(nodes) > MAX_NODES {
535 return Err(IgraphError::InvalidArgument(format!(
536 "nodes = {nodes} exceeds f64-exact ceiling 2^53"
537 )));
538 }
539 Ok(())
540}
541
542fn sorted_intersection(a: &[u32], b: &[u32]) -> Vec<u32> {
544 let mut out = Vec::new();
545 let (mut i, mut j) = (0usize, 0usize);
546 while i < a.len() && j < b.len() {
547 match a[i].cmp(&b[j]) {
548 std::cmp::Ordering::Less => i += 1,
549 std::cmp::Ordering::Greater => j += 1,
550 std::cmp::Ordering::Equal => {
551 out.push(a[i]);
552 i += 1;
553 j += 1;
554 }
555 }
556 }
557 out
558}
559
560fn binsearch_pos(xs: &[u32], target: u32) -> Option<usize> {
562 xs.binary_search(&target).ok()
563}
564
565fn sample_asym_pair(
569 rng: &mut SplitMix64,
570 edges: &mut Vec<(VertexId, VertexId)>,
571 v1: &[u32],
572 v2: &[u32],
573 intersect: &[u32],
574 p: f64,
575 maxedges: u64,
576 loops: bool,
577) {
578 if maxedges == 0 || p <= 0.0 {
579 return;
580 }
581 let v1size = v1.len() as u32;
582 let v2size = v2.len() as u32;
583 let max_f = maxedges as f64;
584 let intersect_count = intersect.len();
585
586 let mut last = rng.gen_geom(p);
587 while last < max_f {
588 let idx = last.trunc() as u64;
589 if idx >= maxedges {
590 break;
591 }
592 let to = idx / u64::from(v1size);
593 let from = idx - to * u64::from(v1size);
594 let (mut vfrom, mut vto) = (from as u32, to as u32);
595
596 if !loops && intersect_count > 0 && v1[vfrom as usize] == v2[vto as usize] {
597 vto = v2size - 1;
601 let loop_vid = v1[vfrom as usize];
602 let mut c = binsearch_pos(intersect, loop_vid).unwrap_or(intersect_count);
603 let mut from_idx: i64 = i64::from(v1size) - 1;
604 if from_idx >= 0 && v1[from_idx as usize] == v2[vto as usize] {
606 from_idx -= 1;
607 }
608 while c > 0 && from_idx >= 0 {
609 c -= 1;
610 from_idx -= 1;
611 if from_idx >= 0 && v1[from_idx as usize] == v2[vto as usize] {
612 from_idx -= 1;
613 }
614 }
615 if from_idx < 0 {
620 last += rng.gen_geom(p);
621 last += 1.0;
622 continue;
623 }
624 vfrom = from_idx as u32;
625 }
626 edges.push((v1[vfrom as usize], v2[vto as usize]));
627 last += rng.gen_geom(p);
628 last += 1.0;
629 }
630}
631
632pub fn asymmetric_preference_game(
680 nodes: u32,
681 no_out_types: u32,
682 no_in_types: u32,
683 type_dist_matrix: Option<&[Vec<f64>]>,
684 pref_matrix: &[Vec<f64>],
685 loops: bool,
686 seed: u64,
687) -> IgraphResult<(Graph, Vec<u32>, Vec<u32>)> {
688 validate_asymmetric(
689 nodes,
690 no_out_types,
691 no_in_types,
692 type_dist_matrix,
693 pref_matrix,
694 )?;
695
696 let no_out = no_out_types as usize;
697 let no_in = no_in_types as usize;
698 let mut node_out = vec![0u32; nodes as usize];
699 let mut node_in = vec![0u32; nodes as usize];
700 let mut vids_by_outtype: Vec<Vec<u32>> = vec![Vec::new(); no_out];
701 let mut vids_by_intype: Vec<Vec<u32>> = vec![Vec::new(); no_in];
702 let mut rng = SplitMix64::new(seed);
703
704 let total_cells = no_out * no_in;
709 let mut cumdist = vec![0.0f64; total_cells + 1];
710 let mut k = 0usize;
711 if let Some(tdm) = type_dist_matrix {
712 for j_in in 0..no_in {
715 for (i_out, row) in tdm.iter().enumerate().take(no_out) {
716 cumdist[k + 1] = cumdist[k] + row[j_in];
717 k += 1;
718 let _ = i_out; }
720 }
721 } else {
722 for i in 0..total_cells {
723 cumdist[i + 1] = (i + 1) as f64;
724 }
725 }
726 let maxcum = cumdist[total_cells];
727 if maxcum <= 0.0 {
728 return Err(IgraphError::InvalidArgument(
729 "type_dist_matrix must have a positive total mass".into(),
730 ));
731 }
732
733 for v in 0..nodes {
734 let u = rng.gen_unit() * maxcum;
735 let pos = cumdist_lookup(&cumdist, u);
736 let cell = (pos - 1).min(total_cells - 1);
737 let out_t = (cell % no_out) as u32;
738 let in_t = (cell / no_out) as u32;
739 node_out[v as usize] = out_t;
740 node_in[v as usize] = in_t;
741 vids_by_outtype[out_t as usize].push(v);
742 vids_by_intype[in_t as usize].push(v);
743 }
744
745 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
746 for i in 0..no_out {
747 for j in 0..no_in {
748 let p = pref_matrix[i][j];
749 if p <= 0.0 {
750 continue;
751 }
752 let v1 = &vids_by_outtype[i];
753 let v2 = &vids_by_intype[j];
754 let v1s = v1.len() as u32;
755 let v2s = v2.len() as u32;
756 if v1s == 0 || v2s == 0 {
757 continue;
758 }
759 let mut maxedges: u64 = u64::from(v1s) * u64::from(v2s);
760 if maxedges as f64 > MAX_NODES as f64 {
761 return Err(IgraphError::InvalidArgument(
762 "asymmetric block maxedges overflows f64-exact representation".into(),
763 ));
764 }
765 let intersect = if loops {
766 Vec::new()
767 } else {
768 sorted_intersection(v1, v2)
769 };
770 if !loops {
771 maxedges -= intersect.len() as u64;
772 }
773 sample_asym_pair(&mut rng, &mut edges, v1, v2, &intersect, p, maxedges, loops);
774 }
775 }
776
777 let mut g = Graph::new(nodes, true)?;
778 g.add_edges(edges)?;
779 Ok((g, node_out, node_in))
780}
781
782#[cfg(test)]
787mod tests {
788 use super::*;
789
790 fn diag_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
791 let mut m = vec![vec![0.0; k]; k];
792 for (i, row) in m.iter_mut().enumerate() {
793 row[i] = p;
794 }
795 m
796 }
797
798 fn uniform_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
799 vec![vec![p; k]; k]
800 }
801
802 #[test]
805 fn rejects_zero_types() {
806 let res = preference_game(10, 0, None, false, &[], false, false, 0);
807 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
808 }
809
810 #[test]
811 fn rejects_pref_wrong_rows() {
812 let pref = vec![vec![0.1, 0.1]];
813 let res = preference_game(10, 2, None, false, &pref, false, false, 0);
814 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
815 }
816
817 #[test]
818 fn rejects_pref_wrong_cols() {
819 let pref = vec![vec![0.1], vec![0.1, 0.1]];
820 let res = preference_game(10, 2, None, false, &pref, false, false, 0);
821 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
822 }
823
824 #[test]
825 fn rejects_nan_pref() {
826 let pref = vec![vec![f64::NAN, 0.1], vec![0.1, 0.1]];
827 let res = preference_game(10, 2, None, false, &pref, false, false, 0);
828 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
829 }
830
831 #[test]
832 fn rejects_pref_above_one() {
833 let pref = vec![vec![0.5, 1.5], vec![1.5, 0.5]];
834 let res = preference_game(10, 2, None, false, &pref, false, false, 0);
835 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
836 }
837
838 #[test]
839 fn rejects_negative_pref() {
840 let pref = vec![vec![0.5, -0.1], vec![-0.1, 0.5]];
841 let res = preference_game(10, 2, None, false, &pref, false, false, 0);
842 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
843 }
844
845 #[test]
846 fn rejects_asymmetric_pref_when_undirected() {
847 let pref = vec![vec![0.1, 0.2], vec![0.3, 0.1]];
848 let res = preference_game(10, 2, None, false, &pref, false, false, 0);
849 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
850 }
851
852 #[test]
853 fn accepts_asymmetric_pref_when_directed() {
854 let pref = vec![vec![0.1, 0.2], vec![0.3, 0.1]];
855 let g = preference_game(20, 2, None, false, &pref, true, false, 7).unwrap();
856 assert_eq!(g.0.vcount(), 20);
857 assert!(g.0.is_directed());
858 }
859
860 #[test]
861 fn rejects_nan_type_dist() {
862 let pref = uniform_pref_sym(2, 0.1);
863 let td = vec![1.0, f64::NAN];
864 let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
865 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
866 }
867
868 #[test]
869 fn rejects_negative_type_dist() {
870 let pref = uniform_pref_sym(2, 0.1);
871 let td = vec![1.0, -0.5];
872 let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
873 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
874 }
875
876 #[test]
877 fn rejects_type_dist_length_mismatch() {
878 let pref = uniform_pref_sym(2, 0.1);
879 let td = vec![1.0, 1.0, 1.0];
880 let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
881 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
882 }
883
884 #[test]
885 fn rejects_zero_total_mass() {
886 let pref = uniform_pref_sym(2, 0.1);
887 let td = vec![0.0, 0.0];
888 let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
889 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
890 }
891
892 #[test]
893 fn fixed_sizes_rejects_wrong_sum() {
894 let pref = uniform_pref_sym(2, 0.1);
895 let td = vec![3.0, 3.0]; let res = preference_game(10, 2, Some(&td), true, &pref, false, false, 0);
897 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
898 }
899
900 #[test]
901 fn fixed_sizes_rejects_non_integer_counts() {
902 let pref = uniform_pref_sym(2, 0.1);
903 let td = vec![5.5, 4.5];
904 let res = preference_game(10, 2, Some(&td), true, &pref, false, false, 0);
905 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
906 }
907
908 #[test]
911 fn empty_pref_yields_no_edges() {
912 let pref = uniform_pref_sym(3, 0.0);
913 let (g, types) = preference_game(15, 3, None, false, &pref, false, false, 42).unwrap();
914 assert_eq!(g.vcount(), 15);
915 assert_eq!(g.ecount(), 0);
916 assert!(types.iter().all(|&t| t < 3));
917 }
918
919 #[test]
920 fn no_self_loops_when_loops_false_undirected() {
921 let pref = uniform_pref_sym(3, 0.5);
922 let (g, _types) = preference_game(20, 3, None, false, &pref, false, false, 7).unwrap();
923 assert!(!g.is_directed());
924 for e in 0..g.ecount() as u32 {
925 let (u, v) = g.edge(e).unwrap();
926 assert_ne!(u, v);
927 }
928 }
929
930 #[test]
931 fn no_self_loops_when_loops_false_directed() {
932 let pref = uniform_pref_sym(3, 0.5);
933 let (g, _types) = preference_game(20, 3, None, false, &pref, true, false, 7).unwrap();
934 assert!(g.is_directed());
935 for e in 0..g.ecount() as u32 {
936 let (u, v) = g.edge(e).unwrap();
937 assert_ne!(u, v);
938 }
939 }
940
941 #[test]
942 fn loops_allowed_when_loops_true() {
943 let pref = diag_pref_sym(2, 1.0);
946 let (g, _types) = preference_game(8, 2, None, false, &pref, false, true, 1).unwrap();
947 let mut found_loop = false;
948 for e in 0..g.ecount() as u32 {
949 let (u, v) = g.edge(e).unwrap();
950 if u == v {
951 found_loop = true;
952 break;
953 }
954 }
955 assert!(
956 found_loop,
957 "expected at least one self-loop with p=1, loops=true"
958 );
959 }
960
961 #[test]
962 fn fixed_sizes_equal_split_no_dist() {
963 let pref = uniform_pref_sym(3, 0.0);
964 let (g, types) = preference_game(9, 3, None, true, &pref, false, false, 0).unwrap();
965 assert_eq!(g.vcount(), 9);
966 let mut counts = [0u32; 3];
968 for &t in &types {
969 counts[t as usize] += 1;
970 }
971 assert_eq!(counts, [3, 3, 3]);
972 }
973
974 #[test]
975 fn fixed_sizes_explicit_distribution() {
976 let pref = uniform_pref_sym(3, 0.0);
977 let td = vec![5.0, 3.0, 2.0];
978 let (g, types) = preference_game(10, 3, Some(&td), true, &pref, false, false, 0).unwrap();
979 assert_eq!(g.vcount(), 10);
980 let mut counts = [0u32; 3];
981 for &t in &types {
982 counts[t as usize] += 1;
983 }
984 assert_eq!(counts, [5, 3, 2]);
985 }
986
987 #[test]
988 fn deterministic_same_seed() {
989 let pref = uniform_pref_sym(3, 0.3);
990 let (g1, t1) = preference_game(20, 3, None, false, &pref, false, false, 12345).unwrap();
991 let (g2, t2) = preference_game(20, 3, None, false, &pref, false, false, 12345).unwrap();
992 assert_eq!(g1.ecount(), g2.ecount());
993 assert_eq!(t1, t2);
994 let edges1: Vec<_> = (0..g1.ecount() as u32)
995 .map(|e| g1.edge(e).unwrap())
996 .collect();
997 let edges2: Vec<_> = (0..g2.ecount() as u32)
998 .map(|e| g2.edge(e).unwrap())
999 .collect();
1000 assert_eq!(edges1, edges2);
1001 }
1002
1003 #[test]
1004 fn diag_pref_keeps_edges_within_type() {
1005 let pref = diag_pref_sym(3, 0.5);
1006 let (g, types) = preference_game(30, 3, None, false, &pref, false, false, 42).unwrap();
1007 for e in 0..g.ecount() as u32 {
1008 let (u, v) = g.edge(e).unwrap();
1009 assert_eq!(types[u as usize], types[v as usize]);
1010 }
1011 }
1012
1013 #[test]
1014 fn type_dist_skews_assignment() {
1015 let pref = uniform_pref_sym(2, 0.0);
1017 let td = vec![100.0, 1.0];
1018 let (_g, types) =
1019 preference_game(50, 2, Some(&td), false, &pref, false, false, 99).unwrap();
1020 let count0 = types.iter().filter(|&&t| t == 0).count();
1021 let count1 = types.iter().filter(|&&t| t == 1).count();
1022 assert!(
1023 count0 > count1,
1024 "expected type 0 to dominate, got {count0} vs {count1}"
1025 );
1026 }
1027
1028 #[test]
1031 fn asym_rejects_zero_out_types() {
1032 let pref: Vec<Vec<f64>> = vec![];
1033 let res = asymmetric_preference_game(10, 0, 2, None, &pref, false, 0);
1034 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1035 }
1036
1037 #[test]
1038 fn asym_rejects_zero_in_types() {
1039 let pref = vec![vec![]];
1040 let res = asymmetric_preference_game(10, 1, 0, None, &pref, false, 0);
1041 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1042 }
1043
1044 #[test]
1045 fn asym_rejects_pref_wrong_dim() {
1046 let pref = vec![vec![0.1, 0.1]];
1047 let res = asymmetric_preference_game(10, 2, 2, None, &pref, false, 0);
1048 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1049 }
1050
1051 #[test]
1052 fn asym_rejects_nan_pref() {
1053 let pref = vec![vec![0.1, f64::NAN], vec![0.1, 0.1]];
1054 let res = asymmetric_preference_game(10, 2, 2, None, &pref, false, 0);
1055 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1056 }
1057
1058 #[test]
1059 fn asym_rejects_pref_out_of_range() {
1060 let pref = vec![vec![0.1, 1.5], vec![0.1, 0.1]];
1061 let res = asymmetric_preference_game(10, 2, 2, None, &pref, false, 0);
1062 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1063 }
1064
1065 #[test]
1066 fn asym_rejects_tdm_wrong_dim() {
1067 let pref = vec![vec![0.1, 0.1], vec![0.1, 0.1]];
1068 let tdm = vec![vec![1.0, 1.0]]; let res = asymmetric_preference_game(10, 2, 2, Some(&tdm), &pref, false, 0);
1070 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1071 }
1072
1073 #[test]
1074 fn asym_rejects_negative_tdm() {
1075 let pref = vec![vec![0.1, 0.1], vec![0.1, 0.1]];
1076 let tdm = vec![vec![1.0, -1.0], vec![1.0, 1.0]];
1077 let res = asymmetric_preference_game(10, 2, 2, Some(&tdm), &pref, false, 0);
1078 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1079 }
1080
1081 #[test]
1082 fn asym_rejects_zero_total_mass() {
1083 let pref = vec![vec![0.1, 0.1], vec![0.1, 0.1]];
1084 let tdm = vec![vec![0.0, 0.0], vec![0.0, 0.0]];
1085 let res = asymmetric_preference_game(10, 2, 2, Some(&tdm), &pref, false, 0);
1086 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1087 }
1088
1089 #[test]
1092 fn asym_empty_pref_yields_no_edges() {
1093 let pref = vec![vec![0.0, 0.0], vec![0.0, 0.0]];
1094 let (g, out, inp) = asymmetric_preference_game(15, 2, 2, None, &pref, false, 42).unwrap();
1095 assert_eq!(g.vcount(), 15);
1096 assert_eq!(g.ecount(), 0);
1097 assert!(g.is_directed());
1098 assert!(out.iter().all(|&t| t < 2));
1099 assert!(inp.iter().all(|&t| t < 2));
1100 }
1101
1102 #[test]
1103 fn asym_always_directed() {
1104 let pref = vec![vec![0.3, 0.1], vec![0.1, 0.3]];
1105 let (g, _, _) = asymmetric_preference_game(20, 2, 2, None, &pref, false, 1).unwrap();
1106 assert!(g.is_directed());
1107 }
1108
1109 #[test]
1110 fn asym_no_self_loops_when_loops_false() {
1111 let pref = vec![vec![0.7, 0.3], vec![0.3, 0.7]];
1112 let (g, _out, _inp) = asymmetric_preference_game(30, 2, 2, None, &pref, false, 11).unwrap();
1113 for e in 0..g.ecount() as u32 {
1114 let (u, v) = g.edge(e).unwrap();
1115 assert_ne!(u, v);
1116 }
1117 }
1118
1119 #[test]
1120 fn asym_loops_allowed_when_loops_true() {
1121 let pref = vec![vec![1.0]];
1124 let (g, _out, _inp) = asymmetric_preference_game(5, 1, 1, None, &pref, true, 0).unwrap();
1125 assert_eq!(g.ecount(), 25);
1127 let mut found_loop = false;
1128 for e in 0..g.ecount() as u32 {
1129 let (u, v) = g.edge(e).unwrap();
1130 if u == v {
1131 found_loop = true;
1132 break;
1133 }
1134 }
1135 assert!(found_loop);
1136 }
1137
1138 #[test]
1139 fn asym_loops_false_full_pref_yields_no_loops_full_off_diag() {
1140 let pref = vec![vec![1.0]];
1141 let (g, _out, _inp) = asymmetric_preference_game(5, 1, 1, None, &pref, false, 0).unwrap();
1142 assert_eq!(g.ecount(), 20);
1144 for e in 0..g.ecount() as u32 {
1145 let (u, v) = g.edge(e).unwrap();
1146 assert_ne!(u, v);
1147 }
1148 }
1149
1150 #[test]
1151 fn asym_deterministic_same_seed() {
1152 let pref = vec![vec![0.3, 0.1], vec![0.1, 0.3]];
1153 let (g1, o1, i1) = asymmetric_preference_game(20, 2, 2, None, &pref, false, 9999).unwrap();
1154 let (g2, o2, i2) = asymmetric_preference_game(20, 2, 2, None, &pref, false, 9999).unwrap();
1155 assert_eq!(g1.ecount(), g2.ecount());
1156 assert_eq!(o1, o2);
1157 assert_eq!(i1, i2);
1158 let e1: Vec<_> = (0..g1.ecount() as u32)
1159 .map(|e| g1.edge(e).unwrap())
1160 .collect();
1161 let e2: Vec<_> = (0..g2.ecount() as u32)
1162 .map(|e| g2.edge(e).unwrap())
1163 .collect();
1164 assert_eq!(e1, e2);
1165 }
1166
1167 #[test]
1170 fn cumdist_lookup_handles_endpoints() {
1171 let cd = vec![0.0, 1.0, 3.0, 6.0];
1172 assert_eq!(cumdist_lookup(&cd, 0.0), 1);
1173 assert_eq!(cumdist_lookup(&cd, 0.5), 1);
1174 assert_eq!(cumdist_lookup(&cd, 1.0), 2);
1175 assert_eq!(cumdist_lookup(&cd, 2.99), 2);
1176 assert_eq!(cumdist_lookup(&cd, 3.0), 3);
1177 assert_eq!(cumdist_lookup(&cd, 5.5), 3);
1178 }
1179
1180 #[test]
1181 fn sorted_intersection_basic() {
1182 assert_eq!(
1183 sorted_intersection(&[1, 3, 5, 7], &[2, 3, 5, 9]),
1184 vec![3, 5]
1185 );
1186 assert_eq!(sorted_intersection(&[], &[1, 2]), Vec::<u32>::new());
1187 assert_eq!(sorted_intersection(&[1, 2, 3], &[4, 5]), Vec::<u32>::new());
1188 assert_eq!(sorted_intersection(&[1, 2, 3], &[1, 2, 3]), vec![1, 2, 3]);
1189 }
1190}
1191
1192#[cfg(all(test, feature = "proptest-harness"))]
1193mod proptest_invariants {
1194 use super::*;
1195 use proptest::prelude::*;
1196
1197 fn uniform_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
1198 vec![vec![p; k]; k]
1199 }
1200
1201 fn diag_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
1202 let mut m = vec![vec![0.0; k]; k];
1203 for (i, row) in m.iter_mut().enumerate() {
1204 row[i] = p;
1205 }
1206 m
1207 }
1208
1209 proptest! {
1210 #![proptest_config(ProptestConfig::with_cases(48))]
1211
1212 #[test]
1213 fn vcount_equals_nodes(
1214 n in 1u32..40,
1215 k in 1u32..5,
1216 seed: u64,
1217 directed: bool,
1218 loops: bool,
1219 ) {
1220 let pref = uniform_pref_sym(k as usize, 0.2);
1221 let (g, types) = preference_game(n, k, None, false, &pref, directed, loops, seed).unwrap();
1222 prop_assert_eq!(g.vcount(), n);
1223 prop_assert_eq!(types.len(), n as usize);
1224 for &t in &types {
1225 prop_assert!(t < k);
1226 }
1227 }
1228
1229 #[test]
1230 fn no_self_loops_when_loops_false(
1231 n in 1u32..30,
1232 k in 1u32..4,
1233 seed: u64,
1234 directed: bool,
1235 ) {
1236 let pref = uniform_pref_sym(k as usize, 0.4);
1237 let (g, _types) = preference_game(n, k, None, false, &pref, directed, false, seed).unwrap();
1238 for e in 0..g.ecount() as u32 {
1239 let (u, v) = g.edge(e).unwrap();
1240 prop_assert_ne!(u, v);
1241 }
1242 }
1243
1244 #[test]
1245 fn diag_pref_keeps_edges_within_type(
1246 n in 4u32..30,
1247 k in 2u32..4,
1248 seed: u64,
1249 ) {
1250 let pref = diag_pref_sym(k as usize, 0.5);
1251 let (g, types) = preference_game(n, k, None, false, &pref, false, false, seed).unwrap();
1252 for e in 0..g.ecount() as u32 {
1253 let (u, v) = g.edge(e).unwrap();
1254 prop_assert_eq!(types[u as usize], types[v as usize]);
1255 }
1256 }
1257
1258 #[test]
1259 fn fixed_sizes_equal_split_counts_match(
1260 size_one in 1u32..6,
1261 k in 1u32..5,
1262 seed: u64,
1263 ) {
1264 let pref = uniform_pref_sym(k as usize, 0.0);
1265 let n = size_one * k;
1266 let (_g, types) = preference_game(n, k, None, true, &pref, false, false, seed).unwrap();
1267 let mut counts = vec![0u32; k as usize];
1268 for &t in &types {
1269 counts[t as usize] += 1;
1270 }
1271 for &c in &counts {
1272 prop_assert_eq!(c, size_one);
1273 }
1274 }
1275
1276 #[test]
1277 fn determinism_seed(
1278 n in 1u32..25,
1279 k in 1u32..4,
1280 seed: u64,
1281 ) {
1282 let pref = uniform_pref_sym(k as usize, 0.3);
1283 let (g1, t1) = preference_game(n, k, None, false, &pref, false, false, seed).unwrap();
1284 let (g2, t2) = preference_game(n, k, None, false, &pref, false, false, seed).unwrap();
1285 prop_assert_eq!(g1.ecount(), g2.ecount());
1286 prop_assert_eq!(t1, t2);
1287 }
1288
1289 #[test]
1290 fn asym_vcount_matches(
1291 n in 1u32..30,
1292 no_out in 1u32..4,
1293 no_in in 1u32..4,
1294 seed: u64,
1295 loops: bool,
1296 ) {
1297 let pref = vec![vec![0.2; no_in as usize]; no_out as usize];
1298 let (g, out, inp) =
1299 asymmetric_preference_game(n, no_out, no_in, None, &pref, loops, seed).unwrap();
1300 prop_assert_eq!(g.vcount(), n);
1301 prop_assert!(g.is_directed());
1302 prop_assert_eq!(out.len(), n as usize);
1303 prop_assert_eq!(inp.len(), n as usize);
1304 for &t in &out {
1305 prop_assert!(t < no_out);
1306 }
1307 for &t in &inp {
1308 prop_assert!(t < no_in);
1309 }
1310 }
1311
1312 #[test]
1313 fn asym_no_self_loops_when_loops_false(
1314 n in 1u32..30,
1315 no_out in 1u32..3,
1316 no_in in 1u32..3,
1317 seed: u64,
1318 ) {
1319 let pref = vec![vec![0.5; no_in as usize]; no_out as usize];
1320 let (g, _out, _inp) =
1321 asymmetric_preference_game(n, no_out, no_in, None, &pref, false, seed).unwrap();
1322 for e in 0..g.ecount() as u32 {
1323 let (u, v) = g.edge(e).unwrap();
1324 prop_assert_ne!(u, v);
1325 }
1326 }
1327
1328 #[test]
1329 fn asym_determinism_seed(
1330 n in 1u32..25,
1331 no_out in 1u32..3,
1332 no_in in 1u32..3,
1333 seed: u64,
1334 ) {
1335 let pref = vec![vec![0.3; no_in as usize]; no_out as usize];
1336 let (g1, o1, i1) =
1337 asymmetric_preference_game(n, no_out, no_in, None, &pref, false, seed).unwrap();
1338 let (g2, o2, i2) =
1339 asymmetric_preference_game(n, no_out, no_in, None, &pref, false, seed).unwrap();
1340 prop_assert_eq!(g1.ecount(), g2.ecount());
1341 prop_assert_eq!(o1, o2);
1342 prop_assert_eq!(i1, i2);
1343 }
1344 }
1345}