use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn triangular_lattice(dims: &[u32], directed: bool, mutual: bool) -> IgraphResult<Graph> {
if !matches!(dims.len(), 1..=3) {
return Err(IgraphError::InvalidArgument(format!(
"triangular_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(n: u32) -> (Vec<u32>, Vec<u32>) {
let mut row_lengths = Vec::with_capacity(n as usize);
let mut row_start = Vec::with_capacity(n as usize);
for j in 0..n {
row_lengths.push(n - j);
row_start.push(0);
}
(row_lengths, row_start)
}
fn rectangle_shape(size_x: u32, size_y: u32) -> (Vec<u32>, Vec<u32>) {
let mut row_lengths = Vec::with_capacity(size_x as usize);
let mut row_start = Vec::with_capacity(size_x as usize);
for j in 0..size_x {
row_lengths.push(size_y);
row_start.push((size_x - j) / 2);
}
(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 - 1;
let mut row_length: u32 = size_x;
let mut row_start_val: u32 = size_y - 1;
let first_threshold: u32 = size_y.min(size_z) - 1;
let second_threshold: u32 = size_y.max(size_z) - 1;
let shrink_in_middle: bool = size_y >= size_z;
let mut row_lengths = Vec::with_capacity(row_count as usize);
let mut row_start = Vec::with_capacity(row_count as usize);
for i in 0..row_count {
row_lengths.push(row_length);
row_start.push(row_start_val);
if i < first_threshold {
row_length += 1;
row_start_val -= 1;
} else if i < second_threshold {
if shrink_in_middle {
row_start_val -= 1;
}
} else {
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 {
add_if_in_bounds(&mut edges, k, j, Some(k), j + 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!("triangular_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 = triangular_lattice(&[], false, false);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn four_dim_rejected() {
let r = triangular_lattice(&[1, 2, 3, 4], false, false);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn zero_dim_yields_empty_graph() {
let g = triangular_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 = triangular_lattice(&[0, 3, 4], true, true).expect("ok");
assert_eq!(g.vcount(), 0);
assert!(g.is_directed());
}
#[test]
fn triangle_single_vertex() {
let g = triangular_lattice(&[1], true, false).expect("ok");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
assert!(g.is_directed());
}
#[test]
fn triangle_side_five_matches_upstream_canon() {
let g = triangular_lattice(&[5], true, false).expect("ok");
assert_eq!(g.vcount(), 15);
assert_eq!(g.ecount(), 30);
let want: BTreeSet<(u32, u32)> = [
(0, 1),
(0, 5),
(1, 2),
(1, 5),
(1, 6),
(2, 3),
(2, 6),
(2, 7),
(3, 4),
(3, 7),
(3, 8),
(4, 8),
(5, 6),
(5, 9),
(6, 7),
(6, 9),
(6, 10),
(7, 8),
(7, 10),
(7, 11),
(8, 11),
(9, 10),
(9, 12),
(10, 11),
(10, 12),
(10, 13),
(11, 13),
(12, 13),
(12, 14),
(13, 14),
]
.into_iter()
.collect();
assert_eq!(directed_arcs(&g), want);
}
#[test]
fn rectangle_two_by_two_matches_python_igraph_undirected() {
let g = triangular_lattice(&[2, 2], false, false).expect("ok");
let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)]
.into_iter()
.collect();
assert_eq!(canonical_undirected(&g), want);
}
#[test]
fn rectangle_two_by_two_directed_mutual_doubles_edges() {
let g = triangular_lattice(&[2, 2], true, true).expect("ok");
let want: BTreeSet<(u32, u32)> = [
(0, 1),
(0, 2),
(0, 3),
(1, 0),
(1, 3),
(2, 0),
(2, 3),
(3, 0),
(3, 1),
(3, 2),
]
.into_iter()
.collect();
assert_eq!(directed_arcs(&g), want);
assert!(g.is_directed());
}
#[test]
fn rectangle_two_by_two_directed_unilateral_matches_undirected_set() {
let g = triangular_lattice(&[2, 2], true, false).expect("ok");
let want: BTreeSet<(u32, u32)> = [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)]
.into_iter()
.collect();
assert_eq!(directed_arcs(&g), want);
}
#[test]
fn rectangle_four_by_five_matches_upstream_vcount() {
let g = triangular_lattice(&[4, 5], true, true).expect("ok");
assert_eq!(g.vcount(), 20);
assert_eq!(g.ecount(), 86);
}
#[test]
fn hexagonal_3_4_5_matches_upstream_vcount() {
let g = triangular_lattice(&[3, 4, 5], false, true).expect("ok");
assert_eq!(g.vcount(), 36);
assert_eq!(g.ecount(), 87);
}
#[test]
fn triangle_three_full_topology() {
let g = triangular_lattice(&[3], false, false).expect("ok");
assert_eq!(g.vcount(), 6);
assert_eq!(g.ecount(), 9);
let want: BTreeSet<(u32, u32)> = [
(0, 1),
(0, 3),
(1, 2),
(1, 3),
(1, 4),
(2, 4),
(3, 4),
(3, 5),
(4, 5),
]
.into_iter()
.collect();
assert_eq!(canonical_undirected(&g), want);
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn triangle_vcount_is_triangular_number(
n in 1u32..30,
) {
let g = triangular_lattice(&[n], false, false).expect("ok");
let expected = (u64::from(n) * (u64::from(n) + 1)) / 2;
prop_assert_eq!(u64::from(g.vcount()), expected);
}
#[test]
fn rectangle_vcount_is_product(
sx in 1u32..15,
sy in 1u32..15,
) {
let g = triangular_lattice(&[sx, sy], false, false).expect("ok");
prop_assert_eq!(u64::from(g.vcount()), u64::from(sx) * u64::from(sy));
}
#[test]
fn directed_mutual_doubles_undirected_ecount(
sx in 1u32..10,
sy in 1u32..10,
) {
let undirected = triangular_lattice(&[sx, sy], false, false).expect("ok");
let mutual = triangular_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..10,
sy in 1u32..10,
) {
let undirected = triangular_lattice(&[sx, sy], false, false).expect("ok");
let unilateral = triangular_lattice(&[sx, sy], true, false).expect("ok");
prop_assert_eq!(unilateral.ecount(), undirected.ecount());
}
#[test]
fn directedness_flag_round_trips(
sx in 1u32..8,
sy in 1u32..8,
directed: bool,
mutual: bool,
) {
let g = triangular_lattice(&[sx, sy], directed, mutual).expect("ok");
prop_assert_eq!(g.is_directed(), directed);
}
#[test]
fn max_degree_at_most_six(
sx in 1u32..8,
sy in 1u32..8,
) {
let g = triangular_lattice(&[sx, sy], false, false).expect("ok");
for v in 0..g.vcount() {
prop_assert!(g.degree(v).unwrap() <= 6);
}
}
}
}