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 Some(&last) = prefix_sum.last() else {
209 return Err(overflow_error("empty prefix sum"));
210 };
211 let next = last
212 .checked_add(len)
213 .ok_or_else(|| overflow_error("vertex count"))?;
214 prefix_sum.push(next);
215 }
216 let Some(&vcount) = prefix_sum.last() else {
217 return Err(overflow_error("empty prefix sum"));
218 };
219
220 let vertex_index = |i: u32, j: usize| -> u32 { prefix_sum[j] + i - row_start[j] };
222 let row_end = |j: usize| -> u32 {
223 row_start[j] + row_lengths[j] - 1
226 };
227
228 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
229
230 let add_if_in_bounds =
235 |edges: &mut Vec<(VertexId, VertexId)>, i: u32, j: usize, k_opt: Option<u32>, l: usize| {
236 let Some(k) = k_opt else {
237 return;
238 };
239 if l >= row_count {
240 return;
241 }
242 let l_start = row_start[l];
243 let l_end = row_end(l);
244 if k < l_start || k > l_end {
245 return;
246 }
247 let src = vertex_index(i, j);
248 let dst = vertex_index(k, l);
249 edges.push((src, dst));
250 if directed && mutual {
251 edges.push((dst, src));
252 }
253 };
254
255 for j in 0..row_count {
256 let row_len = row_lengths[j];
257 let start = row_start[j];
258 for i in 0..row_len {
259 let k = start + i;
260 add_if_in_bounds(&mut edges, k, j, Some(k + 1), j);
262 if j + 1 < row_count {
263 add_if_in_bounds(&mut edges, k, j, Some(k), j + 1);
265 add_if_in_bounds(&mut edges, k, j, k.checked_sub(1), j + 1);
267 }
268 }
269 }
270
271 let mut g = Graph::new(vcount, directed)?;
272 g.add_edges(edges)?;
273 Ok(g)
274}
275
276fn overflow_error(kind: &str) -> IgraphError {
277 IgraphError::InvalidArgument(format!("triangular_lattice: {kind} overflows u32"))
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use std::collections::BTreeSet;
284
285 fn canonical_undirected(g: &Graph) -> BTreeSet<(u32, u32)> {
286 let mut s = BTreeSet::new();
287 for v in 0..g.vcount() {
288 for &u in &g.neighbors(v).expect("neighbors") {
289 let key = if v <= u { (v, u) } else { (u, v) };
290 s.insert(key);
291 }
292 }
293 s
294 }
295
296 fn directed_arcs(g: &Graph) -> BTreeSet<(u32, u32)> {
297 (0..u32::try_from(g.ecount()).expect("ecount fits u32"))
298 .map(|eid| g.edge(eid).expect("edge"))
299 .collect()
300 }
301
302 #[test]
303 fn empty_dims_rejected() {
304 let r = triangular_lattice(&[], false, false);
305 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
306 }
307
308 #[test]
309 fn four_dim_rejected() {
310 let r = triangular_lattice(&[1, 2, 3, 4], false, false);
311 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
312 }
313
314 #[test]
315 fn zero_dim_yields_empty_graph() {
316 let g = triangular_lattice(&[3, 0], false, false).expect("ok");
317 assert_eq!(g.vcount(), 0);
318 assert_eq!(g.ecount(), 0);
319 }
320
321 #[test]
322 fn zero_dim_directed_keeps_flag() {
323 let g = triangular_lattice(&[0, 3, 4], true, true).expect("ok");
324 assert_eq!(g.vcount(), 0);
325 assert!(g.is_directed());
326 }
327
328 #[test]
329 fn triangle_single_vertex() {
330 let g = triangular_lattice(&[1], true, false).expect("ok");
331 assert_eq!(g.vcount(), 1);
332 assert_eq!(g.ecount(), 0);
333 assert!(g.is_directed());
334 }
335
336 #[test]
337 fn triangle_side_five_matches_upstream_canon() {
338 let g = triangular_lattice(&[5], true, false).expect("ok");
341 assert_eq!(g.vcount(), 15);
342 assert_eq!(g.ecount(), 30);
343 let want: BTreeSet<(u32, u32)> = [
344 (0, 1),
345 (0, 5),
346 (1, 2),
347 (1, 5),
348 (1, 6),
349 (2, 3),
350 (2, 6),
351 (2, 7),
352 (3, 4),
353 (3, 7),
354 (3, 8),
355 (4, 8),
356 (5, 6),
357 (5, 9),
358 (6, 7),
359 (6, 9),
360 (6, 10),
361 (7, 8),
362 (7, 10),
363 (7, 11),
364 (8, 11),
365 (9, 10),
366 (9, 12),
367 (10, 11),
368 (10, 12),
369 (10, 13),
370 (11, 13),
371 (12, 13),
372 (12, 14),
373 (13, 14),
374 ]
375 .into_iter()
376 .collect();
377 assert_eq!(directed_arcs(&g), want);
378 }
379
380 #[test]
381 fn rectangle_two_by_two_matches_python_igraph_undirected() {
382 let g = triangular_lattice(&[2, 2], false, false).expect("ok");
385 let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)]
386 .into_iter()
387 .collect();
388 assert_eq!(canonical_undirected(&g), want);
389 }
390
391 #[test]
392 fn rectangle_two_by_two_directed_mutual_doubles_edges() {
393 let g = triangular_lattice(&[2, 2], true, true).expect("ok");
395 let want: BTreeSet<(u32, u32)> = [
396 (0, 1),
397 (0, 2),
398 (0, 3),
399 (1, 0),
400 (1, 3),
401 (2, 0),
402 (2, 3),
403 (3, 0),
404 (3, 1),
405 (3, 2),
406 ]
407 .into_iter()
408 .collect();
409 assert_eq!(directed_arcs(&g), want);
410 assert!(g.is_directed());
411 }
412
413 #[test]
414 fn rectangle_two_by_two_directed_unilateral_matches_undirected_set() {
415 let g = triangular_lattice(&[2, 2], true, false).expect("ok");
418 let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)]
419 .into_iter()
420 .collect();
421 assert_eq!(directed_arcs(&g), want);
422 }
423
424 #[test]
425 fn rectangle_four_by_five_matches_upstream_vcount() {
426 let g = triangular_lattice(&[4, 5], true, true).expect("ok");
429 assert_eq!(g.vcount(), 20);
430 assert_eq!(g.ecount(), 86);
431 }
432
433 #[test]
434 fn hexagonal_3_4_5_matches_upstream_vcount() {
435 let g = triangular_lattice(&[3, 4, 5], false, true).expect("ok");
438 assert_eq!(g.vcount(), 36);
439 assert_eq!(g.ecount(), 87);
440 }
441
442 #[test]
443 fn triangle_three_full_topology() {
444 let g = triangular_lattice(&[3], false, false).expect("ok");
451 assert_eq!(g.vcount(), 6);
452 assert_eq!(g.ecount(), 9);
453 let want: BTreeSet<(u32, u32)> = [
454 (0, 1),
455 (0, 3),
456 (1, 2),
457 (1, 3),
458 (1, 4),
459 (2, 4),
460 (3, 4),
461 (3, 5),
462 (4, 5),
463 ]
464 .into_iter()
465 .collect();
466 assert_eq!(canonical_undirected(&g), want);
467 }
468}
469
470#[cfg(all(test, feature = "proptest-harness"))]
471mod proptests {
472 use super::*;
473 use proptest::prelude::*;
474
475 proptest! {
476 #[test]
477 fn triangle_vcount_is_triangular_number(
478 n in 1u32..30,
479 ) {
480 let g = triangular_lattice(&[n], false, false).expect("ok");
481 let expected = (u64::from(n) * (u64::from(n) + 1)) / 2;
482 prop_assert_eq!(u64::from(g.vcount()), expected);
483 }
484
485 #[test]
486 fn rectangle_vcount_is_product(
487 sx in 1u32..15,
488 sy in 1u32..15,
489 ) {
490 let g = triangular_lattice(&[sx, sy], false, false).expect("ok");
491 prop_assert_eq!(u64::from(g.vcount()), u64::from(sx) * u64::from(sy));
492 }
493
494 #[test]
495 fn directed_mutual_doubles_undirected_ecount(
496 sx in 1u32..10,
497 sy in 1u32..10,
498 ) {
499 let undirected = triangular_lattice(&[sx, sy], false, false).expect("ok");
500 let mutual = triangular_lattice(&[sx, sy], true, true).expect("ok");
501 prop_assert_eq!(mutual.ecount(), undirected.ecount() * 2);
502 }
503
504 #[test]
505 fn directed_unilateral_matches_undirected_ecount(
506 sx in 1u32..10,
507 sy in 1u32..10,
508 ) {
509 let undirected = triangular_lattice(&[sx, sy], false, false).expect("ok");
510 let unilateral = triangular_lattice(&[sx, sy], true, false).expect("ok");
511 prop_assert_eq!(unilateral.ecount(), undirected.ecount());
512 }
513
514 #[test]
515 fn directedness_flag_round_trips(
516 sx in 1u32..8,
517 sy in 1u32..8,
518 directed: bool,
519 mutual: bool,
520 ) {
521 let g = triangular_lattice(&[sx, sy], directed, mutual).expect("ok");
522 prop_assert_eq!(g.is_directed(), directed);
523 }
524
525 #[test]
526 fn max_degree_at_most_six(
527 sx in 1u32..8,
528 sy in 1u32..8,
529 ) {
530 let g = triangular_lattice(&[sx, sy], false, false).expect("ok");
531 for v in 0..g.vcount() {
532 prop_assert!(g.degree(v).unwrap() <= 6);
533 }
534 }
535 }
536}