use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn hexagonal_lattice(dims: &[u32], directed: bool, mutual: bool) -> IgraphResult<Graph> {
if !matches!(dims.len(), 1..=3) {
return Err(IgraphError::InvalidArgument(format!(
"hexagonal_lattice: size of dimension vector must be 1, 2 or 3, got {}",
dims.len()
)));
}
if dims.contains(&0) {
return Graph::new(0, directed);
}
let (row_lengths, row_start) = match dims.len() {
1 => triangle_shape(dims[0]),
2 => rectangle_shape(dims[0], dims[1]),
3 => hex_shape(dims[0], dims[1], dims[2]),
_ => unreachable!("dims length already checked"),
};
layout(&row_lengths, &row_start, directed, mutual)
}
fn triangle_shape(size: u32) -> (Vec<u32>, Vec<u32>) {
let row_count_raw = size + 2;
let n_rows = (size + 1) as usize;
let mut row_lengths = Vec::with_capacity(n_rows);
let mut row_start = Vec::with_capacity(n_rows);
for i in 0..(row_count_raw - 1) {
let len = 2 * (row_count_raw - i) - if i == 0 { 3 } else { 1 };
row_lengths.push(len);
row_start.push(u32::from(i == 0));
}
(row_lengths, row_start)
}
fn rectangle_shape(size_x: u32, size_y: u32) -> (Vec<u32>, Vec<u32>) {
let row_count = size_x + 1;
let n_rows = row_count as usize;
let actual_size_y = (size_y + 1) * 2;
let mut row_lengths = Vec::with_capacity(n_rows);
let mut row_start = Vec::with_capacity(n_rows);
for i in 0..row_count {
let is_first_row = i == 0;
let is_last_row = i == row_count - 1;
let is_start_odd = ((row_count - i - 1) % 2) != 0;
let len = actual_size_y - u32::from(is_first_row || is_last_row);
let start = (row_count - i - 1) + u32::from(is_first_row && !is_start_odd);
row_lengths.push(len);
row_start.push(start);
}
(row_lengths, row_start)
}
fn hex_shape(size_x: u32, size_y: u32, size_z: u32) -> (Vec<u32>, Vec<u32>) {
let row_count = size_y + size_z;
let n_rows = row_count as usize;
let mut row_length: i64 = i64::from(size_x) * 2 + 1;
let mut row_start_val: i64 = i64::from(size_y) * 2 - 1;
let first_threshold: i64 = i64::from(size_y.min(size_z)) - 1;
let second_threshold: i64 = i64::from(size_y.max(size_z)) - 1;
let middle_shrinks: bool = size_y >= size_z;
let mut row_lengths = Vec::with_capacity(n_rows);
let mut row_start = Vec::with_capacity(n_rows);
for i in 0..row_count {
let len_u32 = u32::try_from(row_length).expect("row_length non-negative by construction");
let start_u32 =
u32::try_from(row_start_val).expect("row_start non-negative by construction");
row_lengths.push(len_u32);
row_start.push(start_u32);
let ii = i64::from(i);
if ii < first_threshold {
row_length += 2;
row_start_val -= 2;
} else if ii < second_threshold {
if middle_shrinks {
row_start_val -= 2;
}
} else {
row_length -= 2;
}
if i == size_y - 1 {
row_start_val -= 1;
row_length += 1;
}
if i == size_z - 1 {
row_length += 1;
}
}
(row_lengths, row_start)
}
fn layout(
row_lengths: &[u32],
row_start: &[u32],
directed: bool,
mutual: bool,
) -> IgraphResult<Graph> {
debug_assert_eq!(row_lengths.len(), row_start.len());
let row_count = row_lengths.len();
let mut prefix_sum: Vec<u32> = Vec::with_capacity(row_count + 1);
prefix_sum.push(0);
for &len in row_lengths {
let last = *prefix_sum.last().expect("non-empty");
let next = last
.checked_add(len)
.ok_or_else(|| overflow_error("vertex count"))?;
prefix_sum.push(next);
}
let vcount = *prefix_sum.last().expect("non-empty");
let vertex_index = |i: u32, j: usize| -> u32 { prefix_sum[j] + i - row_start[j] };
let row_end = |j: usize| -> u32 { row_start[j] + row_lengths[j] - 1 };
let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
let add_if_in_bounds =
|edges: &mut Vec<(VertexId, VertexId)>, i: u32, j: usize, k_opt: Option<u32>, l: usize| {
let Some(k) = k_opt else {
return;
};
if l >= row_count {
return;
}
let l_start = row_start[l];
let l_end = row_end(l);
if k < l_start || k > l_end {
return;
}
let src = vertex_index(i, j);
let dst = vertex_index(k, l);
edges.push((src, dst));
if directed && mutual {
edges.push((dst, src));
}
};
for j in 0..row_count {
let row_len = row_lengths[j];
let start = row_start[j];
for i in 0..row_len {
let k = start + i;
add_if_in_bounds(&mut edges, k, j, Some(k + 1), j);
if j + 1 < row_count && k % 2 == 1 {
add_if_in_bounds(&mut edges, k, j, k.checked_sub(1), j + 1);
}
}
}
let mut g = Graph::new(vcount, directed)?;
g.add_edges(edges)?;
Ok(g)
}
fn overflow_error(kind: &str) -> IgraphError {
IgraphError::InvalidArgument(format!("hexagonal_lattice: {kind} overflows u32"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn canonical_undirected(g: &Graph) -> BTreeSet<(u32, u32)> {
let mut s = BTreeSet::new();
for v in 0..g.vcount() {
for &u in &g.neighbors(v).expect("neighbors") {
let key = if v <= u { (v, u) } else { (u, v) };
s.insert(key);
}
}
s
}
fn directed_arcs(g: &Graph) -> BTreeSet<(u32, u32)> {
(0..u32::try_from(g.ecount()).expect("ecount fits u32"))
.map(|eid| g.edge(eid).expect("edge"))
.collect()
}
#[test]
fn empty_dims_rejected() {
let r = hexagonal_lattice(&[], false, false);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn four_dim_rejected() {
let r = hexagonal_lattice(&[1, 2, 3, 4], false, false);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn zero_dim_yields_empty_graph() {
let g = hexagonal_lattice(&[3, 0], false, false).expect("ok");
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn zero_dim_directed_keeps_flag() {
let g = hexagonal_lattice(&[0, 3, 4], true, true).expect("ok");
assert_eq!(g.vcount(), 0);
assert!(g.is_directed());
}
#[test]
fn single_hexagon_matches_upstream_c6() {
let g = hexagonal_lattice(&[1], true, false).expect("ok");
assert_eq!(g.vcount(), 6);
assert_eq!(g.ecount(), 6);
let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 3), (1, 2), (2, 5), (3, 4), (4, 5)]
.into_iter()
.collect();
assert_eq!(directed_arcs(&g), want);
}
#[test]
fn triangular_hex_lattice_side_5_matches_upstream_vcount() {
let g = hexagonal_lattice(&[5], true, false).expect("ok");
assert_eq!(g.vcount(), 46);
assert_eq!(g.ecount(), 60);
}
#[test]
fn rectangle_4x5_directed_mutual_matches_upstream_vcount() {
let g = hexagonal_lattice(&[4, 5], true, true).expect("ok");
assert_eq!(g.vcount(), 58);
assert_eq!(g.ecount(), 154);
}
#[test]
fn rectangle_2x2_matches_python_igraph_undirected() {
let g = hexagonal_lattice(&[2, 2], false, false).expect("ok");
assert_eq!(g.vcount(), 16);
assert_eq!(g.ecount(), 19);
let want: BTreeSet<(u32, u32)> = [
(0, 1),
(0, 6),
(1, 2),
(2, 3),
(2, 8),
(3, 4),
(4, 10),
(5, 6),
(5, 11),
(6, 7),
(7, 8),
(7, 13),
(8, 9),
(9, 10),
(9, 15),
(11, 12),
(12, 13),
(13, 14),
(14, 15),
]
.into_iter()
.collect();
assert_eq!(canonical_undirected(&g), want);
}
#[test]
fn rectangle_2x2_directed_unilateral_matches_undirected_edges() {
let g = hexagonal_lattice(&[2, 2], true, false).expect("ok");
let want: BTreeSet<(u32, u32)> = [
(0, 1),
(0, 6),
(1, 2),
(2, 3),
(2, 8),
(3, 4),
(4, 10),
(5, 6),
(5, 11),
(6, 7),
(7, 8),
(7, 13),
(8, 9),
(9, 10),
(9, 15),
(11, 12),
(12, 13),
(13, 14),
(14, 15),
]
.into_iter()
.collect();
assert_eq!(directed_arcs(&g), want);
}
#[test]
fn rectangle_2x2_directed_mutual_doubles_edges() {
let g = hexagonal_lattice(&[2, 2], true, true).expect("ok");
assert_eq!(g.ecount(), 19 * 2);
assert!(g.is_directed());
}
#[test]
fn hexagonal_3_4_5_matches_upstream_vcount() {
let g = hexagonal_lattice(&[3, 4, 5], false, true).expect("ok");
assert_eq!(g.vcount(), 94);
assert_eq!(g.ecount(), 129);
for v in 0..g.vcount() {
assert!(g.degree(v).expect("deg") <= 3);
}
}
#[test]
fn all_vertices_have_degree_at_most_three() {
for &(sx, sy, sz) in &[(3u32, 4u32, 5u32), (2, 3, 4), (5, 5, 5)] {
let g = hexagonal_lattice(&[sx, sy, sz], false, false).expect("ok");
for v in 0..g.vcount() {
assert!(g.degree(v).expect("deg") <= 3);
}
}
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn directed_mutual_doubles_undirected_ecount(
sx in 1u32..6,
sy in 1u32..6,
) {
let undirected = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
let mutual = hexagonal_lattice(&[sx, sy], true, true).expect("ok");
prop_assert_eq!(mutual.ecount(), undirected.ecount() * 2);
}
#[test]
fn directed_unilateral_matches_undirected_ecount(
sx in 1u32..6,
sy in 1u32..6,
) {
let undirected = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
let unilateral = hexagonal_lattice(&[sx, sy], true, false).expect("ok");
prop_assert_eq!(unilateral.ecount(), undirected.ecount());
}
#[test]
fn directedness_flag_round_trips(
sx in 1u32..6,
sy in 1u32..6,
directed: bool,
mutual: bool,
) {
let g = hexagonal_lattice(&[sx, sy], directed, mutual).expect("ok");
prop_assert_eq!(g.is_directed(), directed);
}
#[test]
fn max_degree_at_most_three(
sx in 1u32..6,
sy in 1u32..6,
) {
let g = hexagonal_lattice(&[sx, sy], false, false).expect("ok");
for v in 0..g.vcount() {
prop_assert!(g.degree(v).unwrap() <= 3);
}
}
#[test]
fn triangle_shape_vcount_grows_quadratically(
n in 1u32..8,
) {
let g = hexagonal_lattice(&[n], false, false).expect("ok");
prop_assert!(u64::from(g.vcount()) >= u64::from(n) * 5);
}
#[test]
fn hex_shape_max_degree_bounded(
sx in 1u32..5,
sy in 1u32..5,
sz in 1u32..5,
) {
let g = hexagonal_lattice(&[sx, sy, sz], false, false).expect("ok");
for v in 0..g.vcount() {
prop_assert!(g.degree(v).unwrap() <= 3);
}
}
}
}