1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
47
48pub fn hexagonal_lattice(dims: &[u32], directed: bool, mutual: bool) -> IgraphResult<Graph> {
83 if !matches!(dims.len(), 1..=3) {
84 return Err(IgraphError::InvalidArgument(format!(
85 "hexagonal_lattice: size of dimension vector must be 1, 2 or 3, got {}",
86 dims.len()
87 )));
88 }
89
90 if dims.contains(&0) {
92 return Graph::new(0, directed);
93 }
94
95 let (row_lengths, row_start) = match dims.len() {
96 1 => triangle_shape(dims[0]),
97 2 => rectangle_shape(dims[0], dims[1]),
98 3 => hex_shape(dims[0], dims[1], dims[2]),
99 _ => unreachable!("dims length already checked"),
100 };
101
102 layout(&row_lengths, &row_start, directed, mutual)
103}
104
105fn triangle_shape(size: u32) -> (Vec<u32>, Vec<u32>) {
112 let row_count_raw = size + 2;
113 let n_rows = (size + 1) as usize;
114 let mut row_lengths = Vec::with_capacity(n_rows);
115 let mut row_start = Vec::with_capacity(n_rows);
116 for i in 0..(row_count_raw - 1) {
117 let len = 2 * (row_count_raw - i) - if i == 0 { 3 } else { 1 };
118 row_lengths.push(len);
119 row_start.push(u32::from(i == 0));
120 }
121 (row_lengths, row_start)
122}
123
124fn rectangle_shape(size_x: u32, size_y: u32) -> (Vec<u32>, Vec<u32>) {
132 let row_count = size_x + 1;
133 let n_rows = row_count as usize;
134 let actual_size_y = (size_y + 1) * 2;
135
136 let mut row_lengths = Vec::with_capacity(n_rows);
137 let mut row_start = Vec::with_capacity(n_rows);
138
139 for i in 0..row_count {
140 let is_first_row = i == 0;
141 let is_last_row = i == row_count - 1;
142 let is_start_odd = ((row_count - i - 1) % 2) != 0;
143 let len = actual_size_y - u32::from(is_first_row || is_last_row);
144 let start = (row_count - i - 1) + u32::from(is_first_row && !is_start_odd);
145 row_lengths.push(len);
146 row_start.push(start);
147 }
148 (row_lengths, row_start)
149}
150
151fn hex_shape(size_x: u32, size_y: u32, size_z: u32) -> (Vec<u32>, Vec<u32>) {
161 let row_count = size_y + size_z;
162 let n_rows = row_count as usize;
163
164 let mut row_length: i64 = i64::from(size_x) * 2 + 1;
169 let mut row_start_val: i64 = i64::from(size_y) * 2 - 1;
170 let first_threshold: i64 = i64::from(size_y.min(size_z)) - 1;
171 let second_threshold: i64 = i64::from(size_y.max(size_z)) - 1;
172 let middle_shrinks: bool = size_y >= size_z;
173
174 let mut row_lengths = Vec::with_capacity(n_rows);
175 let mut row_start = Vec::with_capacity(n_rows);
176
177 for i in 0..row_count {
178 let len_u32 = u32::try_from(row_length).expect("row_length non-negative by construction");
179 let start_u32 =
180 u32::try_from(row_start_val).expect("row_start non-negative by construction");
181 row_lengths.push(len_u32);
182 row_start.push(start_u32);
183
184 let ii = i64::from(i);
185 if ii < first_threshold {
186 row_length += 2;
187 row_start_val -= 2;
188 } else if ii < second_threshold {
189 if middle_shrinks {
190 row_start_val -= 2;
191 }
192 } else {
193 row_length -= 2;
194 }
195 if i == size_y - 1 {
196 row_start_val -= 1;
197 row_length += 1;
198 }
199 if i == size_z - 1 {
200 row_length += 1;
201 }
202 }
203
204 (row_lengths, row_start)
205}
206
207fn layout(
218 row_lengths: &[u32],
219 row_start: &[u32],
220 directed: bool,
221 mutual: bool,
222) -> IgraphResult<Graph> {
223 debug_assert_eq!(row_lengths.len(), row_start.len());
224 let row_count = row_lengths.len();
225
226 let mut prefix_sum: Vec<u32> = Vec::with_capacity(row_count + 1);
227 prefix_sum.push(0);
228 for &len in row_lengths {
229 let last = *prefix_sum.last().expect("non-empty");
230 let next = last
231 .checked_add(len)
232 .ok_or_else(|| overflow_error("vertex count"))?;
233 prefix_sum.push(next);
234 }
235 let vcount = *prefix_sum.last().expect("non-empty");
236
237 let vertex_index = |i: u32, j: usize| -> u32 { prefix_sum[j] + i - row_start[j] };
238 let row_end = |j: usize| -> u32 { row_start[j] + row_lengths[j] - 1 };
239
240 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
241
242 let add_if_in_bounds =
243 |edges: &mut Vec<(VertexId, VertexId)>, i: u32, j: usize, k_opt: Option<u32>, l: usize| {
244 let Some(k) = k_opt else {
245 return;
246 };
247 if l >= row_count {
248 return;
249 }
250 let l_start = row_start[l];
251 let l_end = row_end(l);
252 if k < l_start || k > l_end {
253 return;
254 }
255 let src = vertex_index(i, j);
256 let dst = vertex_index(k, l);
257 edges.push((src, dst));
258 if directed && mutual {
259 edges.push((dst, src));
260 }
261 };
262
263 for j in 0..row_count {
264 let row_len = row_lengths[j];
265 let start = row_start[j];
266 for i in 0..row_len {
267 let k = start + i;
268 add_if_in_bounds(&mut edges, k, j, Some(k + 1), j);
272 if j + 1 < row_count && k % 2 == 1 {
275 add_if_in_bounds(&mut edges, k, j, k.checked_sub(1), j + 1);
276 }
277 }
278 }
279
280 let mut g = Graph::new(vcount, directed)?;
281 g.add_edges(edges)?;
282 Ok(g)
283}
284
285fn overflow_error(kind: &str) -> IgraphError {
286 IgraphError::InvalidArgument(format!("hexagonal_lattice: {kind} overflows u32"))
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use std::collections::BTreeSet;
293
294 fn canonical_undirected(g: &Graph) -> BTreeSet<(u32, u32)> {
295 let mut s = BTreeSet::new();
296 for v in 0..g.vcount() {
297 for &u in &g.neighbors(v).expect("neighbors") {
298 let key = if v <= u { (v, u) } else { (u, v) };
299 s.insert(key);
300 }
301 }
302 s
303 }
304
305 fn directed_arcs(g: &Graph) -> BTreeSet<(u32, u32)> {
306 (0..u32::try_from(g.ecount()).expect("ecount fits u32"))
307 .map(|eid| g.edge(eid).expect("edge"))
308 .collect()
309 }
310
311 #[test]
312 fn empty_dims_rejected() {
313 let r = hexagonal_lattice(&[], false, false);
314 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
315 }
316
317 #[test]
318 fn four_dim_rejected() {
319 let r = hexagonal_lattice(&[1, 2, 3, 4], false, false);
320 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
321 }
322
323 #[test]
324 fn zero_dim_yields_empty_graph() {
325 let g = hexagonal_lattice(&[3, 0], false, false).expect("ok");
326 assert_eq!(g.vcount(), 0);
327 assert_eq!(g.ecount(), 0);
328 }
329
330 #[test]
331 fn zero_dim_directed_keeps_flag() {
332 let g = hexagonal_lattice(&[0, 3, 4], true, true).expect("ok");
333 assert_eq!(g.vcount(), 0);
334 assert!(g.is_directed());
335 }
336
337 #[test]
338 fn single_hexagon_matches_upstream_c6() {
339 let g = hexagonal_lattice(&[1], true, false).expect("ok");
342 assert_eq!(g.vcount(), 6);
343 assert_eq!(g.ecount(), 6);
344 let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 3), (1, 2), (2, 5), (3, 4), (4, 5)]
345 .into_iter()
346 .collect();
347 assert_eq!(directed_arcs(&g), want);
348 }
349
350 #[test]
351 fn triangular_hex_lattice_side_5_matches_upstream_vcount() {
352 let g = hexagonal_lattice(&[5], true, false).expect("ok");
354 assert_eq!(g.vcount(), 46);
355 assert_eq!(g.ecount(), 60);
356 }
357
358 #[test]
359 fn rectangle_4x5_directed_mutual_matches_upstream_vcount() {
360 let g = hexagonal_lattice(&[4, 5], true, true).expect("ok");
363 assert_eq!(g.vcount(), 58);
364 assert_eq!(g.ecount(), 154);
365 }
366
367 #[test]
368 fn rectangle_2x2_matches_python_igraph_undirected() {
369 let g = hexagonal_lattice(&[2, 2], false, false).expect("ok");
372 assert_eq!(g.vcount(), 16);
373 assert_eq!(g.ecount(), 19);
374 let want: BTreeSet<(u32, u32)> = [
375 (0, 1),
376 (0, 6),
377 (1, 2),
378 (2, 3),
379 (2, 8),
380 (3, 4),
381 (4, 10),
382 (5, 6),
383 (5, 11),
384 (6, 7),
385 (7, 8),
386 (7, 13),
387 (8, 9),
388 (9, 10),
389 (9, 15),
390 (11, 12),
391 (12, 13),
392 (13, 14),
393 (14, 15),
394 ]
395 .into_iter()
396 .collect();
397 assert_eq!(canonical_undirected(&g), want);
398 }
399
400 #[test]
401 fn rectangle_2x2_directed_unilateral_matches_undirected_edges() {
402 let g = hexagonal_lattice(&[2, 2], true, false).expect("ok");
403 let want: BTreeSet<(u32, u32)> = [
404 (0, 1),
405 (0, 6),
406 (1, 2),
407 (2, 3),
408 (2, 8),
409 (3, 4),
410 (4, 10),
411 (5, 6),
412 (5, 11),
413 (6, 7),
414 (7, 8),
415 (7, 13),
416 (8, 9),
417 (9, 10),
418 (9, 15),
419 (11, 12),
420 (12, 13),
421 (13, 14),
422 (14, 15),
423 ]
424 .into_iter()
425 .collect();
426 assert_eq!(directed_arcs(&g), want);
427 }
428
429 #[test]
430 fn rectangle_2x2_directed_mutual_doubles_edges() {
431 let g = hexagonal_lattice(&[2, 2], true, true).expect("ok");
432 assert_eq!(g.ecount(), 19 * 2);
433 assert!(g.is_directed());
434 }
435
436 #[test]
437 fn hexagonal_3_4_5_matches_upstream_vcount() {
438 let g = hexagonal_lattice(&[3, 4, 5], false, true).expect("ok");
442 assert_eq!(g.vcount(), 94);
443 assert_eq!(g.ecount(), 129);
444 for v in 0..g.vcount() {
446 assert!(g.degree(v).expect("deg") <= 3);
447 }
448 }
449
450 #[test]
451 fn all_vertices_have_degree_at_most_three() {
452 for &(sx, sy, sz) in &[(3u32, 4u32, 5u32), (2, 3, 4), (5, 5, 5)] {
453 let g = hexagonal_lattice(&[sx, sy, sz], false, false).expect("ok");
454 for v in 0..g.vcount() {
455 assert!(g.degree(v).expect("deg") <= 3);
456 }
457 }
458 }
459}
460
461#[cfg(all(test, feature = "proptest-harness"))]
462mod proptests {
463 use super::*;
464 use proptest::prelude::*;
465
466 proptest! {
467 #[test]
468 fn directed_mutual_doubles_undirected_ecount(
469 sx in 1u32..6,
470 sy in 1u32..6,
471 ) {
472 let undirected = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
473 let mutual = hexagonal_lattice(&[sx, sy], true, true).expect("ok");
474 prop_assert_eq!(mutual.ecount(), undirected.ecount() * 2);
475 }
476
477 #[test]
478 fn directed_unilateral_matches_undirected_ecount(
479 sx in 1u32..6,
480 sy in 1u32..6,
481 ) {
482 let undirected = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
483 let unilateral = hexagonal_lattice(&[sx, sy], true, false).expect("ok");
484 prop_assert_eq!(unilateral.ecount(), undirected.ecount());
485 }
486
487 #[test]
488 fn directedness_flag_round_trips(
489 sx in 1u32..6,
490 sy in 1u32..6,
491 directed: bool,
492 mutual: bool,
493 ) {
494 let g = hexagonal_lattice(&[sx, sy], directed, mutual).expect("ok");
495 prop_assert_eq!(g.is_directed(), directed);
496 }
497
498 #[test]
499 fn max_degree_at_most_three(
500 sx in 1u32..6,
501 sy in 1u32..6,
502 ) {
503 let g = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
504 for v in 0..g.vcount() {
505 prop_assert!(g.degree(v).unwrap() <= 3);
506 }
507 }
508
509 #[test]
510 fn triangle_shape_vcount_grows_quadratically(
511 n in 1u32..8,
512 ) {
513 let g = hexagonal_lattice(&[n], false, false).expect("ok");
517 prop_assert!(u64::from(g.vcount()) >= u64::from(n) * 5);
520 }
521
522 #[test]
523 fn hex_shape_max_degree_bounded(
524 sx in 1u32..5,
525 sy in 1u32..5,
526 sz in 1u32..5,
527 ) {
528 let g = hexagonal_lattice(&[sx, sy, sz], false, false).expect("ok");
529 for v in 0..g.vcount() {
530 prop_assert!(g.degree(v).unwrap() <= 3);
531 }
532 }
533 }
534}