Skip to main content

geographdb_core/algorithms/
astar.rs

1//! A* Pathfinding Algorithm for CFG with 3D Spatial Heuristics
2
3use std::cmp::Ordering;
4use std::collections::{BinaryHeap, HashMap, HashSet};
5
6#[derive(Debug, Clone)]
7pub struct CfgGraphNode {
8    pub id: u64,
9    pub x: f32,
10    pub y: f32,
11    pub z: f32,
12    pub successors: Vec<u64>,
13}
14
15#[derive(Debug, Clone)]
16pub struct CfgPath {
17    pub node_ids: Vec<u64>,
18    pub total_cost: f32,
19    pub heuristic_cost: f32,
20    pub actual_cost: f32,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
24struct AStarNode {
25    node_id: u64,
26    f_score: f32,
27    g_score: f32,
28}
29
30impl Eq for AStarNode {}
31
32impl Ord for AStarNode {
33    fn cmp(&self, other: &Self) -> Ordering {
34        other
35            .f_score
36            .partial_cmp(&self.f_score)
37            .unwrap_or(Ordering::Equal)
38    }
39}
40
41impl PartialOrd for AStarNode {
42    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
43        Some(self.cmp(other))
44    }
45}
46
47#[inline]
48fn euclidean_distance(x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> f32 {
49    let dx = x2 - x1;
50    let dy = y2 - y1;
51    let dz = z2 - z1;
52    (dx * dx + dy * dy + dz * dz).sqrt()
53}
54
55pub fn astar_find_path(nodes: &[CfgGraphNode], start_id: u64, goal_id: u64) -> Option<CfgPath> {
56    eprintln!(
57        "DEBUG astar_find_path: start_id={}, goal_id={}, nodes.len()={}",
58        start_id,
59        goal_id,
60        nodes.len()
61    );
62    let node_map: HashMap<u64, &CfgGraphNode> = nodes.iter().map(|n| (n.id, n)).collect();
63    let start_node = node_map.get(&start_id)?;
64
65    // Special case: goal_id = u64::MAX means "find path to farthest node"
66    let actual_goal_id = if goal_id == u64::MAX {
67        // Find the farthest node from start
68        let mut farthest_id = start_id;
69        let mut max_dist = 0.0f32;
70        for (&id, node) in &node_map {
71            if id == start_id {
72                continue;
73            }
74            let dist = euclidean_distance(
75                start_node.x,
76                start_node.y,
77                start_node.z,
78                node.x,
79                node.y,
80                node.z,
81            );
82            if dist > max_dist {
83                max_dist = dist;
84                farthest_id = id;
85            }
86        }
87        eprintln!(
88            "DEBUG: start_id={}, goal_id=MAX, farthest_id={}, max_dist={}",
89            start_id, farthest_id, max_dist
90        );
91        farthest_id
92    } else {
93        goal_id
94    };
95
96    let goal_node = node_map.get(&actual_goal_id)?;
97
98    let mut open_set = BinaryHeap::new();
99    let mut in_open_set: HashSet<u64> = HashSet::new();
100    let mut closed_set: HashSet<u64> = HashSet::new();
101    let mut g_score: HashMap<u64, f32> = HashMap::new();
102    let mut came_from: HashMap<u64, u64> = HashMap::new();
103
104    let initial_h = euclidean_distance(
105        start_node.x,
106        start_node.y,
107        start_node.z,
108        goal_node.x,
109        goal_node.y,
110        goal_node.z,
111    );
112    g_score.insert(start_id, 0.0);
113    open_set.push(AStarNode {
114        node_id: start_id,
115        f_score: initial_h,
116        g_score: 0.0,
117    });
118    in_open_set.insert(start_id);
119
120    while let Some(current) = open_set.pop() {
121        let current_id = current.node_id;
122        if closed_set.contains(&current_id) {
123            continue;
124        }
125        if current_id == actual_goal_id {
126            let mut path = vec![actual_goal_id];
127            let mut cur = actual_goal_id;
128            while let Some(&prev) = came_from.get(&cur) {
129                path.push(prev);
130                cur = prev;
131            }
132            path.reverse();
133            return Some(CfgPath {
134                node_ids: path,
135                total_cost: current.f_score,
136                heuristic_cost: initial_h,
137                actual_cost: current.g_score,
138            });
139        }
140        in_open_set.remove(&current_id);
141        closed_set.insert(current_id);
142        let current_node = node_map.get(&current_id)?;
143        eprintln!(
144            "DEBUG: Processing node_id={}, successors={:?}",
145            current_id, current_node.successors
146        );
147        for &succ_id in &current_node.successors {
148            if closed_set.contains(&succ_id) {
149                continue;
150            }
151            let succ_node = match node_map.get(&succ_id) {
152                Some(n) => n,
153                None => continue,
154            };
155            let tentative_g = current.g_score + 1.0;
156            let existing_g = g_score.get(&succ_id).copied().unwrap_or(f32::INFINITY);
157            if tentative_g < existing_g {
158                came_from.insert(succ_id, current_id);
159                g_score.insert(succ_id, tentative_g);
160                let h = euclidean_distance(
161                    succ_node.x,
162                    succ_node.y,
163                    succ_node.z,
164                    goal_node.x,
165                    goal_node.y,
166                    goal_node.z,
167                );
168                if !in_open_set.contains(&succ_id) {
169                    open_set.push(AStarNode {
170                        node_id: succ_id,
171                        f_score: tentative_g + h,
172                        g_score: tentative_g,
173                    });
174                    in_open_set.insert(succ_id);
175                }
176            }
177        }
178    }
179    None
180}
181
182pub fn astar_find_k_paths(
183    nodes: &[CfgGraphNode],
184    start_id: u64,
185    goal_id: u64,
186    k: usize,
187) -> Vec<CfgPath> {
188    let mut paths: Vec<CfgPath> = Vec::new();
189    if let Some(first) = astar_find_path(nodes, start_id, goal_id) {
190        paths.push(first);
191    } else {
192        return paths;
193    }
194    let mut candidates: Vec<CfgPath> = Vec::new();
195    for i in 0..(k - 1).min(paths.len()) {
196        let base = &paths[i];
197        for j in 0..(base.node_ids.len() - 1) {
198            let spur = base.node_ids[j];
199            let modified: Vec<CfgGraphNode> = nodes
200                .iter()
201                .map(|n| {
202                    let mut m = n.clone();
203                    if j > 0 && n.id == base.node_ids[j - 1] {
204                        m.successors.retain(|&s| s != spur);
205                    }
206                    m
207                })
208                .collect();
209            if let Some(spur_path) = astar_find_path(&modified, spur, goal_id) {
210                let mut combined: Vec<u64> = base.node_ids[..=j].to_vec();
211                combined.extend_from_slice(&spur_path.node_ids[1..]);
212                let cp = CfgPath {
213                    node_ids: combined,
214                    total_cost: spur_path.total_cost + j as f32,
215                    heuristic_cost: spur_path.heuristic_cost,
216                    actual_cost: spur_path.actual_cost + j as f32,
217                };
218                if !candidates.iter().any(|p| p.node_ids == cp.node_ids) {
219                    candidates.push(cp);
220                }
221            }
222        }
223        candidates.sort_by(|a, b| {
224            a.total_cost
225                .partial_cmp(&b.total_cost)
226                .unwrap_or(Ordering::Equal)
227        });
228        if let Some(best) = candidates.pop() {
229            paths.push(best);
230        }
231    }
232    paths
233}
234
235#[derive(Debug, Clone)]
236pub struct PathComplexity {
237    pub path_length: usize,
238    pub max_loop_depth: u32,
239    pub branch_count: usize,
240    pub total_spatial_distance: f32,
241    pub heuristic_efficiency: f32,
242}
243
244pub fn analyze_path_complexity(path: &CfgPath, nodes: &[CfgGraphNode]) -> PathComplexity {
245    let node_map: HashMap<u64, &CfgGraphNode> = nodes.iter().map(|n| (n.id, n)).collect();
246    let mut max_ld = 0.0f32;
247    let mut bc = 0;
248    let mut tsd = 0.0f32;
249    for i in 0..path.node_ids.len() {
250        if let Some(&n) = node_map.get(&path.node_ids[i]) {
251            max_ld = max_ld.max(n.y);
252            if n.z > 0.0 {
253                bc += 1;
254            }
255            if i > 0 {
256                if let Some(&p) = node_map.get(&path.node_ids[i - 1]) {
257                    tsd += euclidean_distance(p.x, p.y, p.z, n.x, n.y, n.z);
258                }
259            }
260        }
261    }
262    PathComplexity {
263        path_length: path.node_ids.len(),
264        max_loop_depth: max_ld as u32,
265        branch_count: bc,
266        total_spatial_distance: tsd,
267        heuristic_efficiency: if path.heuristic_cost > 0.0 {
268            path.actual_cost / path.heuristic_cost
269        } else {
270            1.0
271        },
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    #[test]
279    fn test_astar_simple() {
280        let n = vec![
281            CfgGraphNode {
282                id: 0,
283                x: 0.0,
284                y: 0.0,
285                z: 0.0,
286                successors: vec![1],
287            },
288            CfgGraphNode {
289                id: 1,
290                x: 1.0,
291                y: 0.0,
292                z: 0.0,
293                successors: vec![2],
294            },
295            CfgGraphNode {
296                id: 2,
297                x: 2.0,
298                y: 0.0,
299                z: 0.0,
300                successors: vec![3],
301            },
302            CfgGraphNode {
303                id: 3,
304                x: 3.0,
305                y: 0.0,
306                z: 0.0,
307                successors: vec![],
308            },
309        ];
310        assert_eq!(
311            astar_find_path(&n, 0, 3).unwrap().node_ids,
312            vec![0, 1, 2, 3]
313        );
314    }
315    #[test]
316    fn test_astar_branch() {
317        let n = vec![
318            CfgGraphNode {
319                id: 0,
320                x: 0.0,
321                y: 0.0,
322                z: 0.0,
323                successors: vec![1, 2],
324            },
325            CfgGraphNode {
326                id: 1,
327                x: 1.0,
328                y: 0.0,
329                z: 1.0,
330                successors: vec![3],
331            },
332            CfgGraphNode {
333                id: 2,
334                x: 1.0,
335                y: 0.0,
336                z: 0.0,
337                successors: vec![3],
338            },
339            CfgGraphNode {
340                id: 3,
341                x: 2.0,
342                y: 0.0,
343                z: 0.0,
344                successors: vec![],
345            },
346        ];
347        assert!(astar_find_path(&n, 0, 3).is_some());
348    }
349    #[test]
350    fn test_astar_nopath() {
351        let n = vec![
352            CfgGraphNode {
353                id: 0,
354                x: 0.0,
355                y: 0.0,
356                z: 0.0,
357                successors: vec![1],
358            },
359            CfgGraphNode {
360                id: 1,
361                x: 1.0,
362                y: 0.0,
363                z: 0.0,
364                successors: vec![],
365            },
366        ];
367        assert!(astar_find_path(&n, 0, 2).is_none());
368    }
369    #[test]
370    fn test_astar_kpaths() {
371        let n = vec![
372            CfgGraphNode {
373                id: 0,
374                x: 0.0,
375                y: 0.0,
376                z: 0.0,
377                successors: vec![1, 2, 3],
378            },
379            CfgGraphNode {
380                id: 1,
381                x: 1.0,
382                y: 0.0,
383                z: 0.0,
384                successors: vec![4],
385            },
386            CfgGraphNode {
387                id: 2,
388                x: 1.0,
389                y: 0.0,
390                z: 0.0,
391                successors: vec![4],
392            },
393            CfgGraphNode {
394                id: 3,
395                x: 1.0,
396                y: 0.0,
397                z: 0.0,
398                successors: vec![4],
399            },
400            CfgGraphNode {
401                id: 4,
402                x: 2.0,
403                y: 0.0,
404                z: 0.0,
405                successors: vec![],
406            },
407        ];
408        let paths = astar_find_k_paths(&n, 0, 4, 3);
409        assert!(paths.len() >= 2 && paths.len() <= 3);
410    }
411}