use crate::core::{Graph, IgraphError, IgraphResult};
const TOLERANCE: f64 = 128.0 * f64::EPSILON;
#[allow(clippy::many_single_char_names, clippy::similar_names)]
pub fn relative_neighborhood_graph(points: &[Vec<f64>]) -> IgraphResult<Graph> {
let n = points.len();
let dim = if n == 0 { 0 } else { points[0].len() };
if n > 0 {
if dim == 0 {
return Err(IgraphError::InvalidArgument(
"relative_neighborhood_graph: points must not be zero-dimensional".into(),
));
}
for (i, row) in points.iter().enumerate().skip(1) {
if row.len() != dim {
return Err(IgraphError::InvalidArgument(format!(
"relative_neighborhood_graph: point row {i} has dimension {} but expected {dim}",
row.len()
)));
}
}
}
let n_u32 = u32::try_from(n).map_err(|_| {
IgraphError::InvalidArgument("relative_neighborhood_graph: too many points".into())
})?;
let mut graph = Graph::with_vertices(n_u32);
let tol = 1.0 - TOLERANCE;
let tol_sq = tol * tol;
for i in 0..n {
let a = &points[i];
for j in (i + 1)..n {
let b = &points[j];
let mut dist_sq = 0.0_f64;
for d in 0..dim {
let t = a[d] - b[d];
dist_sq += t * t;
}
let threshold = dist_sq * tol_sq;
let mut empty = true;
for (k, c) in points.iter().enumerate() {
if k == i || k == j {
continue;
}
let mut to_a_sq = 0.0_f64;
let mut to_b_sq = 0.0_f64;
for d in 0..dim {
let ta = c[d] - a[d];
to_a_sq += ta * ta;
let tb = c[d] - b[d];
to_b_sq += tb * tb;
}
if to_a_sq < threshold && to_b_sq < threshold {
empty = false;
break;
}
}
if empty {
let u = u32::try_from(i).map_err(|_| {
IgraphError::Internal("relative_neighborhood_graph: vertex id overflow")
})?;
let v = u32::try_from(j).map_err(|_| {
IgraphError::Internal("relative_neighborhood_graph: vertex id overflow")
})?;
graph.add_edge(u, v)?;
}
}
}
Ok(graph)
}
#[cfg(test)]
mod tests {
use super::*;
fn edge_set(g: &Graph) -> Vec<(u32, u32)> {
let mut edges: Vec<(u32, u32)> = (0..g.ecount())
.map(|e| {
let (u, v) = g
.edge(u32::try_from(e).expect("edge id fits in u32"))
.expect("edge");
(u.min(v), u.max(v))
})
.collect();
edges.sort_unstable();
edges
}
#[test]
fn empty_point_set() {
let g = relative_neighborhood_graph(&[]).expect("empty ok");
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
}
#[test]
fn single_point() {
let g = relative_neighborhood_graph(&[vec![0.5, 0.5]]).expect("singleton ok");
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn two_points_always_connected() {
let g = relative_neighborhood_graph(&[vec![0.0, 0.0], vec![3.0, 4.0]]).expect("pair ok");
assert_eq!(g.vcount(), 2);
assert_eq!(edge_set(&g), vec![(0, 1)]);
}
#[test]
fn equilateral_triangle_open_lune_keeps_all() {
let h = 3.0_f64.sqrt() / 2.0;
let pts = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.5, h]];
let g = relative_neighborhood_graph(&pts).expect("triangle ok");
assert_eq!(edge_set(&g), vec![(0, 1), (0, 2), (1, 2)]);
}
#[test]
fn collinear_midpoint_breaks_long_edge() {
let pts = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![2.0, 0.0]];
let g = relative_neighborhood_graph(&pts).expect("collinear ok");
assert_eq!(edge_set(&g), vec![(0, 1), (1, 2)]);
}
#[test]
fn unit_square_keeps_sides_drops_diagonals() {
let pts = vec![
vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0], ];
let g = relative_neighborhood_graph(&pts).expect("square ok");
assert_eq!(edge_set(&g), vec![(0, 1), (0, 2), (1, 3), (2, 3)]);
}
#[test]
fn three_dimensional_points() {
let pts = vec![vec![0.0, 0.0, 0.0], vec![1.0, 1.0, 1.0]];
let g = relative_neighborhood_graph(&pts).expect("3d ok");
assert_eq!(edge_set(&g), vec![(0, 1)]);
}
#[test]
fn zero_dimensional_error() {
let err = relative_neighborhood_graph(&[vec![], vec![]]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn inconsistent_dimensions_error() {
let err = relative_neighborhood_graph(&[vec![0.0, 0.0], vec![1.0]]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[allow(clippy::unreadable_literal, clippy::excessive_precision)]
#[test]
fn triangular_lattice_c_anchor() {
let pts = vec![
vec![0.5, 2.5980762113533159402911695122588085504142078807156],
vec![0.0, 1.7320508075688772935274463415058723669428052538104],
vec![1.0, 1.7320508075688772935274463415058723669428052538104],
vec![-0.5, 0.86602540378443864676372317075293618347140262690519],
vec![0.5, 0.86602540378443864676372317075293618347140262690519],
vec![1.5, 0.86602540378443864676372317075293618347140262690519],
vec![-1.0, 0.0],
vec![0.0, 0.0],
vec![1.0, 0.0],
vec![2.0, 0.0],
];
let g = relative_neighborhood_graph(&pts).expect("lattice ok");
let expected: Vec<(u32, u32)> = vec![
(0, 1),
(0, 2),
(1, 2),
(1, 3),
(1, 4),
(2, 4),
(2, 5),
(3, 4),
(3, 6),
(3, 7),
(4, 5),
(4, 7),
(4, 8),
(5, 8),
(5, 9),
(6, 7),
(7, 8),
(8, 9),
];
assert_eq!(g.vcount(), 10);
assert_eq!(edge_set(&g), expected);
}
}