use crate::core::{Graph, IgraphError, IgraphResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DistanceMetric {
Euclidean,
Manhattan,
}
pub fn spatial_edge_lengths(
graph: &Graph,
coords: &[Vec<f64>],
metric: DistanceMetric,
) -> IgraphResult<Vec<f64>> {
let vcount = graph.vcount() as usize;
let ecount = graph.ecount();
if coords.len() != vcount {
return Err(IgraphError::InvalidArgument(format!(
"spatial_edge_lengths: coords length {} != vertex count {vcount}",
coords.len()
)));
}
if vcount > 0 {
let dim = coords[0].len();
if dim == 0 {
return Err(IgraphError::InvalidArgument(
"spatial_edge_lengths: coordinates must not be zero-dimensional".into(),
));
}
for (i, row) in coords.iter().enumerate().skip(1) {
if row.len() != dim {
return Err(IgraphError::InvalidArgument(format!(
"spatial_edge_lengths: coord row {i} has dimension {} but expected {dim}",
row.len()
)));
}
}
}
let mut lengths: Vec<f64> = Vec::with_capacity(ecount);
for eid in 0..ecount {
let eid_u32 = u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow"))?;
let (from, to) = graph.edge(eid_u32)?;
let from_coords = &coords[from as usize];
let to_coords = &coords[to as usize];
let length = match metric {
DistanceMetric::Euclidean => {
let sum_sq: f64 = from_coords
.iter()
.zip(to_coords.iter())
.map(|(&a, &b)| (a - b) * (a - b))
.sum();
sum_sq.sqrt()
}
DistanceMetric::Manhattan => from_coords
.iter()
.zip(to_coords.iter())
.map(|(&a, &b)| (a - b).abs())
.sum(),
};
lengths.push(length);
}
Ok(lengths)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph() {
let g = Graph::with_vertices(0);
let lengths = spatial_edge_lengths(&g, &[], DistanceMetric::Euclidean).unwrap();
assert!(lengths.is_empty());
}
#[test]
fn no_edges() {
let g = Graph::with_vertices(3);
let coords = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
assert!(lengths.is_empty());
}
#[test]
fn euclidean_2d() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(0, 2).unwrap();
let coords = vec![vec![0.0, 0.0], vec![3.0, 0.0], vec![0.0, 4.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
assert!((lengths[0] - 3.0).abs() < 1e-10);
assert!((lengths[1] - 5.0).abs() < 1e-10);
assert!((lengths[2] - 4.0).abs() < 1e-10);
}
#[test]
fn manhattan_2d() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(0, 2).unwrap();
let coords = vec![vec![0.0, 0.0], vec![3.0, 0.0], vec![0.0, 4.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Manhattan).unwrap();
assert!((lengths[0] - 3.0).abs() < 1e-10);
assert!((lengths[1] - 7.0).abs() < 1e-10); assert!((lengths[2] - 4.0).abs() < 1e-10);
}
#[test]
fn euclidean_3d() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let coords = vec![vec![0.0, 0.0, 0.0], vec![1.0, 2.0, 2.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
assert!((lengths[0] - 3.0).abs() < 1e-10); }
#[test]
fn manhattan_3d() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let coords = vec![vec![0.0, 0.0, 0.0], vec![1.0, 2.0, 3.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Manhattan).unwrap();
assert!((lengths[0] - 6.0).abs() < 1e-10); }
#[test]
fn self_loop_zero_distance() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
let coords = vec![vec![0.0, 0.0], vec![1.0, 0.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
assert!((lengths[0] - 0.0).abs() < 1e-10);
assert!((lengths[1] - 1.0).abs() < 1e-10);
}
#[test]
fn high_dimensional() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let coords = vec![vec![0.0; 5], vec![1.0; 5]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
assert!((lengths[0] - 5.0_f64.sqrt()).abs() < 1e-10);
}
#[test]
fn coords_length_mismatch() {
let g = Graph::with_vertices(3);
let coords = vec![vec![0.0], vec![1.0]]; let err = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn zero_dimensional_error() {
let g = Graph::with_vertices(2);
let coords = vec![vec![], vec![]];
let err = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn inconsistent_dimensions_error() {
let g = Graph::with_vertices(2);
let coords = vec![vec![0.0, 0.0], vec![1.0]]; let err = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn directed_graph() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(2, 0).unwrap();
let coords = vec![vec![0.0, 0.0], vec![3.0, 4.0], vec![1.0, 0.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
assert!((lengths[0] - 5.0).abs() < 1e-10); assert!((lengths[1] - 1.0).abs() < 1e-10); }
#[test]
fn single_dimension() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let coords = vec![vec![0.0], vec![5.0], vec![2.0]];
let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
assert!((lengths[0] - 5.0).abs() < 1e-10);
assert!((lengths[1] - 3.0).abs() < 1e-10);
}
}