1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
53
54pub fn triangular_lattice(dims: &[u32], directed: bool, mutual: bool) -> IgraphResult<Graph> {
90 if !matches!(dims.len(), 1..=3) {
91 return Err(IgraphError::InvalidArgument(format!(
92 "triangular_lattice: size of dimension vector must be 1, 2 or 3, got {}",
93 dims.len()
94 )));
95 }
96
97 if dims.contains(&0) {
99 return Graph::new(0, directed);
100 }
101
102 let (row_lengths, row_start) = match dims.len() {
103 1 => triangle_shape(dims[0]),
104 2 => rectangle_shape(dims[0], dims[1]),
105 3 => hex_shape(dims[0], dims[1], dims[2]),
106 _ => unreachable!("dims length already checked"),
107 };
108
109 layout(&row_lengths, &row_start, directed, mutual)
110}
111
112fn triangle_shape(n: u32) -> (Vec<u32>, Vec<u32>) {
117 let mut row_lengths = Vec::with_capacity(n as usize);
118 let mut row_start = Vec::with_capacity(n as usize);
119 for j in 0..n {
120 row_lengths.push(n - j);
121 row_start.push(0);
122 }
123 (row_lengths, row_start)
124}
125
126fn rectangle_shape(size_x: u32, size_y: u32) -> (Vec<u32>, Vec<u32>) {
132 let mut row_lengths = Vec::with_capacity(size_x as usize);
133 let mut row_start = Vec::with_capacity(size_x as usize);
134 for j in 0..size_x {
135 row_lengths.push(size_y);
136 row_start.push((size_x - j) / 2);
137 }
138 (row_lengths, row_start)
139}
140
141fn hex_shape(size_x: u32, size_y: u32, size_z: u32) -> (Vec<u32>, Vec<u32>) {
156 let row_count = size_y + size_z - 1;
158
159 let mut row_length: u32 = size_x;
160 let mut row_start_val: u32 = size_y - 1;
161 let first_threshold: u32 = size_y.min(size_z) - 1;
162 let second_threshold: u32 = size_y.max(size_z) - 1;
163 let shrink_in_middle: bool = size_y >= size_z;
164
165 let mut row_lengths = Vec::with_capacity(row_count as usize);
166 let mut row_start = Vec::with_capacity(row_count as usize);
167
168 for i in 0..row_count {
169 row_lengths.push(row_length);
170 row_start.push(row_start_val);
171
172 if i < first_threshold {
173 row_length += 1;
174 row_start_val -= 1;
175 } else if i < second_threshold {
176 if shrink_in_middle {
177 row_start_val -= 1;
178 }
179 } else {
180 row_length -= 1;
181 }
182 }
183
184 (row_lengths, row_start)
185}
186
187fn layout(
196 row_lengths: &[u32],
197 row_start: &[u32],
198 directed: bool,
199 mutual: bool,
200) -> IgraphResult<Graph> {
201 debug_assert_eq!(row_lengths.len(), row_start.len());
202 let row_count = row_lengths.len();
203
204 let mut prefix_sum: Vec<u32> = Vec::with_capacity(row_count + 1);
206 prefix_sum.push(0);
207 for &len in row_lengths {
208 let last = *prefix_sum.last().expect("non-empty");
209 let next = last
210 .checked_add(len)
211 .ok_or_else(|| overflow_error("vertex count"))?;
212 prefix_sum.push(next);
213 }
214 let vcount = *prefix_sum.last().expect("non-empty");
215
216 let vertex_index = |i: u32, j: usize| -> u32 { prefix_sum[j] + i - row_start[j] };
218 let row_end = |j: usize| -> u32 {
219 row_start[j] + row_lengths[j] - 1
222 };
223
224 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
225
226 let add_if_in_bounds =
231 |edges: &mut Vec<(VertexId, VertexId)>, i: u32, j: usize, k_opt: Option<u32>, l: usize| {
232 let Some(k) = k_opt else {
233 return;
234 };
235 if l >= row_count {
236 return;
237 }
238 let l_start = row_start[l];
239 let l_end = row_end(l);
240 if k < l_start || k > l_end {
241 return;
242 }
243 let src = vertex_index(i, j);
244 let dst = vertex_index(k, l);
245 edges.push((src, dst));
246 if directed && mutual {
247 edges.push((dst, src));
248 }
249 };
250
251 for j in 0..row_count {
252 let row_len = row_lengths[j];
253 let start = row_start[j];
254 for i in 0..row_len {
255 let k = start + i;
256 add_if_in_bounds(&mut edges, k, j, Some(k + 1), j);
258 if j + 1 < row_count {
259 add_if_in_bounds(&mut edges, k, j, Some(k), j + 1);
261 add_if_in_bounds(&mut edges, k, j, k.checked_sub(1), j + 1);
263 }
264 }
265 }
266
267 let mut g = Graph::new(vcount, directed)?;
268 g.add_edges(edges)?;
269 Ok(g)
270}
271
272fn overflow_error(kind: &str) -> IgraphError {
273 IgraphError::InvalidArgument(format!("triangular_lattice: {kind} overflows u32"))
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use std::collections::BTreeSet;
280
281 fn canonical_undirected(g: &Graph) -> BTreeSet<(u32, u32)> {
282 let mut s = BTreeSet::new();
283 for v in 0..g.vcount() {
284 for &u in &g.neighbors(v).expect("neighbors") {
285 let key = if v <= u { (v, u) } else { (u, v) };
286 s.insert(key);
287 }
288 }
289 s
290 }
291
292 fn directed_arcs(g: &Graph) -> BTreeSet<(u32, u32)> {
293 (0..u32::try_from(g.ecount()).expect("ecount fits u32"))
294 .map(|eid| g.edge(eid).expect("edge"))
295 .collect()
296 }
297
298 #[test]
299 fn empty_dims_rejected() {
300 let r = triangular_lattice(&[], false, false);
301 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
302 }
303
304 #[test]
305 fn four_dim_rejected() {
306 let r = triangular_lattice(&[1, 2, 3, 4], false, false);
307 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
308 }
309
310 #[test]
311 fn zero_dim_yields_empty_graph() {
312 let g = triangular_lattice(&[3, 0], false, false).expect("ok");
313 assert_eq!(g.vcount(), 0);
314 assert_eq!(g.ecount(), 0);
315 }
316
317 #[test]
318 fn zero_dim_directed_keeps_flag() {
319 let g = triangular_lattice(&[0, 3, 4], true, true).expect("ok");
320 assert_eq!(g.vcount(), 0);
321 assert!(g.is_directed());
322 }
323
324 #[test]
325 fn triangle_single_vertex() {
326 let g = triangular_lattice(&[1], true, false).expect("ok");
327 assert_eq!(g.vcount(), 1);
328 assert_eq!(g.ecount(), 0);
329 assert!(g.is_directed());
330 }
331
332 #[test]
333 fn triangle_side_five_matches_upstream_canon() {
334 let g = triangular_lattice(&[5], true, false).expect("ok");
337 assert_eq!(g.vcount(), 15);
338 assert_eq!(g.ecount(), 30);
339 let want: BTreeSet<(u32, u32)> = [
340 (0, 1),
341 (0, 5),
342 (1, 2),
343 (1, 5),
344 (1, 6),
345 (2, 3),
346 (2, 6),
347 (2, 7),
348 (3, 4),
349 (3, 7),
350 (3, 8),
351 (4, 8),
352 (5, 6),
353 (5, 9),
354 (6, 7),
355 (6, 9),
356 (6, 10),
357 (7, 8),
358 (7, 10),
359 (7, 11),
360 (8, 11),
361 (9, 10),
362 (9, 12),
363 (10, 11),
364 (10, 12),
365 (10, 13),
366 (11, 13),
367 (12, 13),
368 (12, 14),
369 (13, 14),
370 ]
371 .into_iter()
372 .collect();
373 assert_eq!(directed_arcs(&g), want);
374 }
375
376 #[test]
377 fn rectangle_two_by_two_matches_python_igraph_undirected() {
378 let g = triangular_lattice(&[2, 2], false, false).expect("ok");
381 let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)]
382 .into_iter()
383 .collect();
384 assert_eq!(canonical_undirected(&g), want);
385 }
386
387 #[test]
388 fn rectangle_two_by_two_directed_mutual_doubles_edges() {
389 let g = triangular_lattice(&[2, 2], true, true).expect("ok");
391 let want: BTreeSet<(u32, u32)> = [
392 (0, 1),
393 (0, 2),
394 (0, 3),
395 (1, 0),
396 (1, 3),
397 (2, 0),
398 (2, 3),
399 (3, 0),
400 (3, 1),
401 (3, 2),
402 ]
403 .into_iter()
404 .collect();
405 assert_eq!(directed_arcs(&g), want);
406 assert!(g.is_directed());
407 }
408
409 #[test]
410 fn rectangle_two_by_two_directed_unilateral_matches_undirected_set() {
411 let g = triangular_lattice(&[2, 2], true, false).expect("ok");
414 let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)]
415 .into_iter()
416 .collect();
417 assert_eq!(directed_arcs(&g), want);
418 }
419
420 #[test]
421 fn rectangle_four_by_five_matches_upstream_vcount() {
422 let g = triangular_lattice(&[4, 5], true, true).expect("ok");
425 assert_eq!(g.vcount(), 20);
426 assert_eq!(g.ecount(), 86);
427 }
428
429 #[test]
430 fn hexagonal_3_4_5_matches_upstream_vcount() {
431 let g = triangular_lattice(&[3, 4, 5], false, true).expect("ok");
434 assert_eq!(g.vcount(), 36);
435 assert_eq!(g.ecount(), 87);
436 }
437
438 #[test]
439 fn triangle_three_full_topology() {
440 let g = triangular_lattice(&[3], false, false).expect("ok");
447 assert_eq!(g.vcount(), 6);
448 assert_eq!(g.ecount(), 9);
449 let want: BTreeSet<(u32, u32)> = [
450 (0, 1),
451 (0, 3),
452 (1, 2),
453 (1, 3),
454 (1, 4),
455 (2, 4),
456 (3, 4),
457 (3, 5),
458 (4, 5),
459 ]
460 .into_iter()
461 .collect();
462 assert_eq!(canonical_undirected(&g), want);
463 }
464}
465
466#[cfg(all(test, feature = "proptest-harness"))]
467mod proptests {
468 use super::*;
469 use proptest::prelude::*;
470
471 proptest! {
472 #[test]
473 fn triangle_vcount_is_triangular_number(
474 n in 1u32..30,
475 ) {
476 let g = triangular_lattice(&[n], false, false).expect("ok");
477 let expected = (u64::from(n) * (u64::from(n) + 1)) / 2;
478 prop_assert_eq!(u64::from(g.vcount()), expected);
479 }
480
481 #[test]
482 fn rectangle_vcount_is_product(
483 sx in 1u32..15,
484 sy in 1u32..15,
485 ) {
486 let g = triangular_lattice(&[sx, sy], false, false).expect("ok");
487 prop_assert_eq!(u64::from(g.vcount()), u64::from(sx) * u64::from(sy));
488 }
489
490 #[test]
491 fn directed_mutual_doubles_undirected_ecount(
492 sx in 1u32..10,
493 sy in 1u32..10,
494 ) {
495 let undirected = triangular_lattice(&[sx, sy], false, false).expect("ok");
496 let mutual = triangular_lattice(&[sx, sy], true, true).expect("ok");
497 prop_assert_eq!(mutual.ecount(), undirected.ecount() * 2);
498 }
499
500 #[test]
501 fn directed_unilateral_matches_undirected_ecount(
502 sx in 1u32..10,
503 sy in 1u32..10,
504 ) {
505 let undirected = triangular_lattice(&[sx, sy], false, false).expect("ok");
506 let unilateral = triangular_lattice(&[sx, sy], true, false).expect("ok");
507 prop_assert_eq!(unilateral.ecount(), undirected.ecount());
508 }
509
510 #[test]
511 fn directedness_flag_round_trips(
512 sx in 1u32..8,
513 sy in 1u32..8,
514 directed: bool,
515 mutual: bool,
516 ) {
517 let g = triangular_lattice(&[sx, sy], directed, mutual).expect("ok");
518 prop_assert_eq!(g.is_directed(), directed);
519 }
520
521 #[test]
522 fn max_degree_at_most_six(
523 sx in 1u32..8,
524 sy in 1u32..8,
525 ) {
526 let g = triangular_lattice(&[sx, sy], false, false).expect("ok");
527 for v in 0..g.vcount() {
528 prop_assert!(g.degree(v).unwrap() <= 6);
529 }
530 }
531 }
532}