Skip to main content

weavatrix_graph/algo/walk/
dijkstra.rs

1use super::super::measure::Measure;
2use super::super::traversal::{Direction, for_each_adjacent};
3use crate::{GraphError, IndexGraphView, Result, String, Vec};
4use alloc::collections::BinaryHeap;
5use core::cmp::Ordering;
6
7#[derive(Debug, Clone, Copy)]
8struct Scored<Cost> {
9    cost: Cost,
10    slot: usize,
11}
12
13impl<Cost: Measure> PartialEq for Scored<Cost> {
14    fn eq(&self, other: &Self) -> bool {
15        self.slot == other.slot && self.cost.compare(other.cost) == Some(Ordering::Equal)
16    }
17}
18
19impl<Cost: Measure> Eq for Scored<Cost> {}
20
21impl<Cost: Measure> PartialOrd for Scored<Cost> {
22    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
23        Some(self.cmp(other))
24    }
25}
26
27impl<Cost: Measure> Ord for Scored<Cost> {
28    fn cmp(&self, other: &Self) -> Ordering {
29        other
30            .cost
31            .compare(self.cost)
32            .unwrap_or(Ordering::Equal)
33            .then_with(|| other.slot.cmp(&self.slot))
34    }
35}
36
37/// Reusable distances, predecessors, node slots, and priority queue.
38#[derive(Debug, Clone)]
39pub struct DijkstraWorkspace<Node, Cost> {
40    distances: Vec<Option<Cost>>,
41    predecessors: Vec<Option<Node>>,
42    nodes: Vec<Option<Node>>,
43    queue: BinaryHeap<Scored<Cost>>,
44}
45
46impl<Node, Cost: Measure> DijkstraWorkspace<Node, Cost> {
47    #[must_use]
48    pub const fn new() -> Self {
49        Self {
50            distances: Vec::new(),
51            predecessors: Vec::new(),
52            nodes: Vec::new(),
53            queue: BinaryHeap::new(),
54        }
55    }
56
57    #[must_use]
58    pub fn distance_at(&self, slot: usize) -> Option<Cost>
59    where
60        Cost: Copy,
61    {
62        self.distances.get(slot).copied().flatten()
63    }
64
65    #[must_use]
66    pub fn predecessor_at(&self, slot: usize) -> Option<Node>
67    where
68        Node: Copy,
69    {
70        self.predecessors.get(slot).copied().flatten()
71    }
72
73    #[must_use]
74    pub fn path_to<G>(&self, source: Node, target: Node) -> Option<Vec<Node>>
75    where
76        G: IndexGraphView<Node = Node>,
77        Node: Copy + Eq,
78        Cost: Copy,
79    {
80        self.distance_at(G::node_slot(target))?;
81        let mut path = vec![target];
82        let mut cursor = target;
83        while cursor != source {
84            cursor = self.predecessor_at(G::node_slot(cursor))?;
85            path.push(cursor);
86        }
87        path.reverse();
88        Some(path)
89    }
90
91    fn begin<G>(&mut self, graph: &G, source: Node)
92    where
93        G: IndexGraphView<Node = Node>,
94        Node: Copy,
95        Cost: Measure,
96    {
97        let bound = graph.node_bound();
98        self.distances.resize(bound, None);
99        self.predecessors.resize(bound, None);
100        self.nodes.resize(bound, None);
101        self.distances.fill(None);
102        self.predecessors.fill(None);
103        self.nodes.fill(None);
104        self.queue.clear();
105        for node in graph.node_indices() {
106            self.nodes[G::node_slot(node)] = Some(node);
107        }
108        if graph.contains_node(source) {
109            let slot = G::node_slot(source);
110            self.distances[slot] = Some(Cost::zero());
111            self.queue.push(Scored {
112                cost: Cost::zero(),
113                slot,
114            });
115        }
116    }
117}
118
119impl<Node, Cost: Measure> Default for DijkstraWorkspace<Node, Cost> {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125/// Lazy Dijkstra settlement order with reusable allocation storage.
126pub struct Dijkstra<'graph, 'workspace, G, Cost, F>
127where
128    G: IndexGraphView,
129{
130    graph: &'graph G,
131    workspace: &'workspace mut DijkstraWorkspace<G::Node, Cost>,
132    direction: Direction,
133    edge_cost: F,
134    failed: bool,
135}
136
137impl<'graph, 'workspace, G, Cost, F> Dijkstra<'graph, 'workspace, G, Cost, F>
138where
139    G: IndexGraphView,
140    Cost: Measure,
141    F: FnMut(G::Edge) -> Option<Cost>,
142{
143    #[must_use]
144    pub fn filtered(
145        graph: &'graph G,
146        source: G::Node,
147        direction: Direction,
148        workspace: &'workspace mut DijkstraWorkspace<G::Node, Cost>,
149        edge_cost: F,
150    ) -> Self {
151        workspace.begin(graph, source);
152        Self {
153            graph,
154            workspace,
155            direction,
156            edge_cost,
157            failed: false,
158        }
159    }
160
161    fn fail(&mut self, error: GraphError) -> Result<(G::Node, Cost)> {
162        self.failed = true;
163        self.workspace.queue.clear();
164        Err(error)
165    }
166}
167
168impl<G, Cost, F> Iterator for Dijkstra<'_, '_, G, Cost, F>
169where
170    G: IndexGraphView,
171    Cost: Measure,
172    F: FnMut(G::Edge) -> Option<Cost>,
173{
174    type Item = Result<(G::Node, Cost)>;
175
176    fn next(&mut self) -> Option<Self::Item> {
177        if self.failed {
178            return None;
179        }
180        while let Some(Scored { cost, slot }) = self.workspace.queue.pop() {
181            let Some(known) = self.workspace.distances.get(slot).copied().flatten() else {
182                continue;
183            };
184            if known.compare(cost) != Some(Ordering::Equal) {
185                continue;
186            }
187            let Some(node) = self.workspace.nodes.get(slot).copied().flatten() else {
188                continue;
189            };
190            let mut error = None;
191            let workspace = &mut *self.workspace;
192            for_each_adjacent(
193                self.graph,
194                node,
195                self.direction,
196                &mut |_| true,
197                |edge, neighbor| {
198                    if error.is_some() {
199                        return;
200                    }
201                    let Some(weight) = (self.edge_cost)(edge) else {
202                        return;
203                    };
204                    if !weight.is_valid() || weight.is_negative() {
205                        error = Some(invalid_weight());
206                        return;
207                    }
208                    let Some(candidate) = cost.checked_add(weight) else {
209                        error = Some(GraphError::ArithmeticOverflow {
210                            operation: "generic Dijkstra path cost",
211                        });
212                        return;
213                    };
214                    let neighbor_slot = G::node_slot(neighbor);
215                    let improves = workspace.distances[neighbor_slot]
216                        .is_none_or(|known| candidate.compare(known) == Some(Ordering::Less));
217                    if improves {
218                        workspace.distances[neighbor_slot] = Some(candidate);
219                        workspace.predecessors[neighbor_slot] = Some(node);
220                        workspace.queue.push(Scored {
221                            cost: candidate,
222                            slot: neighbor_slot,
223                        });
224                    }
225                },
226            );
227            if let Some(error) = error {
228                return Some(self.fail(error));
229            }
230            return Some(Ok((node, cost)));
231        }
232        None
233    }
234}
235
236pub fn dijkstra_iter<'graph, 'workspace, G, Cost, F>(
237    graph: &'graph G,
238    source: G::Node,
239    workspace: &'workspace mut DijkstraWorkspace<G::Node, Cost>,
240    mut edge_cost: F,
241) -> impl Iterator<Item = Result<(G::Node, Cost)>> + 'workspace
242where
243    'graph: 'workspace,
244    G: IndexGraphView + 'graph,
245    Cost: Measure + 'workspace,
246    F: FnMut(G::Edge) -> Cost + 'workspace,
247{
248    Dijkstra::filtered(graph, source, Direction::Outgoing, workspace, move |edge| {
249        Some(edge_cost(edge))
250    })
251}
252
253#[must_use]
254pub fn dijkstra_iter_filtered<'graph, 'workspace, G, Cost, F>(
255    graph: &'graph G,
256    source: G::Node,
257    direction: Direction,
258    workspace: &'workspace mut DijkstraWorkspace<G::Node, Cost>,
259    edge_cost: F,
260) -> Dijkstra<'graph, 'workspace, G, Cost, F>
261where
262    G: IndexGraphView,
263    Cost: Measure,
264    F: FnMut(G::Edge) -> Option<Cost>,
265{
266    Dijkstra::filtered(graph, source, direction, workspace, edge_cost)
267}
268
269fn invalid_weight() -> GraphError {
270    GraphError::InvalidAlgorithmParameter {
271        algorithm: "Dijkstra",
272        parameter: "edge_cost",
273        value: String::from("must be finite and non-negative"),
274    }
275}