1#![allow(
38 unknown_lints,
39 clippy::cast_possible_truncation,
40 clippy::cast_sign_loss,
41 clippy::cast_precision_loss,
42 clippy::manual_midpoint
43)]
44
45use std::collections::HashSet;
46
47use crate::core::rng::SplitMix64;
48use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
49
50fn validate(islands_n: u32, islands_size: u32, islands_pin: f64, n_inter: u32) -> IgraphResult<()> {
51 if !islands_pin.is_finite() || !(0.0..=1.0).contains(&islands_pin) {
52 return Err(IgraphError::InvalidArgument(format!(
53 "simple_interconnected_islands_game: islands_pin must satisfy 0 <= islands_pin <= 1 (got {islands_pin})"
54 )));
55 }
56 let size_sq = u64::from(islands_size) * u64::from(islands_size);
57 if u64::from(n_inter) > size_sq {
58 return Err(IgraphError::InvalidArgument(format!(
59 "simple_interconnected_islands_game: n_inter ({n_inter}) exceeds islands_size^2 ({size_sq})"
60 )));
61 }
62 let total_nodes = u64::from(islands_n) * u64::from(islands_size);
63 if total_nodes > u64::from(u32::MAX) {
64 return Err(IgraphError::InvalidArgument(format!(
65 "simple_interconnected_islands_game: islands_n * islands_size ({total_nodes}) overflows u32"
66 )));
67 }
68 Ok(())
69}
70
71fn decode_triangle_pair(idx: u64) -> (u32, u32) {
76 let x = idx as f64;
77 let to = ((8.0 * x + 1.0).sqrt() + 1.0) / 2.0;
78 let to = to.floor() as u64;
79 let from = idx - (to * (to - 1)) / 2;
80 (from as u32, to as u32)
81}
82
83fn distinct_sample(rng: &mut SplitMix64, n_pairs: u64, m: u64) -> Vec<u64> {
87 debug_assert!(m <= n_pairs);
88 let cap = usize::try_from(m).unwrap_or(usize::MAX);
89 let mut chosen: HashSet<u64> = HashSet::with_capacity(cap);
90 let mut out: Vec<u64> = Vec::with_capacity(cap);
91 for j in (n_pairs - m)..n_pairs {
92 let span = j + 1;
93 let span_usize = usize::try_from(span).unwrap_or(usize::MAX);
94 let t = rng.gen_index(span_usize) as u64;
95 let pick = if chosen.insert(t) {
96 t
97 } else {
98 chosen.insert(j);
99 j
100 };
101 out.push(pick);
102 }
103 out.sort_unstable();
104 out
105}
106
107pub fn simple_interconnected_islands_game(
144 islands_n: u32,
145 islands_size: u32,
146 islands_pin: f64,
147 n_inter: u32,
148 seed: u64,
149) -> IgraphResult<Graph> {
150 validate(islands_n, islands_size, islands_pin, n_inter)?;
151
152 let total_nodes = islands_n * islands_size;
153 if total_nodes == 0 {
154 return Graph::new(total_nodes, false);
155 }
156
157 let mut rng = SplitMix64::new(seed);
158 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
159
160 let intra_cap: u64 = if islands_size >= 2 {
161 u64::from(islands_size) * u64::from(islands_size - 1) / 2
162 } else {
163 0
164 };
165 let intra_cap_f = intra_cap as f64;
166
167 let bipartite_cap: u64 = u64::from(islands_size) * u64::from(islands_size);
168
169 for is in 0..islands_n {
170 let start = is * islands_size;
171
172 if islands_pin > 0.0 && intra_cap > 0 {
175 if islands_pin >= 1.0 {
176 for idx in 0..intra_cap {
178 let (from, to) = decode_triangle_pair(idx);
179 edges.push(((start + from) as VertexId, (start + to) as VertexId));
180 }
181 } else {
182 let mut last = rng.gen_geom(islands_pin);
183 while last < intra_cap_f {
184 let idx = last.trunc() as u64;
185 if idx >= intra_cap {
186 break;
187 }
188 let (from, to) = decode_triangle_pair(idx);
189 edges.push(((start + from) as VertexId, (start + to) as VertexId));
190 last += rng.gen_geom(islands_pin);
191 last += 1.0;
192 }
193 }
194 }
195
196 if n_inter > 0 && islands_size > 0 {
198 for ip in (is + 1)..islands_n {
199 let other_start = ip * islands_size;
200 let picks = distinct_sample(&mut rng, bipartite_cap, u64::from(n_inter));
201 for pick in picks {
202 let from = (pick / u64::from(islands_size)) as u32;
203 let to = (pick - u64::from(from) * u64::from(islands_size)) as u32;
204 edges.push(((start + from) as VertexId, (other_start + to) as VertexId));
205 }
206 }
207 }
208 }
209
210 let mut g = Graph::new(total_nodes, false)?;
211 g.add_edges(edges)?;
212 Ok(g)
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::collections::HashSet;
219
220 fn canonical_edges(g: &Graph) -> HashSet<(VertexId, VertexId)> {
221 let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 for tests");
222 (0..n_edges)
223 .map(|eid| {
224 let (a, b) = g.edge(eid).expect("edge id in bounds");
225 if a <= b { (a, b) } else { (b, a) }
226 })
227 .collect()
228 }
229
230 #[test]
231 fn empty_when_no_islands() {
232 let g = simple_interconnected_islands_game(0, 10, 0.5, 1, 1).unwrap();
233 assert_eq!(g.vcount(), 0);
234 assert_eq!(g.ecount(), 0);
235 }
236
237 #[test]
238 fn empty_when_island_size_zero() {
239 let g = simple_interconnected_islands_game(5, 0, 0.5, 0, 1).unwrap();
240 assert_eq!(g.vcount(), 0);
241 assert_eq!(g.ecount(), 0);
242 }
243
244 #[test]
245 fn single_island_zero_pin_zero_inter_is_edgeless() {
246 let g = simple_interconnected_islands_game(1, 20, 0.0, 0, 7).unwrap();
247 assert_eq!(g.vcount(), 20);
248 assert_eq!(g.ecount(), 0);
249 }
250
251 #[test]
252 fn single_island_pin_one_is_complete_clique() {
253 let g = simple_interconnected_islands_game(1, 10, 1.0, 0, 9).unwrap();
254 assert_eq!(g.vcount(), 10);
255 assert_eq!(g.ecount(), 45);
257 }
258
259 #[test]
260 fn multiple_islands_pin_zero_only_inter_island() {
261 let g = simple_interconnected_islands_game(4, 5, 0.0, 2, 3).unwrap();
263 assert_eq!(g.vcount(), 20);
264 assert_eq!(g.ecount(), 12);
265 }
266
267 #[test]
268 fn saturated_bipartite_slice() {
269 let g = simple_interconnected_islands_game(2, 3, 0.0, 9, 5).unwrap();
271 assert_eq!(g.vcount(), 6);
272 assert_eq!(g.ecount(), 9);
273 }
274
275 #[test]
276 fn n_inter_above_cap_errors() {
277 let res = simple_interconnected_islands_game(2, 3, 0.0, 10, 5);
278 assert!(res.is_err());
279 }
280
281 #[test]
282 fn invalid_pin_rejected() {
283 assert!(simple_interconnected_islands_game(2, 3, -0.1, 0, 5).is_err());
284 assert!(simple_interconnected_islands_game(2, 3, 1.1, 0, 5).is_err());
285 assert!(simple_interconnected_islands_game(2, 3, f64::NAN, 0, 5).is_err());
286 }
287
288 #[test]
289 fn output_is_undirected_and_simple() {
290 let g = simple_interconnected_islands_game(3, 8, 0.4, 2, 11).unwrap();
291 assert!(!g.is_directed());
292 let n_edges = u32::try_from(g.ecount()).unwrap();
293 let mut seen: HashSet<(VertexId, VertexId)> = HashSet::new();
294 for eid in 0..n_edges {
295 let (a, b) = g.edge(eid).unwrap();
296 assert_ne!(a, b, "self-loop edge {a}-{b}");
297 let pair = if a <= b { (a, b) } else { (b, a) };
298 assert!(seen.insert(pair), "multi-edge {pair:?}");
299 }
300 }
301
302 #[test]
303 fn intra_edges_stay_within_island_boundaries() {
304 let islands_n = 4u32;
308 let islands_size = 10u32;
309 let g = simple_interconnected_islands_game(islands_n, islands_size, 0.3, 5, 13).unwrap();
310 let n_edges = u32::try_from(g.ecount()).unwrap();
311 for eid in 0..n_edges {
312 let (a, b) = g.edge(eid).unwrap();
313 let island_a = a / islands_size;
314 let island_b = b / islands_size;
315 assert!(island_a < islands_n);
316 assert!(island_b < islands_n);
317 }
318 }
319
320 #[test]
321 fn determinism_same_seed_same_graph() {
322 let a = simple_interconnected_islands_game(4, 20, 0.35, 3, 0xC0DE).unwrap();
323 let b = simple_interconnected_islands_game(4, 20, 0.35, 3, 0xC0DE).unwrap();
324 assert_eq!(a.ecount(), b.ecount());
325 assert_eq!(canonical_edges(&a), canonical_edges(&b));
326 }
327
328 #[test]
329 fn distinct_seeds_differ() {
330 let a = simple_interconnected_islands_game(4, 20, 0.35, 3, 1).unwrap();
331 let b = simple_interconnected_islands_game(4, 20, 0.35, 3, 2).unwrap();
332 assert_ne!(canonical_edges(&a), canonical_edges(&b));
333 }
334
335 #[test]
336 fn ecount_band_matches_expectation() {
337 let islands_n = 5u32;
340 let islands_size = 30u32;
341 let p = 0.2;
342 let n_inter = 4u32;
343 let expected_intra =
344 p * f64::from(islands_n) * f64::from(islands_size) * f64::from(islands_size - 1) / 2.0;
345 let expected_inter = f64::from(n_inter * islands_n * (islands_n - 1) / 2);
346 let expected = expected_intra + expected_inter;
347
348 let g =
349 simple_interconnected_islands_game(islands_n, islands_size, p, n_inter, 4242).unwrap();
350 let m = g.ecount() as f64;
351 let lo = 0.6 * expected;
352 let hi = 1.4 * expected;
353 assert!(m >= lo && m <= hi, "ecount {m} outside band [{lo}, {hi}]");
354 }
355
356 #[test]
357 fn triangle_pair_decoder_round_trips() {
358 let expected: Vec<(u32, u32)> = vec![
361 (0, 1),
362 (0, 2),
363 (1, 2),
364 (0, 3),
365 (1, 3),
366 (2, 3),
367 (0, 4),
368 (1, 4),
369 (2, 4),
370 (3, 4),
371 (0, 5),
372 (1, 5),
373 (2, 5),
374 (3, 5),
375 (4, 5),
376 ];
377 for (idx, &want) in expected.iter().enumerate() {
378 assert_eq!(decode_triangle_pair(idx as u64), want);
379 }
380 }
381}