Skip to main content

weavatrix_graph/algo/
dag_paths.rs

1use super::measure::Measure;
2use super::{WeightedPath, topological_sort_filtered};
3use crate::{GraphError, IndexGraphView, Result, String, Vec};
4use core::cmp::Ordering;
5
6/// Returns a deterministic longest path in a directed acyclic graph.
7///
8/// Every accepted edge has unit cost. An empty graph returns `None`.
9///
10/// # Errors
11///
12/// Returns an error when the graph is cyclic or the length overflows.
13pub fn dag_longest_path<G>(graph: &G) -> Result<Option<WeightedPath<G::Node>>>
14where
15    G: IndexGraphView,
16{
17    dag_weighted_longest_path(graph, |_| Some(1_u64))
18}
19
20/// Returns a deterministic longest path over accepted DAG edges.
21///
22/// # Errors
23///
24/// Returns an error when the accepted subgraph is cyclic or the length
25/// overflows.
26pub fn dag_longest_path_filtered<G, F>(
27    graph: &G,
28    mut allows_edge: F,
29) -> Result<Option<WeightedPath<G::Node>>>
30where
31    G: IndexGraphView,
32    F: FnMut(G::Edge) -> bool,
33{
34    dag_weighted_longest_path(graph, |edge| allows_edge(edge).then_some(1_u64))
35}
36
37/// Returns the number of edges in a deterministic longest DAG path.
38///
39/// # Errors
40///
41/// Returns an error when the graph is cyclic or the length overflows.
42pub fn dag_longest_path_length<G>(graph: &G) -> Result<Option<u64>>
43where
44    G: IndexGraphView,
45{
46    Ok(dag_longest_path(graph)?.map(|path| path.total_cost()))
47}
48
49/// Returns the number of accepted edges in a deterministic longest DAG path.
50///
51/// # Errors
52///
53/// Returns the same errors as [`dag_longest_path_filtered`].
54pub fn dag_longest_path_length_filtered<G, F>(graph: &G, allows_edge: F) -> Result<Option<u64>>
55where
56    G: IndexGraphView,
57    F: FnMut(G::Edge) -> bool,
58{
59    Ok(dag_longest_path_filtered(graph, allows_edge)?.map(|path| path.total_cost()))
60}
61
62/// Returns a deterministic maximum-cost path in a directed acyclic graph.
63///
64/// Returning `None` from `edge_cost` excludes that edge. Costs may be negative;
65/// a single node is the zero-cost result when every edge would reduce the cost.
66///
67/// # Errors
68///
69/// Returns an error for a cycle, a non-finite cost, or arithmetic overflow.
70pub fn dag_weighted_longest_path<G, Cost, F>(
71    graph: &G,
72    mut edge_cost: F,
73) -> Result<Option<WeightedPath<G::Node, Cost>>>
74where
75    G: IndexGraphView,
76    Cost: Measure,
77    F: FnMut(G::Edge) -> Option<Cost>,
78{
79    let weights = snapshot_weights(graph, &mut edge_cost)?;
80    let order = topological_sort_filtered(graph, |edge| weights[G::edge_slot(edge)].is_some())
81        .ok_or(GraphError::CyclicGraph {
82            algorithm: "DAG longest path",
83        })?;
84    let Some(&first) = order.first() else {
85        return Ok(None);
86    };
87    let mut distances = vec![None; graph.node_bound()];
88    let mut predecessors = vec![None; graph.node_bound()];
89    for &node in &order {
90        distances[G::node_slot(node)] = Some(Cost::zero());
91    }
92    for &source in &order {
93        let Some(source_cost) = distances[G::node_slot(source)] else {
94            continue;
95        };
96        for edge in graph.outgoing_edges(source) {
97            let Some(weight) = weights[G::edge_slot(edge)] else {
98                continue;
99            };
100            let Some(endpoints) = graph.edge_endpoints(edge) else {
101                continue;
102            };
103            let candidate =
104                source_cost
105                    .checked_add(weight)
106                    .ok_or(GraphError::ArithmeticOverflow {
107                        operation: "DAG longest path",
108                    })?;
109            let target_slot = G::node_slot(endpoints.target());
110            let current = distances[target_slot].unwrap_or_else(Cost::zero);
111            if candidate.compare(current) == Some(Ordering::Greater) {
112                distances[target_slot] = Some(candidate);
113                predecessors[target_slot] = Some(source);
114            }
115        }
116    }
117    let end = order.into_iter().skip(1).fold(first, |best, candidate| {
118        let best_cost = distances[G::node_slot(best)].unwrap_or_else(Cost::zero);
119        let candidate_cost = distances[G::node_slot(candidate)].unwrap_or_else(Cost::zero);
120        if candidate_cost.compare(best_cost) == Some(Ordering::Greater) {
121            candidate
122        } else {
123            best
124        }
125    });
126    let total_cost = distances[G::node_slot(end)].unwrap_or_else(Cost::zero);
127    Ok(Some(WeightedPath::from_parts(
128        reconstruct::<G>(end, &predecessors),
129        total_cost,
130    )))
131}
132
133/// Returns only the maximum path cost.
134///
135/// # Errors
136///
137/// Returns the same errors as [`dag_weighted_longest_path`].
138pub fn dag_weighted_longest_path_length<G, Cost, F>(graph: &G, edge_cost: F) -> Result<Option<Cost>>
139where
140    G: IndexGraphView,
141    Cost: Measure,
142    F: FnMut(G::Edge) -> Option<Cost>,
143{
144    Ok(dag_weighted_longest_path(graph, edge_cost)?.map(|path| path.total_cost()))
145}
146
147fn snapshot_weights<G, Cost, F>(graph: &G, edge_cost: &mut F) -> Result<Vec<Option<Cost>>>
148where
149    G: IndexGraphView,
150    Cost: Measure,
151    F: FnMut(G::Edge) -> Option<Cost>,
152{
153    let mut weights = vec![None; graph.edge_bound()];
154    for edge in graph.edge_indices() {
155        let Some(weight) = edge_cost(edge) else {
156            continue;
157        };
158        if !weight.is_valid() {
159            return Err(GraphError::InvalidAlgorithmParameter {
160                algorithm: "DAG longest path",
161                parameter: "edge_cost",
162                value: String::from("must be finite"),
163            });
164        }
165        weights[G::edge_slot(edge)] = Some(weight);
166    }
167    Ok(weights)
168}
169
170fn reconstruct<G>(mut node: G::Node, predecessors: &[Option<G::Node>]) -> Vec<G::Node>
171where
172    G: IndexGraphView,
173{
174    let mut path = vec![node];
175    while let Some(parent) = predecessors[G::node_slot(node)] {
176        node = parent;
177        path.push(node);
178    }
179    path.reverse();
180    path
181}