problemreductions/topology/
unit_disk_graph.rs

1//! Unit Disk Graph implementation.
2//!
3//! A unit disk graph (UDG) is a graph where vertices have positions in 2D space,
4//! and two vertices are connected if their distance is at most a threshold (radius).
5
6use super::graph::Graph;
7use serde::{Deserialize, Serialize};
8
9/// A unit disk graph with vertices at 2D positions.
10///
11/// Two vertices are connected by an edge if their Euclidean distance
12/// is at most the specified radius.
13///
14/// # Example
15///
16/// ```
17/// use problemreductions::topology::UnitDiskGraph;
18///
19/// // Create a UDG with 3 vertices at positions (0,0), (1,0), (3,0)
20/// // with unit radius (distance <= 1.0 creates an edge)
21/// let udg = UnitDiskGraph::new(
22///     vec![(0.0, 0.0), (1.0, 0.0), (3.0, 0.0)],
23///     1.0,
24/// );
25///
26/// // Vertices 0 and 1 are connected (distance = 1.0)
27/// // Vertex 2 is isolated (distance > 1.0 from both)
28/// assert!(udg.has_edge(0, 1));
29/// assert!(!udg.has_edge(0, 2));
30/// assert!(!udg.has_edge(1, 2));
31/// ```
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct UnitDiskGraph {
34    /// Positions of vertices as (x, y) coordinates.
35    positions: Vec<(f64, f64)>,
36    /// Radius threshold for edge creation.
37    radius: f64,
38    /// Precomputed edges.
39    edges: Vec<(usize, usize)>,
40}
41
42impl UnitDiskGraph {
43    /// Create a new unit disk graph.
44    ///
45    /// # Arguments
46    ///
47    /// * `positions` - 2D coordinates for each vertex
48    /// * `radius` - Maximum distance for an edge to exist
49    pub fn new(positions: Vec<(f64, f64)>, radius: f64) -> Self {
50        let n = positions.len();
51        let mut edges = Vec::new();
52
53        // Compute all edges based on distance
54        for i in 0..n {
55            for j in (i + 1)..n {
56                if Self::distance(&positions[i], &positions[j]) <= radius {
57                    edges.push((i, j));
58                }
59            }
60        }
61
62        Self {
63            positions,
64            radius,
65            edges,
66        }
67    }
68
69    /// Create a unit disk graph with radius 1.0.
70    pub fn unit(positions: Vec<(f64, f64)>) -> Self {
71        Self::new(positions, 1.0)
72    }
73
74    /// Compute Euclidean distance between two points.
75    fn distance(p1: &(f64, f64), p2: &(f64, f64)) -> f64 {
76        let dx = p1.0 - p2.0;
77        let dy = p1.1 - p2.1;
78        (dx * dx + dy * dy).sqrt()
79    }
80
81    /// Get the number of vertices.
82    pub fn num_vertices(&self) -> usize {
83        self.positions.len()
84    }
85
86    /// Get the number of edges.
87    pub fn num_edges(&self) -> usize {
88        self.edges.len()
89    }
90
91    /// Get the radius threshold.
92    pub fn radius(&self) -> f64 {
93        self.radius
94    }
95
96    /// Get the position of a vertex.
97    pub fn position(&self, v: usize) -> Option<(f64, f64)> {
98        self.positions.get(v).copied()
99    }
100
101    /// Get all positions.
102    pub fn positions(&self) -> &[(f64, f64)] {
103        &self.positions
104    }
105
106    /// Get all edges.
107    pub fn edges(&self) -> &[(usize, usize)] {
108        &self.edges
109    }
110
111    /// Check if an edge exists between two vertices.
112    pub fn has_edge(&self, u: usize, v: usize) -> bool {
113        let (u, v) = if u < v { (u, v) } else { (v, u) };
114        self.edges.contains(&(u, v))
115    }
116
117    /// Get the distance between two vertices.
118    pub fn vertex_distance(&self, u: usize, v: usize) -> Option<f64> {
119        match (self.positions.get(u), self.positions.get(v)) {
120            (Some(p1), Some(p2)) => Some(Self::distance(p1, p2)),
121            _ => None,
122        }
123    }
124
125    /// Get all neighbors of a vertex.
126    pub fn neighbors(&self, v: usize) -> Vec<usize> {
127        self.edges
128            .iter()
129            .filter_map(|&(u1, u2)| {
130                if u1 == v {
131                    Some(u2)
132                } else if u2 == v {
133                    Some(u1)
134                } else {
135                    None
136                }
137            })
138            .collect()
139    }
140
141    /// Get the degree of a vertex.
142    pub fn degree(&self, v: usize) -> usize {
143        self.neighbors(v).len()
144    }
145
146    /// Get the bounding box of all positions.
147    pub fn bounding_box(&self) -> Option<((f64, f64), (f64, f64))> {
148        if self.positions.is_empty() {
149            return None;
150        }
151
152        let min_x = self.positions.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
153        let max_x = self.positions.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
154        let min_y = self.positions.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
155        let max_y = self.positions.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);
156
157        Some(((min_x, min_y), (max_x, max_y)))
158    }
159
160    /// Create a unit disk graph on a regular grid.
161    ///
162    /// # Arguments
163    ///
164    /// * `rows` - Number of rows
165    /// * `cols` - Number of columns
166    /// * `spacing` - Distance between adjacent grid points
167    /// * `radius` - Edge creation threshold
168    pub fn grid(rows: usize, cols: usize, spacing: f64, radius: f64) -> Self {
169        let mut positions = Vec::with_capacity(rows * cols);
170        for r in 0..rows {
171            for c in 0..cols {
172                positions.push((c as f64 * spacing, r as f64 * spacing));
173            }
174        }
175        Self::new(positions, radius)
176    }
177}
178
179impl Graph for UnitDiskGraph {
180    fn num_vertices(&self) -> usize {
181        self.positions.len()
182    }
183
184    fn num_edges(&self) -> usize {
185        self.edges.len()
186    }
187
188    fn edges(&self) -> Vec<(usize, usize)> {
189        self.edges.clone()
190    }
191
192    fn has_edge(&self, u: usize, v: usize) -> bool {
193        let (u, v) = if u < v { (u, v) } else { (v, u) };
194        self.edges.contains(&(u, v))
195    }
196
197    fn neighbors(&self, v: usize) -> Vec<usize> {
198        self.edges
199            .iter()
200            .filter_map(|&(u1, u2)| {
201                if u1 == v {
202                    Some(u2)
203                } else if u2 == v {
204                    Some(u1)
205                } else {
206                    None
207                }
208            })
209            .collect()
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_udg_basic() {
219        let udg = UnitDiskGraph::new(
220            vec![(0.0, 0.0), (1.0, 0.0), (3.0, 0.0)],
221            1.0,
222        );
223        assert_eq!(udg.num_vertices(), 3);
224        assert_eq!(udg.num_edges(), 1); // Only 0-1 are within distance 1
225    }
226
227    #[test]
228    fn test_udg_unit() {
229        let udg = UnitDiskGraph::unit(vec![(0.0, 0.0), (0.5, 0.5)]);
230        assert_eq!(udg.radius(), 1.0);
231        // Distance is sqrt(0.5^2 + 0.5^2) ≈ 0.707 < 1, so connected
232        assert_eq!(udg.num_edges(), 1);
233    }
234
235    #[test]
236    fn test_udg_has_edge() {
237        let udg = UnitDiskGraph::new(
238            vec![(0.0, 0.0), (1.0, 0.0), (3.0, 0.0)],
239            1.0,
240        );
241        assert!(udg.has_edge(0, 1));
242        assert!(udg.has_edge(1, 0)); // Symmetric
243        assert!(!udg.has_edge(0, 2));
244        assert!(!udg.has_edge(1, 2));
245    }
246
247    #[test]
248    fn test_udg_neighbors() {
249        let udg = UnitDiskGraph::new(
250            vec![(0.0, 0.0), (1.0, 0.0), (0.5, 0.5)],
251            1.0,
252        );
253        let neighbors = udg.neighbors(0);
254        // 0 is within 1.0 of both 1 and 2
255        assert!(neighbors.contains(&1));
256        assert!(neighbors.contains(&2));
257    }
258
259    #[test]
260    fn test_udg_degree() {
261        let udg = UnitDiskGraph::new(
262            vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (5.0, 5.0)],
263            1.5,
264        );
265        // Vertex 0 is connected to 1 and 2
266        assert_eq!(udg.degree(0), 2);
267        // Vertex 3 is isolated
268        assert_eq!(udg.degree(3), 0);
269    }
270
271    #[test]
272    fn test_udg_vertex_distance() {
273        let udg = UnitDiskGraph::new(
274            vec![(0.0, 0.0), (3.0, 4.0)],
275            10.0,
276        );
277        let dist = udg.vertex_distance(0, 1);
278        assert_eq!(dist, Some(5.0)); // 3-4-5 triangle
279    }
280
281    #[test]
282    fn test_udg_position() {
283        let udg = UnitDiskGraph::new(
284            vec![(1.0, 2.0), (3.0, 4.0)],
285            1.0,
286        );
287        assert_eq!(udg.position(0), Some((1.0, 2.0)));
288        assert_eq!(udg.position(1), Some((3.0, 4.0)));
289        assert_eq!(udg.position(2), None);
290    }
291
292    #[test]
293    fn test_udg_bounding_box() {
294        let udg = UnitDiskGraph::new(
295            vec![(1.0, 2.0), (3.0, 4.0), (-1.0, 0.0)],
296            1.0,
297        );
298        let bbox = udg.bounding_box();
299        assert!(bbox.is_some());
300        let ((min_x, min_y), (max_x, max_y)) = bbox.unwrap();
301        assert_eq!(min_x, -1.0);
302        assert_eq!(max_x, 3.0);
303        assert_eq!(min_y, 0.0);
304        assert_eq!(max_y, 4.0);
305    }
306
307    #[test]
308    fn test_udg_empty_bounding_box() {
309        let udg = UnitDiskGraph::new(vec![], 1.0);
310        assert!(udg.bounding_box().is_none());
311    }
312
313    #[test]
314    fn test_udg_grid() {
315        let udg = UnitDiskGraph::grid(2, 3, 1.0, 1.0);
316        assert_eq!(udg.num_vertices(), 6);
317        // Grid with spacing 1.0 and radius 1.0: only horizontal/vertical neighbors connected
318        // Row 0: 0-1, 1-2
319        // Row 1: 3-4, 4-5
320        // Vertical: 0-3, 1-4, 2-5
321        assert_eq!(udg.num_edges(), 7);
322    }
323
324    #[test]
325    fn test_udg_grid_diagonal() {
326        // With radius > sqrt(2), diagonals are also connected
327        let udg = UnitDiskGraph::grid(2, 2, 1.0, 1.5);
328        assert_eq!(udg.num_vertices(), 4);
329        // All pairs are connected (4 edges: 0-1, 0-2, 0-3, 1-2, 1-3, 2-3)
330        // Actually: 0-1 (1.0), 0-2 (1.0), 1-3 (1.0), 2-3 (1.0), 0-3 (sqrt(2)≈1.41), 1-2 (sqrt(2)≈1.41)
331        assert_eq!(udg.num_edges(), 6);
332    }
333
334    #[test]
335    fn test_udg_edges_list() {
336        let udg = UnitDiskGraph::new(
337            vec![(0.0, 0.0), (1.0, 0.0)],
338            1.0,
339        );
340        let edges = udg.edges();
341        assert_eq!(edges.len(), 1);
342        assert_eq!(edges[0], (0, 1));
343    }
344}