diff --git a/releasenotes/notes/migrate-longest_path-7c11cf2c8ac9781f.yaml b/releasenotes/notes/migrate-longest_path-7c11cf2c8ac9781f.yaml new file mode 100644 index 000000000..85cbca792 --- /dev/null +++ b/releasenotes/notes/migrate-longest_path-7c11cf2c8ac9781f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added a new module ``dag_algo`` to rustworkx-core which contains a new function ``longest_path`` + function to rustworkx-core. Previously the ``longest_path`` functionality for DAGs was only exposed + via the Python interface. Now Rust users can take advantage of this functionality in rustworkx-core. + \ No newline at end of file diff --git a/rustworkx-core/src/dag_algo.rs b/rustworkx-core/src/dag_algo.rs new file mode 100644 index 000000000..0e1a0b9e0 --- /dev/null +++ b/rustworkx-core/src/dag_algo.rs @@ -0,0 +1,236 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +use hashbrown::HashMap; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::visit::{ + EdgeRef, GraphBase, GraphProp, IntoEdgesDirected, IntoNeighborsDirected, IntoNodeIdentifiers, + Visitable, +}; +use petgraph::Directed; + +use num_traits::{Num, Zero}; + +// Type aliases for readability +type NodeId = ::NodeId; +type LongestPathResult = Result>, T)>, E>; + +/// Calculates the longest path in a directed acyclic graph (DAG). +/// +/// This function computes the longest path by weight in a given DAG. It will return the longest path +/// along with its total weight, or `None` if the graph contains cycles which make the longest path +/// computation undefined. +/// +/// # Arguments +/// * `graph`: Reference to a directed graph. +/// * `weight_fn` - An input callable that will be passed the `EdgeRef` for each edge in the graph. +/// The callable should return the weight of the edge as `Result`. The weight must be a type that implements +/// `Num`, `Zero`, `PartialOrd`, and `Copy`. +/// +/// # Type Parameters +/// * `G`: Type of the graph. Must be a directed graph. +/// * `F`: Type of the weight function. +/// * `T`: The type of the edge weight. Must implement `Num`, `Zero`, `PartialOrd`, and `Copy`. +/// * `E`: The type of the error that the weight function can return. +/// +/// # Returns +/// * `None` if the graph contains a cycle. +/// * `Some((Vec>, T))` representing the longest path as a sequence of nodes and its total weight. +/// * `Err(E)` if there is an error computing the weight of any edge. +/// +/// # Example +/// ``` +/// use petgraph::graph::DiGraph; +/// use petgraph::graph::NodeIndex; +/// use petgraph::Directed; +/// use rustworkx_core::dag_algo::longest_path; +/// +/// let mut graph: DiGraph<(), i32> = DiGraph::new(); +/// let n0 = graph.add_node(()); +/// let n1 = graph.add_node(()); +/// let n2 = graph.add_node(()); +/// graph.add_edge(n0, n1, 1); +/// graph.add_edge(n0, n2, 3); +/// graph.add_edge(n1, n2, 1); +/// +/// let weight_fn = |edge: petgraph::graph::EdgeReference| Ok::(*edge.weight()); +/// let result = longest_path(&graph, weight_fn).unwrap(); +/// assert_eq!(result, Some((vec![n0, n2], 3))); +/// ``` +pub fn longest_path(graph: G, mut weight_fn: F) -> LongestPathResult +where + G: GraphProp + + IntoNodeIdentifiers + + IntoNeighborsDirected + + IntoEdgesDirected + + Visitable + + GraphBase, + F: FnMut(G::EdgeRef) -> Result, + T: Num + Zero + PartialOrd + Copy, +{ + let mut path: Vec> = Vec::new(); + let nodes = match algo::toposort(graph, None) { + Ok(nodes) => nodes, + Err(_) => return Ok(None), // Return None if the graph contains a cycle + }; + + if nodes.is_empty() { + return Ok(Some((path, T::zero()))); + } + + let mut dist: HashMap = HashMap::with_capacity(nodes.len()); // Stores the distance and the previous node + + // Iterate over nodes in topological order + for node in nodes { + let parents = graph.edges_directed(node, petgraph::Direction::Incoming); + let mut incoming_path: Vec<(T, NodeIndex)> = Vec::new(); // Stores the distance and the previous node for each parent + for p_edge in parents { + let p_node = p_edge.source(); + let weight: T = weight_fn(p_edge)?; + let length = dist[&p_node].0 + weight; + incoming_path.push((length, p_node)); + } + // Determine the maximum distance and corresponding parent node + let max_path: (T, NodeIndex) = incoming_path + .into_iter() + .max_by(|a, b| a.0.partial_cmp(&b.0).unwrap()) + .unwrap_or((T::zero(), node)); // If there are no incoming edges, the distance is zero + + // Store the maximum distance and the corresponding parent node for the current node + dist.insert(node, max_path); + } + let (first, _) = dist + .iter() + .max_by(|a, b| a.1.partial_cmp(b.1).unwrap()) + .unwrap(); + let mut v = *first; + let mut u: Option = None; + // Backtrack from this node to find the path + while u.map_or(true, |u| u != v) { + path.push(v); + u = Some(v); + v = dist[&v].1; + } + path.reverse(); // Reverse the path to get the correct order + let path_weight = dist[first].0; // The total weight of the longest path + + Ok(Some((path, path_weight))) +} + +#[cfg(test)] +mod test_longest_path { + use super::*; + use petgraph::graph::DiGraph; + use petgraph::stable_graph::StableDiGraph; + + #[test] + fn test_empty_graph() { + let graph: DiGraph<(), ()> = DiGraph::new(); + let weight_fn = |_: petgraph::graph::EdgeReference<()>| Ok::(0); + let result = longest_path(&graph, weight_fn); + assert_eq!(result, Ok(Some((vec![], 0)))); + } + + #[test] + fn test_single_node_graph() { + let mut graph: DiGraph<(), ()> = DiGraph::new(); + let n0 = graph.add_node(()); + let weight_fn = |_: petgraph::graph::EdgeReference<()>| Ok::(0); + let result = longest_path(&graph, weight_fn); + assert_eq!(result, Ok(Some((vec![n0], 0)))); + } + + #[test] + fn test_dag_with_multiple_paths() { + let mut graph: DiGraph<(), i32> = DiGraph::new(); + let n0 = graph.add_node(()); + let n1 = graph.add_node(()); + let n2 = graph.add_node(()); + let n3 = graph.add_node(()); + let n4 = graph.add_node(()); + let n5 = graph.add_node(()); + graph.add_edge(n0, n1, 3); + graph.add_edge(n0, n2, 2); + graph.add_edge(n1, n2, 1); + graph.add_edge(n1, n3, 4); + graph.add_edge(n2, n3, 2); + graph.add_edge(n3, n4, 2); + graph.add_edge(n2, n5, 1); + graph.add_edge(n4, n5, 3); + let weight_fn = |edge: petgraph::graph::EdgeReference| Ok::(*edge.weight()); + let result = longest_path(&graph, weight_fn); + assert_eq!(result, Ok(Some((vec![n0, n1, n3, n4, n5], 12)))); + } + + #[test] + fn test_graph_with_cycle() { + let mut graph: DiGraph<(), i32> = DiGraph::new(); + let n0 = graph.add_node(()); + let n1 = graph.add_node(()); + graph.add_edge(n0, n1, 1); + graph.add_edge(n1, n0, 1); // Creates a cycle + + let weight_fn = |edge: petgraph::graph::EdgeReference| Ok::(*edge.weight()); + let result = longest_path(&graph, weight_fn); + assert_eq!(result, Ok(None)); + } + + #[test] + fn test_negative_weights() { + let mut graph: DiGraph<(), i32> = DiGraph::new(); + let n0 = graph.add_node(()); + let n1 = graph.add_node(()); + let n2 = graph.add_node(()); + graph.add_edge(n0, n1, -1); + graph.add_edge(n0, n2, 2); + graph.add_edge(n1, n2, -2); + let weight_fn = |edge: petgraph::graph::EdgeReference| Ok::(*edge.weight()); + let result = longest_path(&graph, weight_fn); + assert_eq!(result, Ok(Some((vec![n0, n2], 2)))); + } + + #[test] + fn test_longest_path_in_stable_digraph() { + let mut graph: StableDiGraph<(), i32> = StableDiGraph::new(); + let n0 = graph.add_node(()); + let n1 = graph.add_node(()); + let n2 = graph.add_node(()); + graph.add_edge(n0, n1, 1); + graph.add_edge(n0, n2, 3); + graph.add_edge(n1, n2, 1); + let weight_fn = + |edge: petgraph::stable_graph::EdgeReference<'_, i32>| Ok::(*edge.weight()); + let result = longest_path(&graph, weight_fn); + assert_eq!(result, Ok(Some((vec![n0, n2], 3)))); + } + + #[test] + fn test_error_handling() { + let mut graph: DiGraph<(), i32> = DiGraph::new(); + let n0 = graph.add_node(()); + let n1 = graph.add_node(()); + let n2 = graph.add_node(()); + graph.add_edge(n0, n1, 1); + graph.add_edge(n0, n2, 2); + graph.add_edge(n1, n2, 1); + let weight_fn = |edge: petgraph::graph::EdgeReference| { + if *edge.weight() == 2 { + Err("Error: edge weight is 2") + } else { + Ok::(*edge.weight()) + } + }; + let result = longest_path(&graph, weight_fn); + assert_eq!(result, Err("Error: edge weight is 2")); + } +} diff --git a/rustworkx-core/src/lib.rs b/rustworkx-core/src/lib.rs index e5d38eb58..f3d08309c 100644 --- a/rustworkx-core/src/lib.rs +++ b/rustworkx-core/src/lib.rs @@ -76,9 +76,9 @@ pub mod centrality; /// Module for coloring algorithms. pub mod coloring; pub mod connectivity; +pub mod dag_algo; pub mod generators; pub mod line_graph; - /// Module for maximum weight matching algorithms. pub mod max_weight_matching; pub mod planar; diff --git a/src/dag_algo/longest_path.rs b/src/dag_algo/longest_path.rs index a05a714f3..bdb1cf91a 100644 --- a/src/dag_algo/longest_path.rs +++ b/src/dag_algo/longest_path.rs @@ -11,69 +11,54 @@ // under the License. use crate::{digraph, DAGHasCycle}; +use rustworkx_core::dag_algo::longest_path as core_longest_path; -use hashbrown::HashMap; +use petgraph::stable_graph::{EdgeReference, NodeIndex}; +use petgraph::visit::EdgeRef; use pyo3::prelude::*; -use petgraph::algo; -use petgraph::prelude::*; -use petgraph::stable_graph::NodeIndex; - use num_traits::{Num, Zero}; +/// Calculate the longest path in a directed acyclic graph (DAG). +/// +/// This function interfaces with the Python `PyDiGraph` object to compute the longest path +/// using the provided weight function. +/// +/// # Arguments +/// * `graph`: Reference to a `PyDiGraph` object. +/// * `weight_fn`: A callable that takes the source node index, target node index, and the weight +/// object and returns the weight of the edge as a `PyResult`. +/// +/// # Type Parameters +/// * `F`: Type of the weight function. +/// * `T`: The type of the edge weight. Must implement `Num`, `Zero`, `PartialOrd`, and `Copy`. +/// +/// # Returns +/// * `PyResult<(Vec, T)>` representing the longest path as a sequence of node indices and its total weight. pub fn longest_path(graph: &digraph::PyDiGraph, mut weight_fn: F) -> PyResult<(Vec, T)> where F: FnMut(usize, usize, &PyObject) -> PyResult, T: Num + Zero + PartialOrd + Copy, { let dag = &graph.graph; - let mut path: Vec = Vec::new(); - let nodes = match algo::toposort(&graph.graph, None) { - Ok(nodes) => nodes, - Err(_err) => return Err(DAGHasCycle::new_err("Sort encountered a cycle")), + + // Create a new weight function that matches the required signature + let edge_cost = |edge_ref: EdgeReference<'_, PyObject>| -> Result { + let source = edge_ref.source().index(); + let target = edge_ref.target().index(); + let weight = edge_ref.weight(); + weight_fn(source, target, weight) }; - if nodes.is_empty() { - return Ok((path, T::zero())); - } - let mut dist: HashMap = HashMap::new(); - for node in nodes { - let parents = dag.edges_directed(node, petgraph::Direction::Incoming); - let mut us: Vec<(T, NodeIndex)> = Vec::new(); - for p_edge in parents { - let p_node = p_edge.source(); - let weight: T = weight_fn(p_node.index(), p_edge.target().index(), p_edge.weight())?; - let length = dist[&p_node].0 + weight; - us.push((length, p_node)); - } - let maxu: (T, NodeIndex) = if !us.is_empty() { - *us.iter() - .max_by(|a, b| { - let weight_a = a.0; - let weight_b = b.0; - weight_a.partial_cmp(&weight_b).unwrap() - }) - .unwrap() - } else { - (T::zero(), node) - }; - dist.insert(node, maxu); - } - let first = dist - .keys() - .max_by(|a, b| dist[*a].partial_cmp(&dist[*b]).unwrap()) - .unwrap(); - let mut v = *first; - let mut u: Option = None; - while match u { - Some(u) => u != v, - None => true, - } { - path.push(v.index()); - u = Some(v); - v = dist[&v].1; - } - path.reverse(); - let path_weight = dist[first].0; + + let (path, path_weight) = match core_longest_path(dag, edge_cost) { + Ok(Some((path, path_weight))) => ( + path.into_iter().map(NodeIndex::index).collect(), + path_weight, + ), + Ok(None) => return Err(DAGHasCycle::new_err("The graph contains a cycle")), + Err(e) => return Err(e), + }; + Ok((path, path_weight)) }