rust_igraph/algorithms/games/
growing_random.rs1#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
37
38use crate::core::rng::SplitMix64;
39use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
40
41fn validate_inputs(n: u32, m: u32) -> IgraphResult<()> {
42 let n_u64 = u64::from(n);
43 let m_u64 = u64::from(m);
44 let steps = n_u64.saturating_sub(1);
45 let edges = steps
46 .checked_mul(m_u64)
47 .ok_or(IgraphError::Internal("growing_random edge count overflow"))?;
48 if edges > u64::from(u32::MAX) {
54 return Err(IgraphError::Internal(
55 "growing_random edge count exceeds u32::MAX",
56 ));
57 }
58 Ok(())
59}
60
61pub fn growing_random_game(
93 n: u32,
94 m: u32,
95 directed: bool,
96 citation: bool,
97 seed: u64,
98) -> IgraphResult<Graph> {
99 validate_inputs(n, m)?;
100
101 if n == 0 {
102 return Graph::new(0, directed);
103 }
104 if n == 1 || m == 0 {
105 return Graph::new(n, directed);
106 }
107
108 let m_usize = m as usize;
109 let steps = (n as usize) - 1;
110 let total_edges = steps
111 .checked_mul(m_usize)
112 .ok_or(IgraphError::Internal("growing_random edge total overflow"))?;
113
114 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
115 let mut rng = SplitMix64::new(seed);
116
117 for i in 1..n {
118 for _ in 0..m {
119 let (from, to) = if citation {
120 let to = rng.gen_index(i as usize) as VertexId;
122 (i, to)
123 } else {
124 let from = rng.gen_index((i as usize) + 1) as VertexId;
126 let to = (rng.gen_index(i as usize) as VertexId) + 1;
127 (from, to)
128 };
129 edges.push((from, to));
130 }
131 }
132
133 let mut g = Graph::new(n, directed)?;
134 g.add_edges(edges)?;
135 Ok(g)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
143 let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
144 (0..n_edges)
145 .map(|eid| g.edge(eid).expect("edge id in bounds"))
146 .collect()
147 }
148
149 #[test]
150 fn n_zero_returns_empty_graph() {
151 let g = growing_random_game(0, 3, true, true, 1).unwrap();
152 assert_eq!(g.vcount(), 0);
153 assert_eq!(g.ecount(), 0);
154 }
155
156 #[test]
157 fn n_one_returns_singleton() {
158 let g = growing_random_game(1, 5, true, true, 1).unwrap();
159 assert_eq!(g.vcount(), 1);
160 assert_eq!(g.ecount(), 0);
161 }
162
163 #[test]
164 fn m_zero_returns_edgeless() {
165 let g = growing_random_game(20, 0, true, false, 1).unwrap();
166 assert_eq!(g.vcount(), 20);
167 assert_eq!(g.ecount(), 0);
168 }
169
170 #[test]
171 fn exact_edge_count_directed_constant_m() {
172 for &(n, m) in &[(10u32, 1u32), (10, 3), (50, 2), (200, 5)] {
173 let g = growing_random_game(n, m, true, true, 0x00C0_FFEE).unwrap();
174 assert_eq!(g.vcount(), n);
175 assert_eq!(g.ecount(), (n as usize - 1) * m as usize);
176 }
177 }
178
179 #[test]
180 fn exact_edge_count_undirected_free_mode() {
181 let g = growing_random_game(100, 4, false, false, 0xDEAD_BEEF).unwrap();
183 assert_eq!(g.vcount(), 100);
184 assert_eq!(g.ecount(), 99 * 4);
185 assert!(!g.is_directed());
186 }
187
188 #[test]
189 fn deterministic_with_seed() {
190 let a = growing_random_game(80, 3, true, true, 0xABCD).unwrap();
191 let b = growing_random_game(80, 3, true, true, 0xABCD).unwrap();
192 assert_eq!(a.vcount(), b.vcount());
193 assert_eq!(a.ecount(), b.ecount());
194 let ea: Vec<_> = collect_edges(&a);
195 let eb: Vec<_> = collect_edges(&b);
196 assert_eq!(ea, eb);
197 }
198
199 #[test]
200 fn different_seeds_yield_different_graphs() {
201 let a = growing_random_game(60, 2, true, false, 1).unwrap();
202 let b = growing_random_game(60, 2, true, false, 2).unwrap();
203 let ea: Vec<_> = collect_edges(&a);
204 let eb: Vec<_> = collect_edges(&b);
205 assert_ne!(ea, eb, "different seeds must produce different graphs");
206 }
207
208 #[test]
209 fn citation_mode_directed_has_strict_temporal_order() {
210 let g = growing_random_game(100, 3, true, true, 0xBEEF).unwrap();
212 for (src, dst) in collect_edges(&g) {
213 assert!(
214 dst < src,
215 "citation edge ({src} -> {dst}) violates temporal ordering"
216 );
217 }
218 }
219
220 #[test]
221 fn citation_mode_directed_no_self_loops() {
222 let g = growing_random_game(120, 4, true, true, 0xFACE).unwrap();
224 for (src, dst) in collect_edges(&g) {
225 assert_ne!(src, dst, "citation mode must not produce self-loops");
226 }
227 }
228
229 #[test]
230 fn citation_mode_first_vertex_never_source() {
231 let g = growing_random_game(75, 2, true, true, 0x42).unwrap();
233 for (src, _dst) in collect_edges(&g) {
234 assert_ne!(src, 0, "seed vertex must never be the source in citation");
235 }
236 }
237
238 #[test]
239 fn citation_mode_per_step_outdegree_equals_m() {
240 let n: u32 = 50;
242 let m: u32 = 3;
243 let g = growing_random_game(n, m, true, true, 0xC1A0).unwrap();
244 let mut per_src = vec![0u32; n as usize];
245 for (src, _dst) in collect_edges(&g) {
246 per_src[src as usize] += 1;
247 }
248 assert_eq!(per_src[0], 0);
250 for (i, &out) in per_src.iter().enumerate().skip(1) {
251 assert_eq!(out, m, "vertex {i} out-degree should be m={m}");
252 }
253 }
254
255 #[test]
256 fn free_mode_directed_endpoints_within_frontier() {
257 let g = growing_random_game(100, 3, true, false, 0xBABE).unwrap();
264 for (src, dst) in collect_edges(&g) {
265 assert!(
266 dst >= 1,
267 "free-mode sink should never be 0 (got edge ({src}, {dst}))"
268 );
269 }
270 }
271
272 #[test]
273 fn validate_huge_overflow_is_rejected_or_handled() {
274 let huge = u32::MAX;
278 let res = growing_random_game(huge, huge, true, true, 0);
279 let _ = res;
280 }
281}