Skip to main content

rust_igraph/algorithms/spatial/
edge_lengths.rs

1//! Spatial edge lengths (ALGO-GEO-002).
2//!
3//! Counterpart of `igraph_spatial_edge_lengths()` from
4//! `references/igraph/src/spatial/edge_lengths.c`.
5//!
6//! Computes the Euclidean or Manhattan distance for each edge based on
7//! vertex coordinates in arbitrary dimensions.
8
9use crate::core::{Graph, IgraphError, IgraphResult};
10
11/// Distance metric for spatial edge length computation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DistanceMetric {
14    /// Euclidean (L2) distance.
15    Euclidean,
16    /// Manhattan (L1) distance.
17    Manhattan,
18}
19
20/// Compute edge lengths from spatial vertex coordinates.
21///
22/// Given a graph and a matrix of vertex coordinates (one row per vertex,
23/// arbitrary dimensionality), returns a `Vec<f64>` of length `ecount`
24/// where entry `i` is the spatial distance between the endpoints of
25/// edge `i` under the chosen metric.
26///
27/// # Errors
28///
29/// - `InvalidArgument` if `coords.len() != vcount`.
30/// - `InvalidArgument` if `dim == 0` and `vcount > 0`.
31///
32/// # Examples
33///
34/// ```
35/// use rust_igraph::{Graph, spatial_edge_lengths, DistanceMetric};
36///
37/// let mut g = Graph::with_vertices(3);
38/// g.add_edge(0, 1).unwrap();
39/// g.add_edge(1, 2).unwrap();
40///
41/// // 2D coords: (0,0), (3,0), (3,4)
42/// let coords = vec![vec![0.0, 0.0], vec![3.0, 0.0], vec![3.0, 4.0]];
43/// let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
44/// assert!((lengths[0] - 3.0).abs() < 1e-10);
45/// assert!((lengths[1] - 4.0).abs() < 1e-10);
46/// ```
47pub fn spatial_edge_lengths(
48    graph: &Graph,
49    coords: &[Vec<f64>],
50    metric: DistanceMetric,
51) -> IgraphResult<Vec<f64>> {
52    let vcount = graph.vcount() as usize;
53    let ecount = graph.ecount();
54
55    if coords.len() != vcount {
56        return Err(IgraphError::InvalidArgument(format!(
57            "spatial_edge_lengths: coords length {} != vertex count {vcount}",
58            coords.len()
59        )));
60    }
61
62    if vcount > 0 {
63        let dim = coords[0].len();
64        if dim == 0 {
65            return Err(IgraphError::InvalidArgument(
66                "spatial_edge_lengths: coordinates must not be zero-dimensional".into(),
67            ));
68        }
69        for (i, row) in coords.iter().enumerate().skip(1) {
70            if row.len() != dim {
71                return Err(IgraphError::InvalidArgument(format!(
72                    "spatial_edge_lengths: coord row {i} has dimension {} but expected {dim}",
73                    row.len()
74                )));
75            }
76        }
77    }
78
79    let mut lengths: Vec<f64> = Vec::with_capacity(ecount);
80
81    for eid in 0..ecount {
82        let eid_u32 = u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow"))?;
83        let (from, to) = graph.edge(eid_u32)?;
84        let from_coords = &coords[from as usize];
85        let to_coords = &coords[to as usize];
86
87        let length = match metric {
88            DistanceMetric::Euclidean => {
89                let sum_sq: f64 = from_coords
90                    .iter()
91                    .zip(to_coords.iter())
92                    .map(|(&a, &b)| (a - b) * (a - b))
93                    .sum();
94                sum_sq.sqrt()
95            }
96            DistanceMetric::Manhattan => from_coords
97                .iter()
98                .zip(to_coords.iter())
99                .map(|(&a, &b)| (a - b).abs())
100                .sum(),
101        };
102
103        lengths.push(length);
104    }
105
106    Ok(lengths)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn empty_graph() {
115        let g = Graph::with_vertices(0);
116        let lengths = spatial_edge_lengths(&g, &[], DistanceMetric::Euclidean).unwrap();
117        assert!(lengths.is_empty());
118    }
119
120    #[test]
121    fn no_edges() {
122        let g = Graph::with_vertices(3);
123        let coords = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0]];
124        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
125        assert!(lengths.is_empty());
126    }
127
128    #[test]
129    fn euclidean_2d() {
130        let mut g = Graph::with_vertices(3);
131        g.add_edge(0, 1).unwrap();
132        g.add_edge(1, 2).unwrap();
133        g.add_edge(0, 2).unwrap();
134        let coords = vec![vec![0.0, 0.0], vec![3.0, 0.0], vec![0.0, 4.0]];
135        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
136        assert!((lengths[0] - 3.0).abs() < 1e-10);
137        assert!((lengths[1] - 5.0).abs() < 1e-10);
138        assert!((lengths[2] - 4.0).abs() < 1e-10);
139    }
140
141    #[test]
142    fn manhattan_2d() {
143        let mut g = Graph::with_vertices(3);
144        g.add_edge(0, 1).unwrap();
145        g.add_edge(1, 2).unwrap();
146        g.add_edge(0, 2).unwrap();
147        let coords = vec![vec![0.0, 0.0], vec![3.0, 0.0], vec![0.0, 4.0]];
148        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Manhattan).unwrap();
149        assert!((lengths[0] - 3.0).abs() < 1e-10);
150        assert!((lengths[1] - 7.0).abs() < 1e-10); // |3-0| + |0-4| = 7
151        assert!((lengths[2] - 4.0).abs() < 1e-10);
152    }
153
154    #[test]
155    fn euclidean_3d() {
156        let mut g = Graph::with_vertices(2);
157        g.add_edge(0, 1).unwrap();
158        let coords = vec![vec![0.0, 0.0, 0.0], vec![1.0, 2.0, 2.0]];
159        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
160        assert!((lengths[0] - 3.0).abs() < 1e-10); // sqrt(1+4+4)=3
161    }
162
163    #[test]
164    fn manhattan_3d() {
165        let mut g = Graph::with_vertices(2);
166        g.add_edge(0, 1).unwrap();
167        let coords = vec![vec![0.0, 0.0, 0.0], vec![1.0, 2.0, 3.0]];
168        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Manhattan).unwrap();
169        assert!((lengths[0] - 6.0).abs() < 1e-10); // 1+2+3=6
170    }
171
172    #[test]
173    fn self_loop_zero_distance() {
174        let mut g = Graph::with_vertices(2);
175        g.add_edge(0, 0).unwrap();
176        g.add_edge(0, 1).unwrap();
177        let coords = vec![vec![0.0, 0.0], vec![1.0, 0.0]];
178        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
179        assert!((lengths[0] - 0.0).abs() < 1e-10);
180        assert!((lengths[1] - 1.0).abs() < 1e-10);
181    }
182
183    #[test]
184    fn high_dimensional() {
185        let mut g = Graph::with_vertices(2);
186        g.add_edge(0, 1).unwrap();
187        // 5D: distance = sqrt(5*1^2) = sqrt(5)
188        let coords = vec![vec![0.0; 5], vec![1.0; 5]];
189        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
190        assert!((lengths[0] - 5.0_f64.sqrt()).abs() < 1e-10);
191    }
192
193    #[test]
194    fn coords_length_mismatch() {
195        let g = Graph::with_vertices(3);
196        let coords = vec![vec![0.0], vec![1.0]]; // only 2, need 3
197        let err = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap_err();
198        assert!(matches!(err, IgraphError::InvalidArgument(_)));
199    }
200
201    #[test]
202    fn zero_dimensional_error() {
203        let g = Graph::with_vertices(2);
204        let coords = vec![vec![], vec![]];
205        let err = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap_err();
206        assert!(matches!(err, IgraphError::InvalidArgument(_)));
207    }
208
209    #[test]
210    fn inconsistent_dimensions_error() {
211        let g = Graph::with_vertices(2);
212        let coords = vec![vec![0.0, 0.0], vec![1.0]]; // dim mismatch
213        let err = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap_err();
214        assert!(matches!(err, IgraphError::InvalidArgument(_)));
215    }
216
217    #[test]
218    fn directed_graph() {
219        let mut g = Graph::new(3, true).unwrap();
220        g.add_edge(0, 1).unwrap();
221        g.add_edge(2, 0).unwrap();
222        let coords = vec![vec![0.0, 0.0], vec![3.0, 4.0], vec![1.0, 0.0]];
223        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
224        assert!((lengths[0] - 5.0).abs() < 1e-10); // 0→1: sqrt(9+16)
225        assert!((lengths[1] - 1.0).abs() < 1e-10); // 2→0: sqrt(1)
226    }
227
228    #[test]
229    fn single_dimension() {
230        let mut g = Graph::with_vertices(3);
231        g.add_edge(0, 1).unwrap();
232        g.add_edge(1, 2).unwrap();
233        let coords = vec![vec![0.0], vec![5.0], vec![2.0]];
234        let lengths = spatial_edge_lengths(&g, &coords, DistanceMetric::Euclidean).unwrap();
235        assert!((lengths[0] - 5.0).abs() < 1e-10);
236        assert!((lengths[1] - 3.0).abs() < 1e-10);
237    }
238}