Skip to content

Commit

Permalink
Merge branch 'main' into find-arbitrary-cycle
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] committed May 15, 2024
2 parents 208bcb4 + 12f8af5 commit d25903c
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 56 deletions.
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions releasenotes/notes/migrate-longest_path-7c11cf2c8ac9781f.yaml
Original file line number Diff line number Diff line change
@@ -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.
236 changes: 236 additions & 0 deletions rustworkx-core/src/dag_algo.rs
Original file line number Diff line number Diff line change
@@ -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<G> = <G as GraphBase>::NodeId;
type LongestPathResult<G, T, E> = Result<Option<(Vec<NodeId<G>>, 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<T, E>`. 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<NodeId<G>>, 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<i32>| Ok::<i32, &str>(*edge.weight());
/// let result = longest_path(&graph, weight_fn).unwrap();
/// assert_eq!(result, Some((vec![n0, n2], 3)));
/// ```
pub fn longest_path<G, F, T, E>(graph: G, mut weight_fn: F) -> LongestPathResult<G, T, E>
where
G: GraphProp<EdgeType = Directed>
+ IntoNodeIdentifiers
+ IntoNeighborsDirected
+ IntoEdgesDirected
+ Visitable
+ GraphBase<NodeId = NodeIndex>,
F: FnMut(G::EdgeRef) -> Result<T, E>,
T: Num + Zero + PartialOrd + Copy,
{
let mut path: Vec<NodeId<G>> = 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<NodeIndex, (T, NodeIndex)> = 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<NodeIndex> = 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::<i32, &str>(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::<i32, &str>(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<i32>| Ok::<i32, &str>(*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<i32>| Ok::<i32, &str>(*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<i32>| Ok::<i32, &str>(*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::<i32, &str>(*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<i32>| {
if *edge.weight() == 2 {
Err("Error: edge weight is 2")
} else {
Ok::<i32, &str>(*edge.weight())
}
};
let result = longest_path(&graph, weight_fn);
assert_eq!(result, Err("Error: edge weight is 2"));
}
}
2 changes: 1 addition & 1 deletion rustworkx-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
87 changes: 36 additions & 51 deletions src/dag_algo/longest_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`.
///
/// # 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<G::NodeId>, T)>` representing the longest path as a sequence of node indices and its total weight.
pub fn longest_path<F, T>(graph: &digraph::PyDiGraph, mut weight_fn: F) -> PyResult<(Vec<usize>, T)>
where
F: FnMut(usize, usize, &PyObject) -> PyResult<T>,
T: Num + Zero + PartialOrd + Copy,
{
let dag = &graph.graph;
let mut path: Vec<usize> = 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<T, PyErr> {
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<NodeIndex, (T, NodeIndex)> = 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<NodeIndex> = 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))
}

0 comments on commit d25903c

Please sign in to comment.