rust_igraph/algorithms/spatial/
edge_lengths.rs1use crate::core::{Graph, IgraphError, IgraphResult};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DistanceMetric {
14 Euclidean,
16 Manhattan,
18}
19
20pub 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); 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); }
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); }
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 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]]; 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]]; 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); assert!((lengths[1] - 1.0).abs() < 1e-10); }
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}