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) -> IgraphResult<(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).map_err(|_| {
179 crate::core::IgraphError::InvalidArgument("row_length out of u32 range".into())
180 })?;
181 let start_u32 = u32::try_from(row_start_val).map_err(|_| {
182 crate::core::IgraphError::InvalidArgument("row_start out of u32 range".into())
183 })?;
184 row_lengths.push(len_u32);
185 row_start.push(start_u32);
186
187 let ii = i64::from(i);
188 if ii < first_threshold {
189 row_length += 2;
190 row_start_val -= 2;
191 } else if ii < second_threshold {
192 if middle_shrinks {
193 row_start_val -= 2;
194 }
195 } else {
196 row_length -= 2;
197 }
198 if i == size_y - 1 {
199 row_start_val -= 1;
200 row_length += 1;
201 }
202 if i == size_z - 1 {
203 row_length += 1;
204 }
205 }
206
207 Ok((row_lengths, row_start))
208}
209
210fn layout(
221 row_lengths: &[u32],
222 row_start: &[u32],
223 directed: bool,
224 mutual: bool,
225) -> IgraphResult<Graph> {
226 debug_assert_eq!(row_lengths.len(), row_start.len());
227 let row_count = row_lengths.len();
228
229 let mut prefix_sum: Vec<u32> = Vec::with_capacity(row_count + 1);
230 prefix_sum.push(0);
231 for &len in row_lengths {
232 let Some(&last) = prefix_sum.last() else {
233 return Err(overflow_error("empty prefix sum"));
234 };
235 let next = last
236 .checked_add(len)
237 .ok_or_else(|| overflow_error("vertex count"))?;
238 prefix_sum.push(next);
239 }
240 let Some(&vcount) = prefix_sum.last() else {
241 return Err(overflow_error("empty prefix sum"));
242 };
243
244 let vertex_index = |i: u32, j: usize| -> u32 { prefix_sum[j] + i - row_start[j] };
245 let row_end = |j: usize| -> u32 { row_start[j] + row_lengths[j] - 1 };
246
247 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
248
249 let add_if_in_bounds =
250 |edges: &mut Vec<(VertexId, VertexId)>, i: u32, j: usize, k_opt: Option<u32>, l: usize| {
251 let Some(k) = k_opt else {
252 return;
253 };
254 if l >= row_count {
255 return;
256 }
257 let l_start = row_start[l];
258 let l_end = row_end(l);
259 if k < l_start || k > l_end {
260 return;
261 }
262 let src = vertex_index(i, j);
263 let dst = vertex_index(k, l);
264 edges.push((src, dst));
265 if directed && mutual {
266 edges.push((dst, src));
267 }
268 };
269
270 for j in 0..row_count {
271 let row_len = row_lengths[j];
272 let start = row_start[j];
273 for i in 0..row_len {
274 let k = start + i;
275 add_if_in_bounds(&mut edges, k, j, Some(k + 1), j);
279 if j + 1 < row_count && k % 2 == 1 {
282 add_if_in_bounds(&mut edges, k, j, k.checked_sub(1), j + 1);
283 }
284 }
285 }
286
287 let mut g = Graph::new(vcount, directed)?;
288 g.add_edges(edges)?;
289 Ok(g)
290}
291
292fn overflow_error(kind: &str) -> IgraphError {
293 IgraphError::InvalidArgument(format!("hexagonal_lattice: {kind} overflows u32"))
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use std::collections::BTreeSet;
300
301 fn canonical_undirected(g: &Graph) -> BTreeSet<(u32, u32)> {
302 let mut s = BTreeSet::new();
303 for v in 0..g.vcount() {
304 for &u in &g.neighbors(v).expect("neighbors") {
305 let key = if v <= u { (v, u) } else { (u, v) };
306 s.insert(key);
307 }
308 }
309 s
310 }
311
312 fn directed_arcs(g: &Graph) -> BTreeSet<(u32, u32)> {
313 (0..u32::try_from(g.ecount()).expect("ecount fits u32"))
314 .map(|eid| g.edge(eid).expect("edge"))
315 .collect()
316 }
317
318 #[test]
319 fn empty_dims_rejected() {
320 let r = hexagonal_lattice(&[], false, false);
321 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
322 }
323
324 #[test]
325 fn four_dim_rejected() {
326 let r = hexagonal_lattice(&[1, 2, 3, 4], false, false);
327 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
328 }
329
330 #[test]
331 fn zero_dim_yields_empty_graph() {
332 let g = hexagonal_lattice(&[3, 0], false, false).expect("ok");
333 assert_eq!(g.vcount(), 0);
334 assert_eq!(g.ecount(), 0);
335 }
336
337 #[test]
338 fn zero_dim_directed_keeps_flag() {
339 let g = hexagonal_lattice(&[0, 3, 4], true, true).expect("ok");
340 assert_eq!(g.vcount(), 0);
341 assert!(g.is_directed());
342 }
343
344 #[test]
345 fn single_hexagon_matches_upstream_c6() {
346 let g = hexagonal_lattice(&[1], true, false).expect("ok");
349 assert_eq!(g.vcount(), 6);
350 assert_eq!(g.ecount(), 6);
351 let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 3), (1, 2), (2, 5), (3, 4), (4, 5)]
352 .into_iter()
353 .collect();
354 assert_eq!(directed_arcs(&g), want);
355 }
356
357 #[test]
358 fn triangular_hex_lattice_side_5_matches_upstream_vcount() {
359 let g = hexagonal_lattice(&[5], true, false).expect("ok");
361 assert_eq!(g.vcount(), 46);
362 assert_eq!(g.ecount(), 60);
363 }
364
365 #[test]
366 fn rectangle_4x5_directed_mutual_matches_upstream_vcount() {
367 let g = hexagonal_lattice(&[4, 5], true, true).expect("ok");
370 assert_eq!(g.vcount(), 58);
371 assert_eq!(g.ecount(), 154);
372 }
373
374 #[test]
375 fn rectangle_2x2_matches_python_igraph_undirected() {
376 let g = hexagonal_lattice(&[2, 2], false, false).expect("ok");
379 assert_eq!(g.vcount(), 16);
380 assert_eq!(g.ecount(), 19);
381 let want: BTreeSet<(u32, u32)> = [
382 (0, 1),
383 (0, 6),
384 (1, 2),
385 (2, 3),
386 (2, 8),
387 (3, 4),
388 (4, 10),
389 (5, 6),
390 (5, 11),
391 (6, 7),
392 (7, 8),
393 (7, 13),
394 (8, 9),
395 (9, 10),
396 (9, 15),
397 (11, 12),
398 (12, 13),
399 (13, 14),
400 (14, 15),
401 ]
402 .into_iter()
403 .collect();
404 assert_eq!(canonical_undirected(&g), want);
405 }
406
407 #[test]
408 fn rectangle_2x2_directed_unilateral_matches_undirected_edges() {
409 let g = hexagonal_lattice(&[2, 2], true, false).expect("ok");
410 let want: BTreeSet<(u32, u32)> = [
411 (0, 1),
412 (0, 6),
413 (1, 2),
414 (2, 3),
415 (2, 8),
416 (3, 4),
417 (4, 10),
418 (5, 6),
419 (5, 11),
420 (6, 7),
421 (7, 8),
422 (7, 13),
423 (8, 9),
424 (9, 10),
425 (9, 15),
426 (11, 12),
427 (12, 13),
428 (13, 14),
429 (14, 15),
430 ]
431 .into_iter()
432 .collect();
433 assert_eq!(directed_arcs(&g), want);
434 }
435
436 #[test]
437 fn rectangle_2x2_directed_mutual_doubles_edges() {
438 let g = hexagonal_lattice(&[2, 2], true, true).expect("ok");
439 assert_eq!(g.ecount(), 19 * 2);
440 assert!(g.is_directed());
441 }
442
443 #[test]
444 fn hexagonal_3_4_5_matches_upstream_vcount() {
445 let g = hexagonal_lattice(&[3, 4, 5], false, true).expect("ok");
449 assert_eq!(g.vcount(), 94);
450 assert_eq!(g.ecount(), 129);
451 for v in 0..g.vcount() {
453 assert!(g.degree(v).expect("deg") <= 3);
454 }
455 }
456
457 #[test]
458 fn all_vertices_have_degree_at_most_three() {
459 for &(sx, sy, sz) in &[(3u32, 4u32, 5u32), (2, 3, 4), (5, 5, 5)] {
460 let g = hexagonal_lattice(&[sx, sy, sz], false, false).expect("ok");
461 for v in 0..g.vcount() {
462 assert!(g.degree(v).expect("deg") <= 3);
463 }
464 }
465 }
466}
467
468#[cfg(all(test, feature = "proptest-harness"))]
469mod proptests {
470 use super::*;
471 use proptest::prelude::*;
472
473 proptest! {
474 #[test]
475 fn directed_mutual_doubles_undirected_ecount(
476 sx in 1u32..6,
477 sy in 1u32..6,
478 ) {
479 let undirected = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
480 let mutual = hexagonal_lattice(&[sx, sy], true, true).expect("ok");
481 prop_assert_eq!(mutual.ecount(), undirected.ecount() * 2);
482 }
483
484 #[test]
485 fn directed_unilateral_matches_undirected_ecount(
486 sx in 1u32..6,
487 sy in 1u32..6,
488 ) {
489 let undirected = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
490 let unilateral = hexagonal_lattice(&[sx, sy], true, false).expect("ok");
491 prop_assert_eq!(unilateral.ecount(), undirected.ecount());
492 }
493
494 #[test]
495 fn directedness_flag_round_trips(
496 sx in 1u32..6,
497 sy in 1u32..6,
498 directed: bool,
499 mutual: bool,
500 ) {
501 let g = hexagonal_lattice(&[sx, sy], directed, mutual).expect("ok");
502 prop_assert_eq!(g.is_directed(), directed);
503 }
504
505 #[test]
506 fn max_degree_at_most_three(
507 sx in 1u32..6,
508 sy in 1u32..6,
509 ) {
510 let g = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
511 for v in 0..g.vcount() {
512 prop_assert!(g.degree(v).unwrap() <= 3);
513 }
514 }
515
516 #[test]
517 fn triangle_shape_vcount_grows_quadratically(
518 n in 1u32..8,
519 ) {
520 let g = hexagonal_lattice(&[n], false, false).expect("ok");
524 prop_assert!(u64::from(g.vcount()) >= u64::from(n) * 5);
527 }
528
529 #[test]
530 fn hex_shape_max_degree_bounded(
531 sx in 1u32..5,
532 sy in 1u32..5,
533 sz in 1u32..5,
534 ) {
535 let g = hexagonal_lattice(&[sx, sy, sz], false, false).expect("ok");
536 for v in 0..g.vcount() {
537 prop_assert!(g.degree(v).unwrap() <= 3);
538 }
539 }
540 }
541}