diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9594184e37..b006237e30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,74 @@ with Qiskit; the general guidelines and advice still apply here. In addition to the general guidelines there are specific details for contributing to retworkx, these are documented below. +### Making changes to the code + +Retworkx is implemented primarily in Rust with a thin layer of Python. +Because of that, most of your code changes will involve modifications to +Rust files in `src`. To understand which files you need to change, we invite +you for an overview of our simplified source tree: + +``` +├── src/ +│ ├── lib.rs +│ ├── tiny.rs +│ ├── large/ +│ │ ├── mod.rs +│ │ ├── pure_rust_code.rs +│ │ └── more_pure_rust_code.rs +``` + +#### Module exports in `lib.rs` + +To add new functions, you will need to export them in `lib.rs`. `lib.rs` will +import functions defined in Rust modules (see the next section), and export +them to Python using `m.add_wrapped(wrap_pyfunction!(your_new_function))?;` + +#### Adding and changing functions in modules + +To add and change functions, you will need to modify module files. Modules contain pyfunctions +that will be exported, and can be defined either as a single file such as `tiny.rs` or as a +directory with `mod.rs` such as `large/`. + +Rust functions that are exported to Python are annotated with `#[pyfunction]`. The +annotation gives them power to interact both with the Python interpreter and pure +Rust code. To change an existing function, search for its name and edit the code that +already exists. + +If you want to add a new function, find the module you'd like to insert it in +or create a new one like `your_module.rs`. Then, start with the boilerplate bellow: + +```rust +/// Docstring containing description of the function +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn your_new_function( + py: Python, + graph: &graph::PyGraph, +) -> PyResult<()> { + /* Your code goes here */ +} +``` + +> __NOTE:__ If you create a new `your_module.rs`, remember to declare and import it in `lib.rs`: +> ```rust +> mod your_module; +> use your_module::*; +> ``` + +#### Module directories: when a single file is not enough + +Sometimes you will find that it is hard to organize a module in a tiny +file like `tiny.rs`. In those cases, we suggest moving the files to a directory +and splitting them following the structure of `large/`. + +Module directories have a `mod.rs` file containing the pyfunctions. The pyfunctions +in that file then delegate most of logic by importing and calling pure Rust code from +`pure_rust_code.rs` and `more_pure_rust_code.rs`. + +> __NOTE:__ Do you still have questions about making your contribution? +> Contact us at the [\#retworkx channel in Qiskit Slack](https://qiskit.slack.com/messages/retworkx/) + ### Tests Once you've made a code change, it is important to verify that your change diff --git a/src/coloring.rs b/src/coloring.rs new file mode 100644 index 0000000000..63a7146b7a --- /dev/null +++ b/src/coloring.rs @@ -0,0 +1,78 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use crate::graph; + +use ahash::RandomState; +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexMap; +use std::cmp::Reverse; + +use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::Python; + +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::visit::NodeCount; + +use rayon::prelude::*; + +/// Color a PyGraph using a largest_first strategy greedy graph coloring. +/// +/// :param PyGraph: The input PyGraph object to color +/// +/// :returns: A dictionary where keys are node indices and the value is +/// the color +/// :rtype: dict +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +fn graph_greedy_color( + py: Python, + graph: &graph::PyGraph, +) -> PyResult { + let mut colors: IndexMap = + IndexMap::with_hasher(RandomState::default()); + let mut node_vec: Vec = graph.graph.node_indices().collect(); + let mut sort_map: HashMap = + HashMap::with_capacity(graph.node_count()); + for k in node_vec.iter() { + sort_map.insert(*k, graph.graph.edges(*k).count()); + } + node_vec.par_sort_by_key(|k| Reverse(sort_map.get(k))); + for u_index in node_vec { + let mut neighbor_colors: HashSet = HashSet::new(); + for edge in graph.graph.edges(u_index) { + let target = edge.target().index(); + let existing_color = match colors.get(&target) { + Some(node) => node, + None => continue, + }; + neighbor_colors.insert(*existing_color); + } + let mut count: usize = 0; + loop { + if !neighbor_colors.contains(&count) { + break; + } + count += 1; + } + colors.insert(u_index.index(), count); + } + let out_dict = PyDict::new(py); + for (index, color) in colors { + out_dict.set_item(index, color)?; + } + Ok(out_dict.into()) +} diff --git a/src/connectivity/core_number.rs b/src/connectivity/core_number.rs new file mode 100644 index 0000000000..d089d05b26 --- /dev/null +++ b/src/connectivity/core_number.rs @@ -0,0 +1,96 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use hashbrown::{HashMap, HashSet}; + +use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::Python; + +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::EdgeType; + +use rayon::prelude::*; + +pub fn core_number( + py: Python, + graph: &StableGraph, +) -> PyResult +where + Ty: EdgeType, +{ + let node_num = graph.node_count(); + if node_num == 0 { + return Ok(PyDict::new(py).into()); + } + + let mut cores: HashMap = HashMap::with_capacity(node_num); + let mut node_vec: Vec = graph.node_indices().collect(); + let mut degree_map: HashMap = + HashMap::with_capacity(node_num); + let mut nbrs: HashMap> = + HashMap::with_capacity(node_num); + let mut node_pos: HashMap = + HashMap::with_capacity(node_num); + + for k in node_vec.iter() { + let k_nbrs: HashSet = + graph.neighbors_undirected(*k).collect(); + let k_deg = k_nbrs.len(); + + nbrs.insert(*k, k_nbrs); + cores.insert(*k, k_deg); + degree_map.insert(*k, k_deg); + } + node_vec.par_sort_by_key(|k| degree_map.get(k)); + + let mut bin_boundaries: Vec = + Vec::with_capacity(degree_map[&node_vec[node_num - 1]] + 1); + bin_boundaries.push(0); + let mut curr_degree = 0; + for (i, v) in node_vec.iter().enumerate() { + node_pos.insert(*v, i); + let v_degree = degree_map[v]; + if v_degree > curr_degree { + for _ in 0..v_degree - curr_degree { + bin_boundaries.push(i); + } + curr_degree = v_degree; + } + } + + for v_ind in 0..node_vec.len() { + let v = node_vec[v_ind]; + let v_nbrs = nbrs[&v].clone(); + for u in v_nbrs { + if cores[&u] > cores[&v] { + nbrs.get_mut(&u).unwrap().remove(&v); + let pos = node_pos[&u]; + let bin_start = bin_boundaries[cores[&u]]; + *node_pos.get_mut(&u).unwrap() = bin_start; + *node_pos.get_mut(&node_vec[bin_start]).unwrap() = pos; + node_vec.swap(bin_start, pos); + bin_boundaries[cores[&u]] += 1; + *cores.get_mut(&u).unwrap() -= 1; + } + } + } + + let out_dict = PyDict::new(py); + for (v_index, core) in cores { + out_dict.set_item(v_index.index(), core)?; + } + Ok(out_dict.into()) +} diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs new file mode 100644 index 0000000000..2d5ca10d67 --- /dev/null +++ b/src/connectivity/mod.rs @@ -0,0 +1,627 @@ +// 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. + +#![allow(clippy::float_cmp)] + +mod core_number; + +use super::{ + digraph, get_edge_iter_with_weights, graph, weight_callable, InvalidNode, + NullGraph, +}; + +use hashbrown::{HashMap, HashSet}; +use std::collections::BTreeSet; + +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::visit::NodeCount; + +use ndarray::prelude::*; +use numpy::IntoPyArray; + +use crate::iterators::EdgeList; + +/// Return a list of cycles which form a basis for cycles of a given PyGraph +/// +/// A basis for cycles of a graph is a minimal collection of +/// cycles such that any cycle in the graph can be written +/// as a sum of cycles in the basis. Here summation of cycles +/// is defined as the exclusive or of the edges. +/// +/// This is adapted from algorithm CACM 491 [1]_. +/// +/// :param PyGraph graph: The graph to find the cycle basis in +/// :param int root: Optional index for starting node for basis +/// +/// :returns: A list of cycle lists. Each list is a list of node ids which +/// forms a cycle (loop) in the input graph +/// :rtype: list +/// +/// .. [1] Paton, K. An algorithm for finding a fundamental set of +/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. +#[pyfunction] +#[pyo3(text_signature = "(graph, /, root=None)")] +pub fn cycle_basis( + graph: &graph::PyGraph, + root: Option, +) -> Vec> { + let mut root_node = root; + let mut graph_nodes: HashSet = + graph.graph.node_indices().collect(); + let mut cycles: Vec> = Vec::new(); + while !graph_nodes.is_empty() { + let temp_value: NodeIndex; + // If root_node is not set get an arbitrary node from the set of graph + // nodes we've not "examined" + let root_index = match root_node { + Some(root_value) => NodeIndex::new(root_value), + None => { + temp_value = *graph_nodes.iter().next().unwrap(); + graph_nodes.remove(&temp_value); + temp_value + } + }; + // Stack (ie "pushdown list") of vertices already in the spanning tree + let mut stack: Vec = vec![root_index]; + // Map of node index to predecessor node index + let mut pred: HashMap = HashMap::new(); + pred.insert(root_index, root_index); + // Set of examined nodes during this iteration + let mut used: HashMap> = HashMap::new(); + used.insert(root_index, HashSet::new()); + // Walk the spanning tree + while !stack.is_empty() { + // Use the last element added so that cycles are easier to find + let z = stack.pop().unwrap(); + for neighbor in graph.graph.neighbors(z) { + // A new node was encountered: + if !used.contains_key(&neighbor) { + pred.insert(neighbor, z); + stack.push(neighbor); + let mut temp_set: HashSet = HashSet::new(); + temp_set.insert(z); + used.insert(neighbor, temp_set); + // A self loop: + } else if z == neighbor { + let cycle: Vec = vec![z.index()]; + cycles.push(cycle); + // A cycle was found: + } else if !used.get(&z).unwrap().contains(&neighbor) { + let pn = used.get(&neighbor).unwrap(); + let mut cycle: Vec = vec![neighbor, z]; + let mut p = pred.get(&z).unwrap(); + while !pn.contains(p) { + cycle.push(*p); + p = pred.get(p).unwrap(); + } + cycle.push(*p); + cycles.push(cycle.iter().map(|x| x.index()).collect()); + let neighbor_set = used.get_mut(&neighbor).unwrap(); + neighbor_set.insert(z); + } + } + } + let mut temp_hashset: HashSet = HashSet::new(); + for key in pred.keys() { + temp_hashset.insert(*key); + } + graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect(); + root_node = None; + } + cycles +} + +/// Compute the strongly connected components for a directed graph +/// +/// This function is implemented using Kosaraju's algorithm +/// +/// :param PyDiGraph graph: The input graph to find the strongly connected +/// components for. +/// +/// :return: A list of list of node ids for strongly connected components +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn strongly_connected_components( + graph: &digraph::PyDiGraph, +) -> Vec> { + algo::kosaraju_scc(graph) + .iter() + .map(|x| x.iter().map(|id| id.index()).collect()) + .collect() +} + +/// Return the first cycle encountered during DFS of a given PyDiGraph, +/// empty list is returned if no cycle is found +/// +/// :param PyDiGraph graph: The graph to find the cycle in +/// :param int source: Optional index to find a cycle for. If not specified an +/// arbitrary node will be selected from the graph. +/// +/// :returns: A list describing the cycle. The index of node ids which +/// forms a cycle (loop) in the input graph +/// :rtype: EdgeList +#[pyfunction] +#[pyo3(text_signature = "(graph, /, source=None)")] +pub fn digraph_find_cycle( + graph: &digraph::PyDiGraph, + source: Option, +) -> EdgeList { + let mut graph_nodes: HashSet = + graph.graph.node_indices().collect(); + let mut cycle: Vec<(usize, usize)> = + Vec::with_capacity(graph.graph.edge_count()); + let temp_value: NodeIndex; + // If source is not set get an arbitrary node from the set of graph + // nodes we've not "examined" + let source_index = match source { + Some(source_value) => NodeIndex::new(source_value), + None => { + temp_value = *graph_nodes.iter().next().unwrap(); + graph_nodes.remove(&temp_value); + temp_value + } + }; + + // Stack (ie "pushdown list") of vertices already in the spanning tree + let mut stack: Vec = vec![source_index]; + // map to store parent of a node + let mut pred: HashMap = HashMap::new(); + // a node is in the visiting set if at least one of its child is unexamined + let mut visiting = HashSet::new(); + // a node is in visited set if all of its children have been examined + let mut visited = HashSet::new(); + while !stack.is_empty() { + let mut z = *stack.last().unwrap(); + visiting.insert(z); + + let children = graph + .graph + .neighbors_directed(z, petgraph::Direction::Outgoing); + + for child in children { + //cycle is found + if visiting.contains(&child) { + cycle.push((z.index(), child.index())); + //backtrack + loop { + if z == child { + cycle.reverse(); + break; + } + cycle.push((pred[&z].index(), z.index())); + z = pred[&z]; + } + return EdgeList { edges: cycle }; + } + //if an unexplored node is encountered + if !visited.contains(&child) { + stack.push(child); + pred.insert(child, z); + } + } + + let top = *stack.last().unwrap(); + //if no further children and explored, move to visited + if top.index() == z.index() { + stack.pop(); + visiting.remove(&z); + visited.insert(z); + } + } + EdgeList { edges: cycle } +} + +/// Find the number of weakly connected components in a DAG. +/// +/// :param PyDiGraph graph: The graph to find the number of weakly connected +/// components on +/// +/// :returns: The number of weakly connected components in the DAG +/// :rtype: int +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +fn number_weakly_connected_components(graph: &digraph::PyDiGraph) -> usize { + algo::connected_components(graph) +} + +/// Find the weakly connected components in a directed graph +/// +/// :param PyDiGraph graph: The graph to find the weakly connected components +/// in +/// +/// :returns: A list of sets where each set it a weakly connected component of +/// the graph +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn weakly_connected_components( + graph: &digraph::PyDiGraph, +) -> Vec> { + let mut seen: HashSet = + HashSet::with_capacity(graph.node_count()); + let mut out_vec: Vec> = Vec::new(); + for node in graph.graph.node_indices() { + if !seen.contains(&node) { + // BFS node generator + let mut component_set: BTreeSet = BTreeSet::new(); + let mut bfs_seen: HashSet = HashSet::new(); + let mut next_level: HashSet = HashSet::new(); + next_level.insert(node); + while !next_level.is_empty() { + let this_level = next_level; + next_level = HashSet::new(); + for bfs_node in this_level { + if !bfs_seen.contains(&bfs_node) { + component_set.insert(bfs_node.index()); + bfs_seen.insert(bfs_node); + for neighbor in + graph.graph.neighbors_undirected(bfs_node) + { + next_level.insert(neighbor); + } + } + } + } + out_vec.push(component_set); + seen.extend(bfs_seen); + } + } + out_vec +} + +/// Check if the graph is weakly connected +/// +/// :param PyDiGraph graph: The graph to check if it is weakly connected +/// +/// :returns: Whether the graph is weakly connected or not +/// :rtype: bool +/// +/// :raises NullGraph: If an empty graph is passed in +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn is_weakly_connected(graph: &digraph::PyDiGraph) -> PyResult { + if graph.graph.node_count() == 0 { + return Err(NullGraph::new_err("Invalid operation on a NullGraph")); + } + Ok(weakly_connected_components(graph)[0].len() == graph.graph.node_count()) +} + +/// Return the adjacency matrix for a PyDiGraph object +/// +/// In the case where there are multiple edges between nodes the value in the +/// output matrix will be the sum of the edges' weights. +/// +/// :param PyDiGraph graph: The DiGraph used to generate the adjacency matrix +/// from +/// :param callable weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// dag_adjacency_matrix(dag, weight_fn: lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// dag_adjacency_matrix(dag, weight_fn: lambda x: float(x)) +/// +/// to cast the edge object as a float as the weight. If this is not +/// specified a default value (either ``default_weight`` or 1) will be used +/// for all edges. +/// :param float default_weight: If ``weight_fn`` is not used this can be +/// optionally used to specify a default weight to use for all edges. +/// +/// :return: The adjacency matrix for the input dag as a numpy array +/// :rtype: numpy.ndarray +#[pyfunction(default_weight = "1.0")] +#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0)")] +pub fn digraph_adjacency_matrix( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: Option, + default_weight: f64, +) -> PyResult { + let n = graph.node_count(); + let mut matrix = Array2::::zeros((n, n)); + for (i, j, weight) in get_edge_iter_with_weights(graph) { + let edge_weight = + weight_callable(py, &weight_fn, &weight, default_weight)?; + matrix[[i, j]] += edge_weight; + } + Ok(matrix.into_pyarray(py).into()) +} + +/// Return the adjacency matrix for a PyGraph class +/// +/// In the case where there are multiple edges between nodes the value in the +/// output matrix will be the sum of the edges' weights. +/// +/// :param PyGraph graph: The graph used to generate the adjacency matrix from +/// :param weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// graph_adjacency_matrix(graph, weight_fn: lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// graph_adjacency_matrix(graph, weight_fn: lambda x: float(x)) +/// +/// to cast the edge object as a float as the weight. If this is not +/// specified a default value (either ``default_weight`` or 1) will be used +/// for all edges. +/// :param float default_weight: If ``weight_fn`` is not used this can be +/// optionally used to specify a default weight to use for all edges. +/// +/// :return: The adjacency matrix for the input dag as a numpy array +/// :rtype: numpy.ndarray +#[pyfunction(default_weight = "1.0")] +#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0)")] +pub fn graph_adjacency_matrix( + py: Python, + graph: &graph::PyGraph, + weight_fn: Option, + default_weight: f64, +) -> PyResult { + let n = graph.node_count(); + let mut matrix = Array2::::zeros((n, n)); + for (i, j, weight) in get_edge_iter_with_weights(graph) { + let edge_weight = + weight_callable(py, &weight_fn, &weight, default_weight)?; + matrix[[i, j]] += edge_weight; + matrix[[j, i]] += edge_weight; + } + Ok(matrix.into_pyarray(py).into()) +} + +/// Compute the complement of a graph. +/// +/// :param PyGraph graph: The graph to be used. +/// +/// :returns: The complement of the graph. +/// :rtype: PyGraph +/// +/// .. note:: +/// +/// Parallel edges and self-loops are never created, +/// even if the :attr:`~retworkx.PyGraph.multigraph` +/// attribute is set to ``True`` +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn graph_complement( + py: Python, + graph: &graph::PyGraph, +) -> PyResult { + let mut complement_graph = graph.clone(); // keep same node indexes + complement_graph.graph.clear_edges(); + + for node_a in graph.graph.node_indices() { + let old_neighbors: HashSet = + graph.graph.neighbors(node_a).collect(); + for node_b in graph.graph.node_indices() { + if node_a != node_b + && !old_neighbors.contains(&node_b) + && (!complement_graph.multigraph + || !complement_graph + .has_edge(node_a.index(), node_b.index())) + { + // avoid creating parallel edges in multigraph + complement_graph.add_edge( + node_a.index(), + node_b.index(), + py.None(), + )?; + } + } + } + Ok(complement_graph) +} + +/// Compute the complement of a graph. +/// +/// :param PyDiGraph graph: The graph to be used. +/// +/// :returns: The complement of the graph. +/// :rtype: :class:`~retworkx.PyDiGraph` +/// +/// .. note:: +/// +/// Parallel edges and self-loops are never created, +/// even if the :attr:`~retworkx.PyDiGraph.multigraph` +/// attribute is set to ``True`` +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn digraph_complement( + py: Python, + graph: &digraph::PyDiGraph, +) -> PyResult { + let mut complement_graph = graph.clone(); // keep same node indexes + complement_graph.graph.clear_edges(); + + for node_a in graph.graph.node_indices() { + let old_neighbors: HashSet = graph + .graph + .neighbors_directed(node_a, petgraph::Direction::Outgoing) + .collect(); + for node_b in graph.graph.node_indices() { + if node_a != node_b && !old_neighbors.contains(&node_b) { + complement_graph.add_edge( + node_a.index(), + node_b.index(), + py.None(), + )?; + } + } + } + + Ok(complement_graph) +} + +/// Return all simple paths between 2 nodes in a PyGraph object +/// +/// A simple path is a path with no repeated nodes. +/// +/// :param PyGraph graph: The graph to find the path in +/// :param int from: The node index to find the paths from +/// :param int to: The node index to find the paths to +/// :param int min_depth: The minimum depth of the path to include in the output +/// list of paths. By default all paths are included regardless of depth, +/// setting to 0 will behave like the default. +/// :param int cutoff: The maximum depth of path to include in the output list +/// of paths. By default includes all paths regardless of depth, setting to +/// 0 will behave like default. +/// +/// :returns: A list of lists where each inner list is a path of node indices +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, from, to, /, min=None, cutoff=None)")] +fn graph_all_simple_paths( + graph: &graph::PyGraph, + from: usize, + to: usize, + min_depth: Option, + cutoff: Option, +) -> PyResult>> { + let from_index = NodeIndex::new(from); + if !graph.graph.contains_node(from_index) { + return Err(InvalidNode::new_err( + "The input index for 'from' is not a valid node index", + )); + } + let to_index = NodeIndex::new(to); + if !graph.graph.contains_node(to_index) { + return Err(InvalidNode::new_err( + "The input index for 'to' is not a valid node index", + )); + } + let min_intermediate_nodes: usize = match min_depth { + Some(depth) => depth - 2, + None => 0, + }; + let cutoff_petgraph: Option = cutoff.map(|depth| depth - 2); + let result: Vec> = algo::all_simple_paths( + graph, + from_index, + to_index, + min_intermediate_nodes, + cutoff_petgraph, + ) + .map(|v: Vec| v.into_iter().map(|i| i.index()).collect()) + .collect(); + Ok(result) +} + +/// Return all simple paths between 2 nodes in a PyDiGraph object +/// +/// A simple path is a path with no repeated nodes. +/// +/// :param PyDiGraph graph: The graph to find the path in +/// :param int from: The node index to find the paths from +/// :param int to: The node index to find the paths to +/// :param int min_depth: The minimum depth of the path to include in the output +/// list of paths. By default all paths are included regardless of depth, +/// sett to 0 will behave like the default. +/// :param int cutoff: The maximum depth of path to include in the output list +/// of paths. By default includes all paths regardless of depth, setting to +/// 0 will behave like default. +/// +/// :returns: A list of lists where each inner list is a path +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, from, to, /, min_depth=None, cutoff=None)")] +fn digraph_all_simple_paths( + graph: &digraph::PyDiGraph, + from: usize, + to: usize, + min_depth: Option, + cutoff: Option, +) -> PyResult>> { + let from_index = NodeIndex::new(from); + if !graph.graph.contains_node(from_index) { + return Err(InvalidNode::new_err( + "The input index for 'from' is not a valid node index", + )); + } + let to_index = NodeIndex::new(to); + if !graph.graph.contains_node(to_index) { + return Err(InvalidNode::new_err( + "The input index for 'to' is not a valid node index", + )); + } + let min_intermediate_nodes: usize = match min_depth { + Some(depth) => depth - 2, + None => 0, + }; + let cutoff_petgraph: Option = cutoff.map(|depth| depth - 2); + let result: Vec> = algo::all_simple_paths( + graph, + from_index, + to_index, + min_intermediate_nodes, + cutoff_petgraph, + ) + .map(|v: Vec| v.into_iter().map(|i| i.index()).collect()) + .collect(); + Ok(result) +} + +/// Return the core number for each node in the graph. +/// +/// A k-core is a maximal subgraph that contains nodes of degree k or more. +/// +/// .. note:: +/// +/// The function implicitly assumes that there are no parallel edges +/// or self loops. It may produce incorrect/unexpected results if the +/// input graph has self loops or parallel edges. +/// +/// :param PyGraph: The graph to get core numbers +/// +/// :returns: A dictionary keyed by node index to the core number +/// :rtype: dict +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn graph_core_number( + py: Python, + graph: &graph::PyGraph, +) -> PyResult { + core_number::core_number(py, &graph.graph) +} + +/// Return the core number for each node in the directed graph. +/// +/// A k-core is a maximal subgraph that contains nodes of degree k or more. +/// For directed graphs, the degree is calculated as in_degree + out_degree. +/// +/// .. note:: +/// +/// The function implicitly assumes that there are no parallel edges +/// or self loops. It may produce incorrect/unexpected results if the +/// input graph has self loops or parallel edges. +/// +/// :param PyDiGraph: The directed graph to get core numbers +/// +/// :returns: A dictionary keyed by node index to the core number +/// :rtype: dict +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn digraph_core_number( + py: Python, + graph: &digraph::PyDiGraph, +) -> PyResult { + core_number::core_number(py, &graph.graph) +} diff --git a/src/dag_algo/longest_path.rs b/src/dag_algo/longest_path.rs new file mode 100644 index 0000000000..f3c9cb212c --- /dev/null +++ b/src/dag_algo/longest_path.rs @@ -0,0 +1,90 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use crate::{digraph, DAGHasCycle}; + +use hashbrown::HashMap; + +use pyo3::prelude::*; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; + +use num_traits::{Num, Zero}; + +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, None) { + Ok(nodes) => nodes, + Err(_err) => { + return Err(DAGHasCycle::new_err("Sort encountered a cycle")) + } + }; + 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; + Ok((path, path_weight)) +} diff --git a/src/dag_algo/mod.rs b/src/dag_algo/mod.rs new file mode 100644 index 0000000000..48a4332573 --- /dev/null +++ b/src/dag_algo/mod.rs @@ -0,0 +1,625 @@ +// 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. + +#![allow(clippy::float_cmp)] + +mod longest_path; + +use hashbrown::{HashMap, HashSet}; +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use super::iterators::NodeIndices; +use crate::{digraph, DAGHasCycle, InvalidNode}; + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyList; +use pyo3::Python; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::visit::NodeCount; + +/// Find the longest path in a DAG +/// +/// :param PyDiGraph graph: The graph to find the longest path on. The input +/// object must be a DAG without a cycle. +/// :param weight_fn: A python callable that if set will be passed the 3 +/// positional arguments, the source node, the target node, and the edge +/// weight for each edge as the function traverses the graph. It is expected +/// to return an unsigned integer weight for that edge. For example, +/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be +/// use to just use an integer edge weight. It's also worth noting that this +/// function traverses in topological order and only checks incoming edges to +/// each node. +/// +/// :returns: The node indices of the longest path on the DAG +/// :rtype: NodeIndices +/// +/// :raises Exception: If an unexpected error occurs or a path can't be found +/// :raises DAGHasCycle: If the input PyDiGraph has a cycle +#[pyfunction] +#[pyo3(text_signature = "(graph, /, weight_fn=None)")] +pub fn dag_longest_path( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: Option, +) -> PyResult { + let edge_weight_callable = + |source: usize, target: usize, weight: &PyObject| -> PyResult { + match &weight_fn { + Some(weight_fn) => { + let res = weight_fn.call1(py, (source, target, weight))?; + res.extract(py) + } + None => Ok(1), + } + }; + Ok(NodeIndices { + nodes: longest_path::longest_path(graph, edge_weight_callable)?.0, + }) +} + +/// Find the length of the longest path in a DAG +/// +/// :param PyDiGraph graph: The graph to find the longest path on. The input +/// object must be a DAG without a cycle. +/// :param weight_fn: A python callable that if set will be passed the 3 +/// positional arguments, the source node, the target node, and the edge +/// weight for each edge as the function traverses the graph. It is expected +/// to return an unsigned integer weight for that edge. For example, +/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be +/// use to just use an integer edge weight. It's also worth noting that this +/// function traverses in topological order and only checks incoming edges to +/// each node. +/// +/// :returns: The longest path length on the DAG +/// :rtype: int +/// +/// :raises Exception: If an unexpected error occurs or a path can't be found +/// :raises DAGHasCycle: If the input PyDiGraph has a cycle +#[pyfunction] +#[pyo3(text_signature = "(graph, /, weight_fn=None)")] +pub fn dag_longest_path_length( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: Option, +) -> PyResult { + let edge_weight_callable = + |source: usize, target: usize, weight: &PyObject| -> PyResult { + match &weight_fn { + Some(weight_fn) => { + let res = weight_fn.call1(py, (source, target, weight))?; + res.extract(py) + } + None => Ok(1), + } + }; + let (_, path_weight) = + longest_path::longest_path(graph, edge_weight_callable)?; + Ok(path_weight) +} + +/// Find the weighted longest path in a DAG +/// +/// This function differs from :func:`retworkx.dag_longest_path` in that +/// this function requires a ``weight_fn`` parameter, and the ``weight_fn`` is +/// expected to return a ``float`` not an ``int``. +/// +/// :param PyDiGraph graph: The graph to find the longest path on. The input +/// object must be a DAG without a cycle. +/// :param weight_fn: A python callable that will be passed the 3 +/// positional arguments, the source node, the target node, and the edge +/// weight for each edge as the function traverses the graph. It is expected +/// to return a float weight for that edge. For example, +/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be +/// used to just use a float edge weight. It's also worth noting that this +/// function traverses in topological order and only checks incoming edges to +/// each node. +/// +/// :returns: The node indices of the longest path on the DAG +/// :rtype: NodeIndices +/// +/// :raises Exception: If an unexpected error occurs or a path can't be found +/// :raises DAGHasCycle: If the input PyDiGraph has a cycle +#[pyfunction] +#[pyo3(text_signature = "(graph, weight_fn, /)")] +pub fn dag_weighted_longest_path( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: PyObject, +) -> PyResult { + let edge_weight_callable = + |source: usize, target: usize, weight: &PyObject| -> PyResult { + let res = weight_fn.call1(py, (source, target, weight))?; + let float_res: f64 = res.extract(py)?; + if float_res.is_nan() { + return Err(PyValueError::new_err( + "NaN is not a valid edge weight", + )); + } + Ok(float_res) + }; + Ok(NodeIndices { + nodes: longest_path::longest_path(graph, edge_weight_callable)?.0, + }) +} + +/// Find the length of the weighted longest path in a DAG +/// +/// This function differs from :func:`retworkx.dag_longest_path_length` in that +/// this function requires a ``weight_fn`` parameter, and the ``weight_fn`` is +/// expected to return a ``float`` not an ``int``. +/// +/// :param PyDiGraph graph: The graph to find the longest path on. The input +/// object must be a DAG without a cycle. +/// :param weight_fn: A python callable that will be passed the 3 +/// positional arguments, the source node, the target node, and the edge +/// weight for each edge as the function traverses the graph. It is expected +/// to return a float weight for that edge. For example, +/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be +/// used to just use a float edge weight. It's also worth noting that this +/// function traverses in topological order and only checks incoming edges to +/// each node. +/// +/// :returns: The longest path length on the DAG +/// :rtype: float +/// +/// :raises Exception: If an unexpected error occurs or a path can't be found +/// :raises DAGHasCycle: If the input PyDiGraph has a cycle +#[pyfunction] +#[pyo3(text_signature = "(graph, weight_fn, /)")] +pub fn dag_weighted_longest_path_length( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: PyObject, +) -> PyResult { + let edge_weight_callable = + |source: usize, target: usize, weight: &PyObject| -> PyResult { + let res = weight_fn.call1(py, (source, target, weight))?; + let float_res: f64 = res.extract(py)?; + if float_res.is_nan() { + return Err(PyValueError::new_err( + "NaN is not a valid edge weight", + )); + } + Ok(float_res) + }; + let (_, path_weight) = + longest_path::longest_path(graph, edge_weight_callable)?; + Ok(path_weight) +} + +/// Check that the PyDiGraph or PyDAG doesn't have a cycle +/// +/// :param PyDiGraph graph: The graph to check for cycles +/// +/// :returns: ``True`` if there are no cycles in the input graph, ``False`` +/// if there are cycles +/// :rtype: bool +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn is_directed_acyclic_graph(graph: &digraph::PyDiGraph) -> bool { + match algo::toposort(graph, None) { + Ok(_nodes) => true, + Err(_err) => false, + } +} + +/// Return a list of layers +/// +/// A layer is a subgraph whose nodes are disjoint, i.e., +/// a layer has depth 1. The layers are constructed using a greedy algorithm. +/// +/// :param PyDiGraph graph: The DAG to get the layers from +/// :param list first_layer: A list of node ids for the first layer. This +/// will be the first layer in the output +/// +/// :returns: A list of layers, each layer is a list of node data +/// :rtype: list +/// +/// :raises InvalidNode: If a node index in ``first_layer`` is not in the graph +#[pyfunction] +#[pyo3(text_signature = "(dag, first_layer, /)")] +pub fn layers( + py: Python, + dag: &digraph::PyDiGraph, + first_layer: Vec, +) -> PyResult { + let mut output: Vec> = Vec::new(); + // Convert usize to NodeIndex + let mut first_layer_index: Vec = Vec::new(); + for index in first_layer { + first_layer_index.push(NodeIndex::new(index)); + } + + let mut cur_layer = first_layer_index; + let mut next_layer: Vec = Vec::new(); + let mut predecessor_count: HashMap = HashMap::new(); + + let mut layer_node_data: Vec<&PyObject> = Vec::new(); + for layer_node in &cur_layer { + let node_data = match dag.graph.node_weight(*layer_node) { + Some(data) => data, + None => { + return Err(InvalidNode::new_err(format!( + "An index input in 'first_layer' {} is not a valid node index in the graph", + layer_node.index()), + )) + } + }; + layer_node_data.push(node_data); + } + output.push(layer_node_data); + + // Iterate until there are no more + while !cur_layer.is_empty() { + for node in &cur_layer { + let children = dag + .graph + .neighbors_directed(*node, petgraph::Direction::Outgoing); + let mut used_indexes: HashSet = HashSet::new(); + for succ in children { + // Skip duplicate successors + if used_indexes.contains(&succ) { + continue; + } + used_indexes.insert(succ); + let mut multiplicity: usize = 0; + let raw_edges = dag + .graph + .edges_directed(*node, petgraph::Direction::Outgoing); + for edge in raw_edges { + if edge.target() == succ { + multiplicity += 1; + } + } + predecessor_count + .entry(succ) + .and_modify(|e| *e -= multiplicity) + .or_insert(dag.in_degree(succ.index()) - multiplicity); + if *predecessor_count.get(&succ).unwrap() == 0 { + next_layer.push(succ); + predecessor_count.remove(&succ); + } + } + } + let mut layer_node_data: Vec<&PyObject> = Vec::new(); + for layer_node in &next_layer { + layer_node_data.push(&dag[*layer_node]); + } + if !layer_node_data.is_empty() { + output.push(layer_node_data); + } + cur_layer = next_layer; + next_layer = Vec::new(); + } + Ok(PyList::new(py, output).into()) +} + +/// Get the lexicographical topological sorted nodes from the provided DAG +/// +/// This function returns a list of nodes data in a graph lexicographically +/// topologically sorted using the provided key function. +/// +/// :param PyDiGraph dag: The DAG to get the topological sorted nodes from +/// :param callable key: key is a python function or other callable that +/// gets passed a single argument the node data from the graph and is +/// expected to return a string which will be used for sorting. +/// +/// :returns: A list of node's data lexicographically topologically sorted. +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(dag, key, /)")] +fn lexicographical_topological_sort( + py: Python, + dag: &digraph::PyDiGraph, + key: PyObject, +) -> PyResult { + let key_callable = |a: &PyObject| -> PyResult { + let res = key.call1(py, (a,))?; + Ok(res.to_object(py)) + }; + // HashMap of node_index indegree + let node_count = dag.node_count(); + let mut in_degree_map: HashMap = + HashMap::with_capacity(node_count); + for node in dag.graph.node_indices() { + in_degree_map.insert(node, dag.in_degree(node.index())); + } + + #[derive(Clone, Eq, PartialEq)] + struct State { + key: String, + node: NodeIndex, + } + + impl Ord for State { + fn cmp(&self, other: &State) -> Ordering { + // Notice that the we flip the ordering on costs. + // In case of a tie we compare positions - this step is necessary + // to make implementations of `PartialEq` and `Ord` consistent. + other + .key + .cmp(&self.key) + .then_with(|| other.node.index().cmp(&self.node.index())) + } + } + + // `PartialOrd` needs to be implemented as well. + impl PartialOrd for State { + fn partial_cmp(&self, other: &State) -> Option { + Some(self.cmp(other)) + } + } + let mut zero_indegree = BinaryHeap::with_capacity(node_count); + for (node, degree) in in_degree_map.iter() { + if *degree == 0 { + let map_key_raw = key_callable(&dag.graph[*node])?; + let map_key: String = map_key_raw.extract(py)?; + zero_indegree.push(State { + key: map_key, + node: *node, + }); + } + } + let mut out_list: Vec<&PyObject> = Vec::with_capacity(node_count); + let dir = petgraph::Direction::Outgoing; + while let Some(State { node, .. }) = zero_indegree.pop() { + let neighbors = dag.graph.neighbors_directed(node, dir); + for child in neighbors { + let child_degree = in_degree_map.get_mut(&child).unwrap(); + *child_degree -= 1; + if *child_degree == 0 { + let map_key_raw = key_callable(&dag.graph[child])?; + let map_key: String = map_key_raw.extract(py)?; + zero_indegree.push(State { + key: map_key, + node: child, + }); + in_degree_map.remove(&child); + } + } + out_list.push(&dag.graph[node]) + } + Ok(PyList::new(py, out_list).into()) +} + +/// Return the topological sort of node indexes from the provided graph +/// +/// :param PyDiGraph graph: The DAG to get the topological sort on +/// +/// :returns: A list of node indices topologically sorted. +/// :rtype: NodeIndices +/// +/// :raises DAGHasCycle: if a cycle is encountered while sorting the graph +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +fn topological_sort(graph: &digraph::PyDiGraph) -> PyResult { + let nodes = match algo::toposort(graph, None) { + Ok(nodes) => nodes, + Err(_err) => { + return Err(DAGHasCycle::new_err("Sort encountered a cycle")) + } + }; + Ok(NodeIndices { + nodes: nodes.iter().map(|node| node.index()).collect(), + }) +} + +/// Collect runs that match a filter function +/// +/// A run is a path of nodes where there is only a single successor and all +/// nodes in the path match the given condition. Each node in the graph can +/// appear in only a single run. +/// +/// :param PyDiGraph graph: The graph to find runs in +/// :param filter_fn: The filter function to use for matching nodes. It takes +/// in one argument, the node data payload/weight object, and will return a +/// boolean whether the node matches the conditions or not. If it returns +/// ``False`` it will skip that node. +/// +/// :returns: a list of runs, where each run is a list of node data +/// payload/weight for the nodes in the run +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, filter)")] +fn collect_runs( + py: Python, + graph: &digraph::PyDiGraph, + filter_fn: PyObject, +) -> PyResult>> { + let mut out_list: Vec> = Vec::new(); + let mut seen: HashSet = + HashSet::with_capacity(graph.node_count()); + + let filter_node = |node: &PyObject| -> PyResult { + let res = filter_fn.call1(py, (node,))?; + res.extract(py) + }; + + let nodes = match algo::toposort(graph, None) { + Ok(nodes) => nodes, + Err(_err) => { + return Err(DAGHasCycle::new_err("Sort encountered a cycle")) + } + }; + for node in nodes { + if !filter_node(&graph.graph[node])? || seen.contains(&node) { + continue; + } + seen.insert(node); + let mut group: Vec = vec![graph.graph[node].clone_ref(py)]; + let mut successors: Vec = graph + .graph + .neighbors_directed(node, petgraph::Direction::Outgoing) + .collect(); + successors.dedup(); + + while successors.len() == 1 + && filter_node(&graph.graph[successors[0]])? + && !seen.contains(&successors[0]) + { + group.push(graph.graph[successors[0]].clone_ref(py)); + seen.insert(successors[0]); + successors = graph + .graph + .neighbors_directed( + successors[0], + petgraph::Direction::Outgoing, + ) + .collect(); + successors.dedup(); + } + if !group.is_empty() { + out_list.push(group); + } + } + Ok(out_list) +} + +/// Collect runs that match a filter function given edge colors +/// +/// A bicolor run is a list of group of nodes connected by edges of exactly +/// two colors. In addition, all nodes in the group must match the given +/// condition. Each node in the graph can appear in only a single group +/// in the bicolor run. +/// +/// :param PyDiGraph graph: The graph to find runs in +/// :param filter_fn: The filter function to use for matching nodes. It takes +/// in one argument, the node data payload/weight object, and will return a +/// boolean whether the node matches the conditions or not. +/// If it returns ``True``, it will continue the bicolor chain. +/// If it returns ``False``, it will stop the bicolor chain. +/// If it returns ``None`` it will skip that node. +/// :param color_fn: The function that gives the color of the edge. It takes +/// in one argument, the edge data payload/weight object, and will +/// return a non-negative integer, the edge color. If the color is None, +/// the edge is ignored. +/// +/// :returns: a list of groups with exactly two edge colors, where each group +/// is a list of node data payload/weight for the nodes in the bicolor run +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, filter_fn, color_fn)")] +fn collect_bicolor_runs( + py: Python, + graph: &digraph::PyDiGraph, + filter_fn: PyObject, + color_fn: PyObject, +) -> PyResult>> { + let mut pending_list: Vec> = Vec::new(); + let mut block_id: Vec> = Vec::new(); + let mut block_list: Vec> = Vec::new(); + + let filter_node = |node: &PyObject| -> PyResult> { + let res = filter_fn.call1(py, (node,))?; + res.extract(py) + }; + + let color_edge = |edge: &PyObject| -> PyResult> { + let res = color_fn.call1(py, (edge,))?; + res.extract(py) + }; + + let nodes = match algo::toposort(graph, None) { + Ok(nodes) => nodes, + Err(_err) => { + return Err(DAGHasCycle::new_err("Sort encountered a cycle")) + } + }; + + // Utility for ensuring pending_list has the color index + macro_rules! ensure_vector_has_index { + ($pending_list: expr, $block_id: expr, $color: expr) => { + if $color >= $pending_list.len() { + $pending_list.resize($color + 1, Vec::new()); + $block_id.resize($color + 1, None); + } + }; + } + + for node in nodes { + if let Some(is_match) = filter_node(&graph.graph[node])? { + let raw_edges = graph + .graph + .edges_directed(node, petgraph::Direction::Outgoing); + + // Remove all edges that do not yield errors from color_fn + let colors = raw_edges + .map(|edge| { + let edge_weight = edge.weight(); + color_edge(edge_weight) + }) + .collect::>>>()?; + + // Remove null edges from color_fn + let colors = colors.into_iter().flatten().collect::>(); + + if colors.len() <= 2 && is_match { + if colors.len() == 1 { + let c0 = colors[0]; + ensure_vector_has_index!(pending_list, block_id, c0); + if let Some(c0_block_id) = block_id[c0] { + block_list[c0_block_id] + .push(graph.graph[node].clone_ref(py)); + } else { + pending_list[c0].push(graph.graph[node].clone_ref(py)); + } + } else if colors.len() == 2 { + let c0 = colors[0]; + let c1 = colors[1]; + ensure_vector_has_index!(pending_list, block_id, c0); + ensure_vector_has_index!(pending_list, block_id, c1); + + if block_id[c0].is_some() + && block_id[c1].is_some() + && block_id[c0] == block_id[c1] + { + block_list[block_id[c0].unwrap_or_default()] + .push(graph.graph[node].clone_ref(py)); + } else { + let mut new_block: Vec = Vec::with_capacity( + pending_list[c0].len() + pending_list[c1].len() + 1, + ); + + // Clears pending lits and add to new block + new_block.append(&mut pending_list[c0]); + new_block.append(&mut pending_list[c1]); + + new_block.push(graph.graph[node].clone_ref(py)); + + // Create new block, assign its id to color pair + block_id[c0] = Some(block_list.len()); + block_id[c1] = Some(block_list.len()); + block_list.push(new_block); + } + } + } else { + for color in colors { + let color = color; + ensure_vector_has_index!(pending_list, block_id, color); + if let Some(color_block_id) = block_id[color] { + block_list[color_block_id] + .append(&mut pending_list[color]); + } + block_id[color] = None; + pending_list[color].clear(); + } + } + } + } + + Ok(block_list) +} diff --git a/src/digraph.rs b/src/digraph.rs index 5ba1f99fdb..8a934135e8 100644 --- a/src/digraph.rs +++ b/src/digraph.rs @@ -50,10 +50,12 @@ use super::iterators::{ EdgeIndexMap, EdgeIndices, EdgeList, NodeIndices, NodeMap, WeightedEdgeList, }; use super::{ - is_directed_acyclic_graph, DAGHasCycle, DAGWouldCycle, NoEdgeBetweenNodes, - NoSuitableNeighbors, NodesRemoved, + DAGHasCycle, DAGWouldCycle, NoEdgeBetweenNodes, NoSuitableNeighbors, + NodesRemoved, }; +use super::dag_algo::is_directed_acyclic_graph; + /// A class for creating directed graphs /// /// The ``PyDiGraph`` class is used to create a directed graph. It can be a diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs new file mode 100644 index 0000000000..125ff2c623 --- /dev/null +++ b/src/isomorphism/mod.rs @@ -0,0 +1,326 @@ +// 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. + +#![allow(clippy::float_cmp)] + +mod vf2; + +use crate::{digraph, graph}; + +use std::cmp::Ordering; + +use pyo3::prelude::*; +use pyo3::Python; + +/// Determine if 2 directed graphs are isomorphic +/// +/// This checks if 2 graphs are isomorphic both structurally and also +/// comparing the node data and edge data using the provided matcher functions. +/// The matcher function takes in 2 data objects and will compare them. A simple +/// example that checks if they're just equal would be:: +/// +/// graph_a = retworkx.PyDiGraph() +/// graph_b = retworkx.PyDiGraph() +/// retworkx.is_isomorphic(graph_a, graph_b, +/// lambda x, y: x == y) +/// +/// .. note:: +/// +/// For better performance on large graphs, consider setting `id_order=False`. +/// +/// :param PyDiGraph first: The first graph to compare +/// :param PyDiGraph second: The second graph to compare +/// :param callable node_matcher: A python callable object that takes 2 positional +/// one for each node data object. If the return of this +/// function evaluates to True then the nodes passed to it are vieded +/// as matching. +/// :param callable edge_matcher: A python callable object that takes 2 positional +/// one for each edge data object. If the return of this +/// function evaluates to True then the edges passed to it are vieded +/// as matching. +/// :param bool id_order: If set to ``False`` this function will use a +/// heuristic matching order based on [VF2]_ paper. Otherwise it will +/// default to matching the nodes in order specified by their ids. +/// +/// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are +/// not. +/// :rtype: bool +#[pyfunction(id_order = "true")] +#[pyo3( + text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" +)] +fn digraph_is_isomorphic( + py: Python, + first: &digraph::PyDiGraph, + second: &digraph::PyDiGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, +) -> PyResult { + let compare_nodes = node_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let compare_edges = edge_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let res = vf2::is_isomorphic( + py, + &first.graph, + &second.graph, + compare_nodes, + compare_edges, + id_order, + Ordering::Equal, + true, + )?; + Ok(res) +} + +/// Determine if 2 undirected graphs are isomorphic +/// +/// This checks if 2 graphs are isomorphic both structurally and also +/// comparing the node data and edge data using the provided matcher functions. +/// The matcher function takes in 2 data objects and will compare them. A simple +/// example that checks if they're just equal would be:: +/// +/// graph_a = retworkx.PyGraph() +/// graph_b = retworkx.PyGraph() +/// retworkx.is_isomorphic(graph_a, graph_b, +/// lambda x, y: x == y) +/// +/// .. note:: +/// +/// For better performance on large graphs, consider setting `id_order=False`. +/// +/// :param PyGraph first: The first graph to compare +/// :param PyGraph second: The second graph to compare +/// :param callable node_matcher: A python callable object that takes 2 positional +/// one for each node data object. If the return of this +/// function evaluates to True then the nodes passed to it are vieded +/// as matching. +/// :param callable edge_matcher: A python callable object that takes 2 positional +/// one for each edge data object. If the return of this +/// function evaluates to True then the edges passed to it are vieded +/// as matching. +/// :param bool (default=True) id_order: If set to true, the algorithm matches the +/// nodes in order specified by their ids. Otherwise, it uses a heuristic +/// matching order based in [VF2]_ paper. +/// +/// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are +/// not. +/// :rtype: bool +#[pyfunction(id_order = "true")] +#[pyo3( + text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" +)] +fn graph_is_isomorphic( + py: Python, + first: &graph::PyGraph, + second: &graph::PyGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, +) -> PyResult { + let compare_nodes = node_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let compare_edges = edge_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let res = vf2::is_isomorphic( + py, + &first.graph, + &second.graph, + compare_nodes, + compare_edges, + id_order, + Ordering::Equal, + true, + )?; + Ok(res) +} + +/// Determine if 2 directed graphs are subgraph - isomorphic +/// +/// This checks if 2 graphs are subgraph isomorphic both structurally and also +/// comparing the node data and edge data using the provided matcher functions. +/// The matcher function takes in 2 data objects and will compare them. +/// Since there is an ambiguity in the term 'subgraph', do note that we check +/// for an node-induced subgraph if argument `induced` is set to `True`. If it is +/// set to `False`, we check for a non induced subgraph, meaning the second graph +/// can have fewer edges than the subgraph of the first. By default it's `True`. A +/// simple example that checks if they're just equal would be:: +/// +/// graph_a = retworkx.PyDiGraph() +/// graph_b = retworkx.PyDiGraph() +/// retworkx.is_subgraph_isomorphic(graph_a, graph_b, +/// lambda x, y: x == y) +/// +/// .. note:: +/// +/// For better performance on large graphs, consider setting `id_order=False`. +/// +/// :param PyDiGraph first: The first graph to compare +/// :param PyDiGraph second: The second graph to compare +/// :param callable node_matcher: A python callable object that takes 2 positional +/// one for each node data object. If the return of this +/// function evaluates to True then the nodes passed to it are vieded +/// as matching. +/// :param callable edge_matcher: A python callable object that takes 2 positional +/// one for each edge data object. If the return of this +/// function evaluates to True then the edges passed to it are vieded +/// as matching. +/// :param bool id_order: If set to ``True`` this function will match the nodes +/// in order specified by their ids. Otherwise it will default to a heuristic +/// matching order based on [VF2]_ paper. +/// :param bool induced: If set to ``True`` this function will check the existence +/// of a node-induced subgraph of first isomorphic to second graph. +/// Default: ``True``. +/// +/// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, +/// ``False`` if there is not. +/// :rtype: bool +#[pyfunction(id_order = "false", induced = "true")] +#[pyo3( + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" +)] +fn digraph_is_subgraph_isomorphic( + py: Python, + first: &digraph::PyDiGraph, + second: &digraph::PyDiGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + induced: bool, +) -> PyResult { + let compare_nodes = node_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let compare_edges = edge_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let res = vf2::is_isomorphic( + py, + &first.graph, + &second.graph, + compare_nodes, + compare_edges, + id_order, + Ordering::Greater, + induced, + )?; + Ok(res) +} + +/// Determine if 2 undirected graphs are subgraph - isomorphic +/// +/// This checks if 2 graphs are subgraph isomorphic both structurally and also +/// comparing the node data and edge data using the provided matcher functions. +/// The matcher function takes in 2 data objects and will compare them. +/// Since there is an ambiguity in the term 'subgraph', do note that we check +/// for an node-induced subgraph if argument `induced` is set to `True`. If it is +/// set to `False`, we check for a non induced subgraph, meaning the second graph +/// can have fewer edges than the subgraph of the first. By default it's `True`. A +/// simple example that checks if they're just equal would be:: +/// +/// graph_a = retworkx.PyGraph() +/// graph_b = retworkx.PyGraph() +/// retworkx.is_subgraph_isomorphic(graph_a, graph_b, +/// lambda x, y: x == y) +/// +/// .. note:: +/// +/// For better performance on large graphs, consider setting `id_order=False`. +/// +/// :param PyGraph first: The first graph to compare +/// :param PyGraph second: The second graph to compare +/// :param callable node_matcher: A python callable object that takes 2 positional +/// one for each node data object. If the return of this +/// function evaluates to True then the nodes passed to it are vieded +/// as matching. +/// :param callable edge_matcher: A python callable object that takes 2 positional +/// one for each edge data object. If the return of this +/// function evaluates to True then the edges passed to it are vieded +/// as matching. +/// :param bool id_order: If set to ``True`` this function will match the nodes +/// in order specified by their ids. Otherwise it will default to a heuristic +/// matching order based on [VF2]_ paper. +/// :param bool induced: If set to ``True`` this function will check the existence +/// of a node-induced subgraph of first isomorphic to second graph. +/// Default: ``True``. +/// +/// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, +/// ``False`` if there is not. +/// :rtype: bool +#[pyfunction(id_order = "false", induced = "true")] +#[pyo3( + text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" +)] +fn graph_is_subgraph_isomorphic( + py: Python, + first: &graph::PyGraph, + second: &graph::PyGraph, + node_matcher: Option, + edge_matcher: Option, + id_order: bool, + induced: bool, +) -> PyResult { + let compare_nodes = node_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let compare_edges = edge_matcher.map(|f| { + move |a: &PyObject, b: &PyObject| -> PyResult { + let res = f.call1(py, (a, b))?; + Ok(res.is_true(py).unwrap()) + } + }); + + let res = vf2::is_isomorphic( + py, + &first.graph, + &second.graph, + compare_nodes, + compare_edges, + id_order, + Ordering::Greater, + induced, + )?; + Ok(res) +} diff --git a/src/isomorphism.rs b/src/isomorphism/vf2.rs similarity index 99% rename from src/isomorphism.rs rename to src/isomorphism/vf2.rs index c4c9edb484..3915dd894f 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism/vf2.rs @@ -19,7 +19,7 @@ use std::marker; use hashbrown::HashMap; -use super::NodesRemoved; +use crate::NodesRemoved; use pyo3::prelude::*; diff --git a/src/layout/bipartite.rs b/src/layout/bipartite.rs new file mode 100644 index 0000000000..25287ea996 --- /dev/null +++ b/src/layout/bipartite.rs @@ -0,0 +1,105 @@ +// 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 std::iter::Iterator; + +use hashbrown::{HashMap, HashSet}; + +use pyo3::prelude::*; + +use petgraph::prelude::*; +use petgraph::EdgeType; + +use super::spring::{recenter, rescale, Point}; +use crate::iterators::Pos2DMapping; + +pub fn bipartite_layout( + graph: &StableGraph, + first_nodes: HashSet, + horizontal: Option, + scale: Option, + center: Option, + aspect_ratio: Option, +) -> Pos2DMapping { + let node_num = graph.node_count(); + if node_num == 0 { + return Pos2DMapping { + pos_map: HashMap::new(), + }; + } + let left_num = first_nodes.len(); + let right_num = node_num - left_num; + let mut pos: Vec = Vec::with_capacity(node_num); + + let (width, height); + if horizontal == Some(true) { + // width and height viewed from 90 degrees clockwise rotation + width = 1.0; + height = match aspect_ratio { + Some(aspect_ratio) => aspect_ratio * width, + None => 4.0 * width / 3.0, + }; + } else { + height = 1.0; + width = match aspect_ratio { + Some(aspect_ratio) => aspect_ratio * height, + None => 4.0 * height / 3.0, + }; + } + + let x_offset: f64 = width / 2.0; + let y_offset: f64 = height / 2.0; + let left_dy: f64 = match left_num { + 0 | 1 => 0.0, + _ => height / (left_num - 1) as f64, + }; + let right_dy: f64 = match right_num { + 0 | 1 => 0.0, + _ => height / (right_num - 1) as f64, + }; + + let mut lc: f64 = 0.0; + let mut rc: f64 = 0.0; + + for node in graph.node_indices() { + let n = node.index(); + + let (x, y): (f64, f64); + if first_nodes.contains(&n) { + x = -x_offset; + y = lc * left_dy - y_offset; + lc += 1.0; + } else { + x = width - x_offset; + y = rc * right_dy - y_offset; + rc += 1.0; + } + + if horizontal == Some(true) { + pos.push([-y, x]); + } else { + pos.push([x, y]); + } + } + + if let Some(scale) = scale { + rescale(&mut pos, scale, (0..node_num).collect()); + } + + if let Some(center) = center { + recenter(&mut pos, center); + } + + Pos2DMapping { + pos_map: graph.node_indices().map(|n| n.index()).zip(pos).collect(), + } +} diff --git a/src/layout/circular.rs b/src/layout/circular.rs new file mode 100644 index 0000000000..b770ad9a75 --- /dev/null +++ b/src/layout/circular.rs @@ -0,0 +1,52 @@ +// 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 std::iter::Iterator; + +use pyo3::prelude::*; + +use petgraph::prelude::*; +use petgraph::EdgeType; + +use super::spring::{recenter, rescale, Point}; +use crate::iterators::Pos2DMapping; + +pub fn circular_layout( + graph: &StableGraph, + scale: Option, + center: Option, +) -> Pos2DMapping { + let node_num = graph.node_count(); + let mut pos: Vec = Vec::with_capacity(node_num); + let pi = std::f64::consts::PI; + + if node_num == 1 { + pos.push([0.0, 0.0]) + } else { + for i in 0..node_num { + let angle = 2.0 * pi * i as f64 / node_num as f64; + pos.push([angle.cos(), angle.sin()]); + } + } + + if let Some(scale) = scale { + rescale(&mut pos, scale, (0..node_num).collect()); + } + + if let Some(center) = center { + recenter(&mut pos, center); + } + + Pos2DMapping { + pos_map: graph.node_indices().map(|n| n.index()).zip(pos).collect(), + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs new file mode 100644 index 0000000000..4f9ce3bb43 --- /dev/null +++ b/src/layout/mod.rs @@ -0,0 +1,447 @@ +// 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. + +mod bipartite; +mod circular; +mod random; +mod shell; +mod spiral; +mod spring; + +use crate::{digraph, graph}; +use spring::Point; + +use hashbrown::{HashMap, HashSet}; + +use pyo3::prelude::*; +use pyo3::Python; + +use crate::iterators::Pos2DMapping; + +/// Position nodes using Fruchterman-Reingold force-directed algorithm. +/// +/// The algorithm simulates a force-directed representation of the network +/// treating edges as springs holding nodes close, while treating nodes +/// as repelling objects, sometimes called an anti-gravity force. +/// Simulation continues until the positions are close to an equilibrium. +/// +/// :param PyGraph graph: Graph to be used. +/// :param dict pos: +/// Initial node positions as a dictionary with node ids as keys and values +/// as a coordinate list. If ``None``, then use random initial positions. (``default=None``) +/// :param set fixed: Nodes to keep fixed at initial position. +/// Error raised if fixed specified and ``pos`` is not. (``default=None``) +/// :param float k: +/// Optimal distance between nodes. If ``None`` the distance is set to +/// :math:`\frac{1}{\sqrt{n}}` where :math:`n` is the number of nodes. Increase this value +/// to move nodes farther apart. (``default=None``) +/// :param int repulsive_exponent: +/// Repulsive force exponent. (``default=2``) +/// :param bool adaptive_cooling: +/// Use an adaptive cooling scheme. If set to ``False``, +/// a linear cooling scheme is used. (``default=True``) +/// :param int num_iter: +/// Maximum number of iterations. (``default=50``) +/// :param float tol: +/// Threshold for relative error in node position changes. +/// The iteration stops if the error is below this threshold. +/// (``default = 1e-6``) +/// :param weight_fn: An optional weight function for an edge. It will accept +/// a single argument, the edge's weight object and will return a float +/// which will be used to represent the weight of the edge. +/// :param float (default=1) default_weight: If ``weight_fn`` isn't specified +/// this optional float value will be used for the weight/cost of each edge +/// :param float|None scale: Scale factor for positions. +/// Not used unless fixed is None. If scale is ``None``, no re-scaling is +/// performed. (``default=1.0``) +/// :param list center: Coordinate pair around which to center +/// the layout. Not used unless fixed is ``None``. (``default=None``) +/// :param int seed: An optional seed to use for the random number generator +/// +/// :returns: A dictionary of positions keyed by node id. +/// :rtype: dict +#[pyfunction] +#[pyo3( + text_signature = "(graph, pos=None, fixed=None, k=None, repulsive_exponent=2, adaptive_cooling=True, + num_iter=50, tol=1e-6, weight_fn=None, default_weight=1, scale=1, + center=None, seed=None, /)" +)] +#[allow(clippy::too_many_arguments)] +pub fn graph_spring_layout( + py: Python, + graph: &graph::PyGraph, + pos: Option>, + fixed: Option>, + k: Option, + repulsive_exponent: Option, + adaptive_cooling: Option, + num_iter: Option, + tol: Option, + weight_fn: Option, + default_weight: f64, + scale: Option, + center: Option, + seed: Option, +) -> PyResult { + spring::spring_layout( + py, + &graph.graph, + pos, + fixed, + k, + repulsive_exponent, + adaptive_cooling, + num_iter, + tol, + weight_fn, + default_weight, + scale, + center, + seed, + ) +} + +/// Position nodes using Fruchterman-Reingold force-directed algorithm. +/// +/// The algorithm simulates a force-directed representation of the network +/// treating edges as springs holding nodes close, while treating nodes +/// as repelling objects, sometimes called an anti-gravity force. +/// Simulation continues until the positions are close to an equilibrium. +/// +/// :param PyGraph graph: Graph to be used. +/// :param dict pos: +/// Initial node positions as a dictionary with node ids as keys and values +/// as a coordinate list. If ``None``, then use random initial positions. (``default=None``) +/// :param set fixed: Nodes to keep fixed at initial position. +/// Error raised if fixed specified and ``pos`` is not. (``default=None``) +/// :param float k: +/// Optimal distance between nodes. If ``None`` the distance is set to +/// :math:`\frac{1}{\sqrt{n}}` where :math:`n` is the number of nodes. Increase this value +/// to move nodes farther apart. (``default=None``) +/// :param int repulsive_exponent: +/// Repulsive force exponent. (``default=2``) +/// :param bool adaptive_cooling: +/// Use an adaptive cooling scheme. If set to ``False``, +/// a linear cooling scheme is used. (``default=True``) +/// :param int num_iter: +/// Maximum number of iterations. (``default=50``) +/// :param float tol: +/// Threshold for relative error in node position changes. +/// The iteration stops if the error is below this threshold. +/// (``default = 1e-6``) +/// :param weight_fn: An optional weight function for an edge. It will accept +/// a single argument, the edge's weight object and will return a float +/// which will be used to represent the weight of the edge. +/// :param float (default=1) default_weight: If ``weight_fn`` isn't specified +/// this optional float value will be used for the weight/cost of each edge +/// :param float|None scale: Scale factor for positions. +/// Not used unless fixed is None. If scale is ``None``, no re-scaling is +/// performed. (``default=1.0``) +/// :param list center: Coordinate pair around which to center +/// the layout. Not used unless fixed is ``None``. (``default=None``) +/// :param int seed: An optional seed to use for the random number generator +/// +/// :returns: A dictionary of positions keyed by node id. +/// :rtype: dict +#[pyfunction] +#[pyo3( + text_signature = "(graph, pos=None, fixed=None, k=None, repulsive_exponent=2, adaptive_cooling=True, + num_iter=50, tol=1e-6, weight_fn=None, default_weight=1, scale=1, + center=None, seed=None, /)" +)] +#[allow(clippy::too_many_arguments)] +pub fn digraph_spring_layout( + py: Python, + graph: &digraph::PyDiGraph, + pos: Option>, + fixed: Option>, + k: Option, + repulsive_exponent: Option, + adaptive_cooling: Option, + num_iter: Option, + tol: Option, + weight_fn: Option, + default_weight: f64, + scale: Option, + center: Option, + seed: Option, +) -> PyResult { + spring::spring_layout( + py, + &graph.graph, + pos, + fixed, + k, + repulsive_exponent, + adaptive_cooling, + num_iter, + tol, + weight_fn, + default_weight, + scale, + center, + seed, + ) +} + +/// Generate a random layout +/// +/// :param PyGraph graph: The graph to generate the layout for +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// :param int seed: An optional seed to set for the random number generator. +/// +/// :returns: The random layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, / center=None, seed=None)")] +pub fn graph_random_layout( + graph: &graph::PyGraph, + center: Option<[f64; 2]>, + seed: Option, +) -> Pos2DMapping { + random::random_layout(&graph.graph, center, seed) +} + +/// Generate a random layout +/// +/// :param PyDiGraph graph: The graph to generate the layout for +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// :param int seed: An optional seed to set for the random number generator. +/// +/// :returns: The random layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, / center=None, seed=None)")] +pub fn digraph_random_layout( + graph: &digraph::PyDiGraph, + center: Option<[f64; 2]>, + seed: Option, +) -> Pos2DMapping { + random::random_layout(&graph.graph, center, seed) +} + +/// Generate a bipartite layout of the graph +/// +/// :param PyGraph graph: The graph to generate the layout for +/// :param set first_nodes: The set of node indexes on the left (or top if +/// horitontal is true) +/// :param bool horizontal: An optional bool specifying the orientation of the +/// layout +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// :param float aspect_ratio: An optional number for the ratio of the width to +/// the height of the layout. +/// +/// :returns: The bipartite layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, first_nodes, /, horitontal=False, scale=1, + center=None, aspect_ratio=1.33333333333333)")] +pub fn graph_bipartite_layout( + graph: &graph::PyGraph, + first_nodes: HashSet, + horizontal: Option, + scale: Option, + center: Option, + aspect_ratio: Option, +) -> Pos2DMapping { + bipartite::bipartite_layout( + &graph.graph, + first_nodes, + horizontal, + scale, + center, + aspect_ratio, + ) +} + +/// Generate a bipartite layout of the graph +/// +/// :param PyDiGraph graph: The graph to generate the layout for +/// :param set first_nodes: The set of node indexes on the left (or top if +/// horizontal is true) +/// :param bool horizontal: An optional bool specifying the orientation of the +/// layout +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// :param float aspect_ratio: An optional number for the ratio of the width to +/// the height of the layout. +/// +/// :returns: The bipartite layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, first_nodes, /, horitontal=False, scale=1, + center=None, aspect_ratio=1.33333333333333)")] +pub fn digraph_bipartite_layout( + graph: &digraph::PyDiGraph, + first_nodes: HashSet, + horizontal: Option, + scale: Option, + center: Option, + aspect_ratio: Option, +) -> Pos2DMapping { + bipartite::bipartite_layout( + &graph.graph, + first_nodes, + horizontal, + scale, + center, + aspect_ratio, + ) +} + +/// Generate a circular layout of the graph +/// +/// :param PyGraph graph: The graph to generate the layout for +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// +/// :returns: The circular layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, /, scale=1, center=None)")] +pub fn graph_circular_layout( + graph: &graph::PyGraph, + scale: Option, + center: Option, +) -> Pos2DMapping { + circular::circular_layout(&graph.graph, scale, center) +} + +/// Generate a circular layout of the graph +/// +/// :param PyDiGraph graph: The graph to generate the layout for +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// +/// :returns: The circular layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, /, scale=1, center=None)")] +pub fn digraph_circular_layout( + graph: &digraph::PyDiGraph, + scale: Option, + center: Option, +) -> Pos2DMapping { + circular::circular_layout(&graph.graph, scale, center) +} + +/// Generate a shell layout of the graph +/// +/// :param PyGraph graph: The graph to generate the layout for +/// :param list nlist: The list of lists of indexes which represents each shell +/// :param float rotate: Angle (in radians) by which to rotate the starting +/// position of each shell relative to the starting position of the +/// previous shell +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// +/// :returns: The shell layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3( + text_signature = "(graph, /, nlist=None, rotate=None, scale=1, center=None)" +)] +pub fn graph_shell_layout( + graph: &graph::PyGraph, + nlist: Option>>, + rotate: Option, + scale: Option, + center: Option, +) -> Pos2DMapping { + shell::shell_layout(&graph.graph, nlist, rotate, scale, center) +} + +/// Generate a shell layout of the graph +/// +/// :param PyDiGraph graph: The graph to generate the layout for +/// :param list nlist: The list of lists of indexes which represents each shell +/// :param float rotate: Angle by which to rotate the starting position of each shell +/// relative to the starting position of the previous shell (in radians) +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// +/// :returns: The shell layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3( + text_signature = "(graph, /, nlist=None, rotate=None, scale=1, center=None)" +)] +pub fn digraph_shell_layout( + graph: &digraph::PyDiGraph, + nlist: Option>>, + rotate: Option, + scale: Option, + center: Option, +) -> Pos2DMapping { + shell::shell_layout(&graph.graph, nlist, rotate, scale, center) +} + +/// Generate a spiral layout of the graph +/// +/// :param PyGraph graph: The graph to generate the layout for +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// :param float resolution: The compactness of the spiral layout returned. +/// Lower values result in more compressed spiral layouts. +/// :param bool equidistant: If true, nodes will be plotted equidistant from +/// each other. +/// +/// :returns: The spiral layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, /, scale=1, center=None, resolution=0.35, + equidistant=False)")] +pub fn graph_spiral_layout( + graph: &graph::PyGraph, + scale: Option, + center: Option, + resolution: Option, + equidistant: Option, +) -> Pos2DMapping { + spiral::spiral_layout(&graph.graph, scale, center, resolution, equidistant) +} + +/// Generate a spiral layout of the graph +/// +/// :param PyDiGraph graph: The graph to generate the layout for +/// :param float scale: An optional scaling factor to scale positions +/// :param tuple center: An optional center position. This is a 2 tuple of two +/// ``float`` values for the center position +/// :param float resolution: The compactness of the spiral layout returned. +/// Lower values result in more compressed spiral layouts. +/// :param bool equidistant: If true, nodes will be plotted equidistant from +/// each other. +/// +/// :returns: The spiral layout of the graph. +/// :rtype: Pos2DMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, /, scale=1, center=None, resolution=0.35, + equidistant=False)")] +pub fn digraph_spiral_layout( + graph: &digraph::PyDiGraph, + scale: Option, + center: Option, + resolution: Option, + equidistant: Option, +) -> Pos2DMapping { + spiral::spiral_layout(&graph.graph, scale, center, resolution, equidistant) +} diff --git a/src/layout/random.rs b/src/layout/random.rs new file mode 100644 index 0000000000..e58387f5c3 --- /dev/null +++ b/src/layout/random.rs @@ -0,0 +1,51 @@ +// 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 pyo3::prelude::*; + +use petgraph::prelude::*; +use petgraph::EdgeType; + +use rand::prelude::*; +use rand_pcg::Pcg64; + +use crate::iterators::Pos2DMapping; + +pub fn random_layout( + graph: &StableGraph, + center: Option<[f64; 2]>, + seed: Option, +) -> Pos2DMapping { + let mut rng: Pcg64 = match seed { + Some(seed) => Pcg64::seed_from_u64(seed), + None => Pcg64::from_entropy(), + }; + + Pos2DMapping { + pos_map: graph + .node_indices() + .map(|n| { + let random_tuple: [f64; 2] = rng.gen(); + match center { + Some(center) => ( + n.index(), + [ + random_tuple[0] + center[0], + random_tuple[1] + center[1], + ], + ), + None => (n.index(), random_tuple), + } + }) + .collect(), + } +} diff --git a/src/layout/shell.rs b/src/layout/shell.rs new file mode 100644 index 0000000000..8bb2134053 --- /dev/null +++ b/src/layout/shell.rs @@ -0,0 +1,80 @@ +// 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 std::iter::Iterator; + +use pyo3::prelude::*; + +use petgraph::prelude::*; +use petgraph::visit::NodeIndexable; +use petgraph::EdgeType; + +use super::spring::{recenter, Point}; +use crate::iterators::Pos2DMapping; + +pub fn shell_layout( + graph: &StableGraph, + nlist: Option>>, + rotate: Option, + scale: Option, + center: Option, +) -> Pos2DMapping { + let node_num = graph.node_bound(); + let mut pos: Vec = vec![[0.0, 0.0]; node_num]; + let pi = std::f64::consts::PI; + + let shell_list: Vec> = match nlist { + Some(nlist) => nlist, + None => vec![graph.node_indices().map(|n| n.index()).collect()], + }; + let shell_num = shell_list.len(); + + let radius_bump = match scale { + Some(scale) => scale / shell_num as f64, + None => 1.0 / shell_num as f64, + }; + + let mut radius = match node_num { + 1 => 0.0, + _ => radius_bump, + }; + + let rot_angle = match rotate { + Some(rotate) => rotate, + None => pi / shell_num as f64, + }; + + let mut first_theta = rot_angle; + for shell in shell_list { + let shell_len = shell.len(); + for i in 0..shell_len { + let angle = 2.0 * pi * i as f64 / shell_len as f64 + first_theta; + pos[shell[i]] = [radius * angle.cos(), radius * angle.sin()]; + } + radius += radius_bump; + first_theta += rot_angle; + } + + if let Some(center) = center { + recenter(&mut pos, center); + } + + Pos2DMapping { + pos_map: graph + .node_indices() + .map(|n| { + let n = n.index(); + (n, pos[n]) + }) + .collect(), + } +} diff --git a/src/layout/spiral.rs b/src/layout/spiral.rs new file mode 100644 index 0000000000..bad44af0e7 --- /dev/null +++ b/src/layout/spiral.rs @@ -0,0 +1,68 @@ +// 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 std::iter::Iterator; + +use pyo3::prelude::*; + +use petgraph::prelude::*; +use petgraph::EdgeType; + +use super::spring::{recenter, rescale, Point}; +use crate::iterators::Pos2DMapping; + +pub fn spiral_layout( + graph: &StableGraph, + scale: Option, + center: Option, + resolution: Option, + equidistant: Option, +) -> Pos2DMapping { + let node_num = graph.node_count(); + let mut pos: Vec = Vec::with_capacity(node_num); + + let ros = resolution.unwrap_or(0.35); + + if node_num == 1 { + pos.push([0.0, 0.0]); + } else if equidistant == Some(true) { + let mut theta: f64 = ros; + let chord = 1.0; + let step = 0.5; + for _ in 0..node_num { + let r = step * theta; + theta += chord / r; + pos.push([theta.cos() * r, theta.sin() * r]); + } + } else { + let mut angle: f64 = 0.0; + let mut dist = 0.0; + let step = 1.0; + for _ in 0..node_num { + pos.push([dist * angle.cos(), dist * angle.sin()]); + dist += step; + angle += ros; + } + } + + if let Some(scale) = scale { + rescale(&mut pos, scale, (0..node_num).collect()); + } + + if let Some(center) = center { + recenter(&mut pos, center); + } + + Pos2DMapping { + pos_map: graph.node_indices().map(|n| n.index()).zip(pos).collect(), + } +} diff --git a/src/layout.rs b/src/layout/spring.rs similarity index 55% rename from src/layout.rs rename to src/layout/spring.rs index 4e0a26ff3a..62a6aa27c6 100644 --- a/src/layout.rs +++ b/src/layout/spring.rs @@ -10,18 +10,24 @@ // License for the specific language governing permissions and limitations // under the License. +use crate::iterators::Pos2DMapping; +use crate::weight_callable; + use std::iter::Iterator; use hashbrown::{HashMap, HashSet}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use petgraph::graph::NodeIndex; use petgraph::prelude::*; -use petgraph::visit::NodeIndexable; +use petgraph::visit::{IntoEdgeReferences, NodeIndexable}; use petgraph::EdgeType; -use crate::iterators::Pos2DMapping; +use rand::distributions::{Distribution, Uniform}; +use rand::prelude::*; +use rand_pcg::Pcg64; type Nt = f64; pub type Point = [Nt; 2]; @@ -162,7 +168,7 @@ impl CoolingScheme for LinearCoolingScheme { } // Rescale so that pos in [-scale, scale]. -fn rescale(pos: &mut Vec, scale: Nt, indices: Vec) { +pub fn rescale(pos: &mut Vec, scale: Nt, indices: Vec) { let n = indices.len(); if n == 0 { return; @@ -198,7 +204,7 @@ fn rescale(pos: &mut Vec, scale: Nt, indices: Vec) { } } -fn recenter(pos: &mut Vec, center: Point) { +pub fn recenter(pos: &mut Vec, center: Point) { for [px, py] in pos.iter_mut() { *px += center[0]; *py += center[1]; @@ -290,167 +296,83 @@ where pos } -pub fn bipartite_layout( +#[allow(clippy::too_many_arguments)] +pub fn spring_layout( + py: Python, graph: &StableGraph, - first_nodes: HashSet, - horizontal: Option, + pos: Option>, + fixed: Option>, + k: Option, + repulsive_exponent: Option, + adaptive_cooling: Option, + num_iter: Option, + tol: Option, + weight_fn: Option, + default_weight: f64, scale: Option, center: Option, - aspect_ratio: Option, -) -> Pos2DMapping { - let node_num = graph.node_count(); - if node_num == 0 { - return Pos2DMapping { - pos_map: HashMap::new(), - }; - } - let left_num = first_nodes.len(); - let right_num = node_num - left_num; - let mut pos: Vec = Vec::with_capacity(node_num); - - let (width, height); - if horizontal == Some(true) { - // width and height viewed from 90 degrees clockwise rotation - width = 1.0; - height = match aspect_ratio { - Some(aspect_ratio) => aspect_ratio * width, - None => 4.0 * width / 3.0, - }; - } else { - height = 1.0; - width = match aspect_ratio { - Some(aspect_ratio) => aspect_ratio * height, - None => 4.0 * height / 3.0, - }; + seed: Option, +) -> PyResult +where + Ty: EdgeType, +{ + if fixed.is_some() && pos.is_none() { + return Err(PyValueError::new_err("`fixed` specified but `pos` not.")); } - let x_offset: f64 = width / 2.0; - let y_offset: f64 = height / 2.0; - let left_dy: f64 = match left_num { - 0 | 1 => 0.0, - _ => height / (left_num - 1) as f64, + let mut rng: Pcg64 = match seed { + Some(seed) => Pcg64::seed_from_u64(seed), + None => Pcg64::from_entropy(), }; - let right_dy: f64 = match right_num { - 0 | 1 => 0.0, - _ => height / (right_num - 1) as f64, - }; - - let mut lc: f64 = 0.0; - let mut rc: f64 = 0.0; - for node in graph.node_indices() { - let n = node.index(); - - let (x, y): (f64, f64); - if first_nodes.contains(&n) { - x = -x_offset; - y = lc * left_dy - y_offset; - lc += 1.0; - } else { - x = width - x_offset; - y = rc * right_dy - y_offset; - rc += 1.0; - } + let dist = Uniform::new(0.0, 1.0); - if horizontal == Some(true) { - pos.push([-y, x]); - } else { - pos.push([x, y]); - } + let pos = pos.unwrap_or_default(); + let mut vpos: Vec = (0..graph.node_bound()) + .map(|_| [dist.sample(&mut rng), dist.sample(&mut rng)]) + .collect(); + for (n, p) in pos.into_iter() { + vpos[n] = p; } - if let Some(scale) = scale { - rescale(&mut pos, scale, (0..node_num).collect()); - } + let fixed = fixed.unwrap_or_default(); + let k = k.unwrap_or(1.0 / (graph.node_count() as f64).sqrt()); + let f_a = AttractiveForce::new(k); + let f_r = RepulsiveForce::new(k, repulsive_exponent.unwrap_or(2)); - if let Some(center) = center { - recenter(&mut pos, center); - } + let num_iter = num_iter.unwrap_or(50); + let tol = tol.unwrap_or(1e-6); + let step = 0.1; - Pos2DMapping { - pos_map: graph.node_indices().map(|n| n.index()).zip(pos).collect(), - } -} + let mut weights: HashMap<(usize, usize), f64> = + HashMap::with_capacity(2 * graph.edge_count()); + for e in graph.edge_references() { + let w = weight_callable(py, &weight_fn, e.weight(), default_weight)?; + let source = e.source().index(); + let target = e.target().index(); -pub fn circular_layout( - graph: &StableGraph, - scale: Option, - center: Option, -) -> Pos2DMapping { - let node_num = graph.node_count(); - let mut pos: Vec = Vec::with_capacity(node_num); - let pi = std::f64::consts::PI; - - if node_num == 1 { - pos.push([0.0, 0.0]) - } else { - for i in 0..node_num { - let angle = 2.0 * pi * i as f64 / node_num as f64; - pos.push([angle.cos(), angle.sin()]); - } + weights.insert((source, target), w); + weights.insert((target, source), w); } - if let Some(scale) = scale { - rescale(&mut pos, scale, (0..node_num).collect()); - } - - if let Some(center) = center { - recenter(&mut pos, center); - } - - Pos2DMapping { - pos_map: graph.node_indices().map(|n| n.index()).zip(pos).collect(), - } -} - -pub fn shell_layout( - graph: &StableGraph, - nlist: Option>>, - rotate: Option, - scale: Option, - center: Option, -) -> Pos2DMapping { - let node_num = graph.node_bound(); - let mut pos: Vec = vec![[0.0, 0.0]; node_num]; - let pi = std::f64::consts::PI; - - let shell_list: Vec> = match nlist { - Some(nlist) => nlist, - None => vec![graph.node_indices().map(|n| n.index()).collect()], - }; - let shell_num = shell_list.len(); - - let radius_bump = match scale { - Some(scale) => scale / shell_num as f64, - None => 1.0 / shell_num as f64, - }; - - let mut radius = match node_num { - 1 => 0.0, - _ => radius_bump, - }; - - let rot_angle = match rotate { - Some(rotate) => rotate, - None => pi / shell_num as f64, - }; - - let mut first_theta = rot_angle; - for shell in shell_list { - let shell_len = shell.len(); - for i in 0..shell_len { - let angle = 2.0 * pi * i as f64 / shell_len as f64 + first_theta; - pos[shell[i]] = [radius * angle.cos(), radius * angle.sin()]; + let pos = match adaptive_cooling { + Some(false) => { + let cs = LinearCoolingScheme::new(step, num_iter); + evolve( + graph, vpos, fixed, f_a, f_r, cs, num_iter, tol, weights, + scale, center, + ) } - radius += radius_bump; - first_theta += rot_angle; - } - - if let Some(center) = center { - recenter(&mut pos, center); - } + _ => { + let cs = AdaptiveCoolingScheme::new(step); + evolve( + graph, vpos, fixed, f_a, f_r, cs, num_iter, tol, weights, + scale, center, + ) + } + }; - Pos2DMapping { + Ok(Pos2DMapping { pos_map: graph .node_indices() .map(|n| { @@ -458,52 +380,5 @@ pub fn shell_layout( (n, pos[n]) }) .collect(), - } -} - -pub fn spiral_layout( - graph: &StableGraph, - scale: Option, - center: Option, - resolution: Option, - equidistant: Option, -) -> Pos2DMapping { - let node_num = graph.node_count(); - let mut pos: Vec = Vec::with_capacity(node_num); - - let ros = resolution.unwrap_or(0.35); - - if node_num == 1 { - pos.push([0.0, 0.0]); - } else if equidistant == Some(true) { - let mut theta: f64 = ros; - let chord = 1.0; - let step = 0.5; - for _ in 0..node_num { - let r = step * theta; - theta += chord / r; - pos.push([theta.cos() * r, theta.sin() * r]); - } - } else { - let mut angle: f64 = 0.0; - let mut dist = 0.0; - let step = 1.0; - for _ in 0..node_num { - pos.push([dist * angle.cos(), dist * angle.sin()]); - dist += step; - angle += ros; - } - } - - if let Some(scale) = scale { - rescale(&mut pos, scale, (0..node_num).collect()); - } - - if let Some(center) = center { - recenter(&mut pos, center); - } - - Pos2DMapping { - pos_map: graph.node_indices().map(|n| n.index()).zip(pos).collect(), - } + }) } diff --git a/src/lib.rs b/src/lib.rs index eed6bf106c..431df6705f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,1467 +12,62 @@ #![allow(clippy::float_cmp)] -mod astar; +mod coloring; +mod connectivity; +mod dag_algo; mod digraph; -mod dijkstra; mod dot_utils; mod generators; mod graph; mod isomorphism; mod iterators; -mod k_shortest_path; mod layout; -mod max_weight_matching; +mod matching; +mod random_graph; +mod shortest_path; mod steiner_tree; +mod transitivity; +mod traversal; +mod tree; mod union; -use std::cmp::{Ordering, Reverse}; -use std::collections::{BTreeSet, BinaryHeap}; -use std::sync::RwLock; +use coloring::*; +use connectivity::*; +use dag_algo::*; +use isomorphism::*; +use layout::*; +use matching::*; +use random_graph::*; +use shortest_path::*; +use steiner_tree::*; +use transitivity::*; +use traversal::*; +use tree::*; +use union::*; -use ahash::RandomState; -use hashbrown::{HashMap, HashSet}; -use indexmap::IndexMap; +use hashbrown::HashMap; use pyo3::create_exception; -use pyo3::exceptions::{PyException, PyIndexError, PyValueError}; +use pyo3::exceptions::PyException; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList}; use pyo3::wrap_pyfunction; use pyo3::wrap_pymodule; use pyo3::Python; -use petgraph::algo; use petgraph::graph::NodeIndex; use petgraph::prelude::*; -use petgraph::stable_graph::EdgeReference; -use petgraph::unionfind::UnionFind; use petgraph::visit::{ - Bfs, Data, EdgeIndexable, GraphBase, GraphProp, IntoEdgeReferences, - IntoNeighbors, IntoNodeIdentifiers, NodeCount, NodeIndexable, Reversed, - VisitMap, Visitable, + Data, GraphBase, GraphProp, IntoEdgeReferences, IntoNodeIdentifiers, + NodeCount, NodeIndexable, }; -use petgraph::EdgeType; - -use ndarray::prelude::*; -use num_bigint::{BigUint, ToBigUint}; -use num_traits::{Num, Zero}; -use numpy::IntoPyArray; -use rand::distributions::{Distribution, Uniform}; -use rand::prelude::*; -use rand_pcg::Pcg64; -use rayon::prelude::*; use crate::generators::PyInit_generators; -use crate::iterators::{ - AllPairsPathLengthMapping, AllPairsPathMapping, EdgeList, NodeIndices, - NodesCountMapping, PathLengthMapping, PathMapping, Pos2DMapping, - WeightedEdgeList, -}; -use steiner_tree::*; -trait NodesRemoved { +pub trait NodesRemoved { fn nodes_removed(&self) -> bool; } -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, None) { - Ok(nodes) => nodes, - Err(_err) => { - return Err(DAGHasCycle::new_err("Sort encountered a cycle")) - } - }; - 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; - Ok((path, path_weight)) -} - -/// Find the longest path in a DAG -/// -/// :param PyDiGraph graph: The graph to find the longest path on. The input -/// object must be a DAG without a cycle. -/// :param weight_fn: A python callable that if set will be passed the 3 -/// positional arguments, the source node, the target node, and the edge -/// weight for each edge as the function traverses the graph. It is expected -/// to return an unsigned integer weight for that edge. For example, -/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be -/// use to just use an integer edge weight. It's also worth noting that this -/// function traverses in topological order and only checks incoming edges to -/// each node. -/// -/// :returns: The node indices of the longest path on the DAG -/// :rtype: NodeIndices -/// -/// :raises Exception: If an unexpected error occurs or a path can't be found -/// :raises DAGHasCycle: If the input PyDiGraph has a cycle -#[pyfunction] -#[pyo3(text_signature = "(graph, /, weight_fn=None)")] -fn dag_longest_path( - py: Python, - graph: &digraph::PyDiGraph, - weight_fn: Option, -) -> PyResult { - let edge_weight_callable = - |source: usize, target: usize, weight: &PyObject| -> PyResult { - match &weight_fn { - Some(weight_fn) => { - let res = weight_fn.call1(py, (source, target, weight))?; - res.extract(py) - } - None => Ok(1), - } - }; - Ok(NodeIndices { - nodes: longest_path(graph, edge_weight_callable)?.0, - }) -} - -/// Find the length of the longest path in a DAG -/// -/// :param PyDiGraph graph: The graph to find the longest path on. The input -/// object must be a DAG without a cycle. -/// :param weight_fn: A python callable that if set will be passed the 3 -/// positional arguments, the source node, the target node, and the edge -/// weight for each edge as the function traverses the graph. It is expected -/// to return an unsigned integer weight for that edge. For example, -/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be -/// use to just use an integer edge weight. It's also worth noting that this -/// function traverses in topological order and only checks incoming edges to -/// each node. -/// -/// :returns: The longest path length on the DAG -/// :rtype: int -/// -/// :raises Exception: If an unexpected error occurs or a path can't be found -/// :raises DAGHasCycle: If the input PyDiGraph has a cycle -#[pyfunction] -#[pyo3(text_signature = "(graph, /, weight_fn=None)")] -fn dag_longest_path_length( - py: Python, - graph: &digraph::PyDiGraph, - weight_fn: Option, -) -> PyResult { - let edge_weight_callable = - |source: usize, target: usize, weight: &PyObject| -> PyResult { - match &weight_fn { - Some(weight_fn) => { - let res = weight_fn.call1(py, (source, target, weight))?; - res.extract(py) - } - None => Ok(1), - } - }; - let (_, path_weight) = longest_path(graph, edge_weight_callable)?; - Ok(path_weight) -} - -/// Find the weighted longest path in a DAG -/// -/// This function differs from :func:`retworkx.dag_longest_path` in that -/// this function requires a ``weight_fn`` parameter, and the ``weight_fn`` is -/// expected to return a ``float`` not an ``int``. -/// -/// :param PyDiGraph graph: The graph to find the longest path on. The input -/// object must be a DAG without a cycle. -/// :param weight_fn: A python callable that will be passed the 3 -/// positional arguments, the source node, the target node, and the edge -/// weight for each edge as the function traverses the graph. It is expected -/// to return a float weight for that edge. For example, -/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be -/// used to just use a float edge weight. It's also worth noting that this -/// function traverses in topological order and only checks incoming edges to -/// each node. -/// -/// :returns: The node indices of the longest path on the DAG -/// :rtype: NodeIndices -/// -/// :raises Exception: If an unexpected error occurs or a path can't be found -/// :raises DAGHasCycle: If the input PyDiGraph has a cycle -#[pyfunction] -#[pyo3(text_signature = "(graph, weight_fn, /)")] -fn dag_weighted_longest_path( - py: Python, - graph: &digraph::PyDiGraph, - weight_fn: PyObject, -) -> PyResult { - let edge_weight_callable = - |source: usize, target: usize, weight: &PyObject| -> PyResult { - let res = weight_fn.call1(py, (source, target, weight))?; - let float_res: f64 = res.extract(py)?; - if float_res.is_nan() { - return Err(PyValueError::new_err( - "NaN is not a valid edge weight", - )); - } - Ok(float_res) - }; - Ok(NodeIndices { - nodes: longest_path(graph, edge_weight_callable)?.0, - }) -} - -/// Find the length of the weighted longest path in a DAG -/// -/// This function differs from :func:`retworkx.dag_longest_path_length` in that -/// this function requires a ``weight_fn`` parameter, and the ``weight_fn`` is -/// expected to return a ``float`` not an ``int``. -/// -/// :param PyDiGraph graph: The graph to find the longest path on. The input -/// object must be a DAG without a cycle. -/// :param weight_fn: A python callable that will be passed the 3 -/// positional arguments, the source node, the target node, and the edge -/// weight for each edge as the function traverses the graph. It is expected -/// to return a float weight for that edge. For example, -/// ``dag_longest_path(graph, lambda: _, __, weight: weight)`` could be -/// used to just use a float edge weight. It's also worth noting that this -/// function traverses in topological order and only checks incoming edges to -/// each node. -/// -/// :returns: The longest path length on the DAG -/// :rtype: float -/// -/// :raises Exception: If an unexpected error occurs or a path can't be found -/// :raises DAGHasCycle: If the input PyDiGraph has a cycle -#[pyfunction] -#[pyo3(text_signature = "(graph, weight_fn, /)")] -fn dag_weighted_longest_path_length( - py: Python, - graph: &digraph::PyDiGraph, - weight_fn: PyObject, -) -> PyResult { - let edge_weight_callable = - |source: usize, target: usize, weight: &PyObject| -> PyResult { - let res = weight_fn.call1(py, (source, target, weight))?; - let float_res: f64 = res.extract(py)?; - if float_res.is_nan() { - return Err(PyValueError::new_err( - "NaN is not a valid edge weight", - )); - } - Ok(float_res) - }; - let (_, path_weight) = longest_path(graph, edge_weight_callable)?; - Ok(path_weight) -} - -/// Find the number of weakly connected components in a DAG. -/// -/// :param PyDiGraph graph: The graph to find the number of weakly connected -/// components on -/// -/// :returns: The number of weakly connected components in the DAG -/// :rtype: int -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn number_weakly_connected_components(graph: &digraph::PyDiGraph) -> usize { - algo::connected_components(graph) -} - -/// Find the weakly connected components in a directed graph -/// -/// :param PyDiGraph graph: The graph to find the weakly connected components -/// in -/// -/// :returns: A list of sets where each set it a weakly connected component of -/// the graph -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -pub fn weakly_connected_components( - graph: &digraph::PyDiGraph, -) -> Vec> { - let mut seen: HashSet = - HashSet::with_capacity(graph.node_count()); - let mut out_vec: Vec> = Vec::new(); - for node in graph.graph.node_indices() { - if !seen.contains(&node) { - // BFS node generator - let mut component_set: BTreeSet = BTreeSet::new(); - let mut bfs_seen: HashSet = HashSet::new(); - let mut next_level: HashSet = HashSet::new(); - next_level.insert(node); - while !next_level.is_empty() { - let this_level = next_level; - next_level = HashSet::new(); - for bfs_node in this_level { - if !bfs_seen.contains(&bfs_node) { - component_set.insert(bfs_node.index()); - bfs_seen.insert(bfs_node); - for neighbor in - graph.graph.neighbors_undirected(bfs_node) - { - next_level.insert(neighbor); - } - } - } - } - out_vec.push(component_set); - seen.extend(bfs_seen); - } - } - out_vec -} - -/// Check if the graph is weakly connected -/// -/// :param PyDiGraph graph: The graph to check if it is weakly connected -/// -/// :returns: Whether the graph is weakly connected or not -/// :rtype: bool -/// -/// :raises NullGraph: If an empty graph is passed in -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -pub fn is_weakly_connected(graph: &digraph::PyDiGraph) -> PyResult { - if graph.graph.node_count() == 0 { - return Err(NullGraph::new_err("Invalid operation on a NullGraph")); - } - Ok(weakly_connected_components(graph)[0].len() == graph.graph.node_count()) -} - -/// Check that the PyDiGraph or PyDAG doesn't have a cycle -/// -/// :param PyDiGraph graph: The graph to check for cycles -/// -/// :returns: ``True`` if there are no cycles in the input graph, ``False`` -/// if there are cycles -/// :rtype: bool -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn is_directed_acyclic_graph(graph: &digraph::PyDiGraph) -> bool { - match algo::toposort(graph, None) { - Ok(_nodes) => true, - Err(_err) => false, - } -} - -/// Return a new PyDiGraph by forming a union from two input PyDiGraph objects -/// -/// The algorithm in this function operates in three phases: -/// -/// 1. Add all the nodes from ``second`` into ``first``. operates in O(n), -/// with n being number of nodes in `b`. -/// 2. Merge nodes from ``second`` over ``first`` given that: -/// -/// - The ``merge_nodes`` is ``True``. operates in O(n^2), with n being the -/// number of nodes in ``second``. -/// - The respective node in ``second`` and ``first`` share the same -/// weight/data payload. -/// -/// 3. Adds all the edges from ``second`` to ``first``. If the ``merge_edges`` -/// parameter is ``True`` and the respective edge in ``second`` and -/// first`` share the same weight/data payload they will be merged -/// together. -/// -/// :param PyDiGraph first: The first directed graph object -/// :param PyDiGraph second: The second directed graph object -/// :param bool merge_nodes: If set to ``True`` nodes will be merged between -/// ``second`` and ``first`` if the weights are equal. -/// :param bool merge_edges: If set to ``True`` edges will be merged between -/// ``second`` and ``first`` if the weights are equal. -/// -/// :returns: A new PyDiGraph object that is the union of ``second`` and -/// ``first``. It's worth noting the weight/data payload objects are -/// passed by reference from ``first`` and ``second`` to this new object. -/// :rtype: PyDiGraph -#[pyfunction] -#[pyo3(text_signature = "(first, second, merge_nodes, merge_edges, /)")] -fn digraph_union( - py: Python, - first: &digraph::PyDiGraph, - second: &digraph::PyDiGraph, - merge_nodes: bool, - merge_edges: bool, -) -> PyResult { - let res = - union::digraph_union(py, first, second, merge_nodes, merge_edges)?; - Ok(res) -} - -/// Determine if 2 directed graphs are isomorphic -/// -/// This checks if 2 graphs are isomorphic both structurally and also -/// comparing the node data and edge data using the provided matcher functions. -/// The matcher function takes in 2 data objects and will compare them. A simple -/// example that checks if they're just equal would be:: -/// -/// graph_a = retworkx.PyDiGraph() -/// graph_b = retworkx.PyDiGraph() -/// retworkx.is_isomorphic(graph_a, graph_b, -/// lambda x, y: x == y) -/// -/// .. note:: -/// -/// For better performance on large graphs, consider setting `id_order=False`. -/// -/// :param PyDiGraph first: The first graph to compare -/// :param PyDiGraph second: The second graph to compare -/// :param callable node_matcher: A python callable object that takes 2 positional -/// one for each node data object. If the return of this -/// function evaluates to True then the nodes passed to it are vieded -/// as matching. -/// :param callable edge_matcher: A python callable object that takes 2 positional -/// one for each edge data object. If the return of this -/// function evaluates to True then the edges passed to it are vieded -/// as matching. -/// :param bool id_order: If set to ``False`` this function will use a -/// heuristic matching order based on [VF2]_ paper. Otherwise it will -/// default to matching the nodes in order specified by their ids. -/// -/// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are -/// not. -/// :rtype: bool -#[pyfunction(id_order = "true")] -#[pyo3( - text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" -)] -fn digraph_is_isomorphic( - py: Python, - first: &digraph::PyDiGraph, - second: &digraph::PyDiGraph, - node_matcher: Option, - edge_matcher: Option, - id_order: bool, -) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( - py, - &first.graph, - &second.graph, - compare_nodes, - compare_edges, - id_order, - Ordering::Equal, - true, - )?; - Ok(res) -} - -/// Determine if 2 undirected graphs are isomorphic -/// -/// This checks if 2 graphs are isomorphic both structurally and also -/// comparing the node data and edge data using the provided matcher functions. -/// The matcher function takes in 2 data objects and will compare them. A simple -/// example that checks if they're just equal would be:: -/// -/// graph_a = retworkx.PyGraph() -/// graph_b = retworkx.PyGraph() -/// retworkx.is_isomorphic(graph_a, graph_b, -/// lambda x, y: x == y) -/// -/// .. note:: -/// -/// For better performance on large graphs, consider setting `id_order=False`. -/// -/// :param PyGraph first: The first graph to compare -/// :param PyGraph second: The second graph to compare -/// :param callable node_matcher: A python callable object that takes 2 positional -/// one for each node data object. If the return of this -/// function evaluates to True then the nodes passed to it are vieded -/// as matching. -/// :param callable edge_matcher: A python callable object that takes 2 positional -/// one for each edge data object. If the return of this -/// function evaluates to True then the edges passed to it are vieded -/// as matching. -/// :param bool (default=True) id_order: If set to true, the algorithm matches the -/// nodes in order specified by their ids. Otherwise, it uses a heuristic -/// matching order based in [VF2]_ paper. -/// -/// :returns: ``True`` if the 2 graphs are isomorphic ``False`` if they are -/// not. -/// :rtype: bool -#[pyfunction(id_order = "true")] -#[pyo3( - text_signature = "(first, second, node_matcher=None, edge_matcher=None, id_order=True, /)" -)] -fn graph_is_isomorphic( - py: Python, - first: &graph::PyGraph, - second: &graph::PyGraph, - node_matcher: Option, - edge_matcher: Option, - id_order: bool, -) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( - py, - &first.graph, - &second.graph, - compare_nodes, - compare_edges, - id_order, - Ordering::Equal, - true, - )?; - Ok(res) -} - -/// Determine if 2 directed graphs are subgraph - isomorphic -/// -/// This checks if 2 graphs are subgraph isomorphic both structurally and also -/// comparing the node data and edge data using the provided matcher functions. -/// The matcher function takes in 2 data objects and will compare them. -/// Since there is an ambiguity in the term 'subgraph', do note that we check -/// for an node-induced subgraph if argument `induced` is set to `True`. If it is -/// set to `False`, we check for a non induced subgraph, meaning the second graph -/// can have fewer edges than the subgraph of the first. By default it's `True`. A -/// simple example that checks if they're just equal would be:: -/// -/// graph_a = retworkx.PyDiGraph() -/// graph_b = retworkx.PyDiGraph() -/// retworkx.is_subgraph_isomorphic(graph_a, graph_b, -/// lambda x, y: x == y) -/// -/// .. note:: -/// -/// For better performance on large graphs, consider setting `id_order=False`. -/// -/// :param PyDiGraph first: The first graph to compare -/// :param PyDiGraph second: The second graph to compare -/// :param callable node_matcher: A python callable object that takes 2 positional -/// one for each node data object. If the return of this -/// function evaluates to True then the nodes passed to it are vieded -/// as matching. -/// :param callable edge_matcher: A python callable object that takes 2 positional -/// one for each edge data object. If the return of this -/// function evaluates to True then the edges passed to it are vieded -/// as matching. -/// :param bool id_order: If set to ``True`` this function will match the nodes -/// in order specified by their ids. Otherwise it will default to a heuristic -/// matching order based on [VF2]_ paper. -/// :param bool induced: If set to ``True`` this function will check the existence -/// of a node-induced subgraph of first isomorphic to second graph. -/// Default: ``True``. -/// -/// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, -/// ``False`` if there is not. -/// :rtype: bool -#[pyfunction(id_order = "false", induced = "true")] -#[pyo3( - text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" -)] -fn digraph_is_subgraph_isomorphic( - py: Python, - first: &digraph::PyDiGraph, - second: &digraph::PyDiGraph, - node_matcher: Option, - edge_matcher: Option, - id_order: bool, - induced: bool, -) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( - py, - &first.graph, - &second.graph, - compare_nodes, - compare_edges, - id_order, - Ordering::Greater, - induced, - )?; - Ok(res) -} - -/// Determine if 2 undirected graphs are subgraph - isomorphic -/// -/// This checks if 2 graphs are subgraph isomorphic both structurally and also -/// comparing the node data and edge data using the provided matcher functions. -/// The matcher function takes in 2 data objects and will compare them. -/// Since there is an ambiguity in the term 'subgraph', do note that we check -/// for an node-induced subgraph if argument `induced` is set to `True`. If it is -/// set to `False`, we check for a non induced subgraph, meaning the second graph -/// can have fewer edges than the subgraph of the first. By default it's `True`. A -/// simple example that checks if they're just equal would be:: -/// -/// graph_a = retworkx.PyGraph() -/// graph_b = retworkx.PyGraph() -/// retworkx.is_subgraph_isomorphic(graph_a, graph_b, -/// lambda x, y: x == y) -/// -/// .. note:: -/// -/// For better performance on large graphs, consider setting `id_order=False`. -/// -/// :param PyGraph first: The first graph to compare -/// :param PyGraph second: The second graph to compare -/// :param callable node_matcher: A python callable object that takes 2 positional -/// one for each node data object. If the return of this -/// function evaluates to True then the nodes passed to it are vieded -/// as matching. -/// :param callable edge_matcher: A python callable object that takes 2 positional -/// one for each edge data object. If the return of this -/// function evaluates to True then the edges passed to it are vieded -/// as matching. -/// :param bool id_order: If set to ``True`` this function will match the nodes -/// in order specified by their ids. Otherwise it will default to a heuristic -/// matching order based on [VF2]_ paper. -/// :param bool induced: If set to ``True`` this function will check the existence -/// of a node-induced subgraph of first isomorphic to second graph. -/// Default: ``True``. -/// -/// :returns: ``True`` if there is a subgraph of `first` isomorphic to `second`, -/// ``False`` if there is not. -/// :rtype: bool -#[pyfunction(id_order = "false", induced = "true")] -#[pyo3( - text_signature = "(first, second, /, node_matcher=None, edge_matcher=None, id_order=False, induced=True)" -)] -fn graph_is_subgraph_isomorphic( - py: Python, - first: &graph::PyGraph, - second: &graph::PyGraph, - node_matcher: Option, - edge_matcher: Option, - id_order: bool, - induced: bool, -) -> PyResult { - let compare_nodes = node_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let compare_edges = edge_matcher.map(|f| { - move |a: &PyObject, b: &PyObject| -> PyResult { - let res = f.call1(py, (a, b))?; - Ok(res.is_true(py).unwrap()) - } - }); - - let res = isomorphism::is_isomorphic( - py, - &first.graph, - &second.graph, - compare_nodes, - compare_edges, - id_order, - Ordering::Greater, - induced, - )?; - Ok(res) -} - -/// Return the topological sort of node indexes from the provided graph -/// -/// :param PyDiGraph graph: The DAG to get the topological sort on -/// -/// :returns: A list of node indices topologically sorted. -/// :rtype: NodeIndices -/// -/// :raises DAGHasCycle: if a cycle is encountered while sorting the graph -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn topological_sort(graph: &digraph::PyDiGraph) -> PyResult { - let nodes = match algo::toposort(graph, None) { - Ok(nodes) => nodes, - Err(_err) => { - return Err(DAGHasCycle::new_err("Sort encountered a cycle")) - } - }; - Ok(NodeIndices { - nodes: nodes.iter().map(|node| node.index()).collect(), - }) -} - -fn dfs_edges( - graph: G, - source: Option, - edge_count: usize, -) -> Vec<(usize, usize)> -where - G: GraphBase - + IntoNodeIdentifiers - + NodeIndexable - + IntoNeighbors - + NodeCount - + Visitable, - ::Map: VisitMap, -{ - let nodes: Vec = match source { - Some(start) => vec![NodeIndex::new(start)], - None => graph - .node_identifiers() - .map(|ind| NodeIndex::new(graph.to_index(ind))) - .collect(), - }; - let node_count = graph.node_count(); - let mut visited: HashSet = HashSet::with_capacity(node_count); - let mut out_vec: Vec<(usize, usize)> = Vec::with_capacity(edge_count); - for start in nodes { - if visited.contains(&start) { - continue; - } - visited.insert(start); - let mut children: Vec = graph.neighbors(start).collect(); - children.reverse(); - let mut stack: Vec<(NodeIndex, Vec)> = - vec![(start, children)]; - // Used to track the last position in children vec across iterations - let mut index_map: HashMap = - HashMap::with_capacity(node_count); - index_map.insert(start, 0); - while !stack.is_empty() { - let temp_parent = stack.last().unwrap(); - let parent = temp_parent.0; - let children = temp_parent.1.clone(); - let count = *index_map.get(&parent).unwrap(); - let mut found = false; - let mut index = count; - for child in &children[index..] { - index += 1; - if !visited.contains(child) { - out_vec.push((parent.index(), child.index())); - visited.insert(*child); - let mut grandchildren: Vec = - graph.neighbors(*child).collect(); - grandchildren.reverse(); - stack.push((*child, grandchildren)); - index_map.insert(*child, 0); - *index_map.get_mut(&parent).unwrap() = index; - found = true; - break; - } - } - if !found || children.is_empty() { - stack.pop(); - } - } - } - out_vec -} - -/// Get edge list in depth first order -/// -/// :param PyDiGraph graph: The graph to get the DFS edge list from -/// :param int source: An optional node index to use as the starting node -/// for the depth-first search. The edge list will only return edges in -/// the components reachable from this index. If this is not specified -/// then a source will be chosen arbitrarly and repeated until all -/// components of the graph are searched. -/// -/// :returns: A list of edges as a tuple of the form ``(source, target)`` in -/// depth-first order -/// :rtype: EdgeList -#[pyfunction] -#[pyo3(text_signature = "(graph, /, source=None)")] -fn digraph_dfs_edges( - graph: &digraph::PyDiGraph, - source: Option, -) -> EdgeList { - EdgeList { - edges: dfs_edges(graph, source, graph.graph.edge_count()), - } -} - -/// Get edge list in depth first order -/// -/// :param PyGraph graph: The graph to get the DFS edge list from -/// :param int source: An optional node index to use as the starting node -/// for the depth-first search. The edge list will only return edges in -/// the components reachable from this index. If this is not specified -/// then a source will be chosen arbitrarly and repeated until all -/// components of the graph are searched. -/// -/// :returns: A list of edges as a tuple of the form ``(source, target)`` in -/// depth-first order -/// :rtype: EdgeList -#[pyfunction] -#[pyo3(text_signature = "(graph, /, source=None)")] -fn graph_dfs_edges(graph: &graph::PyGraph, source: Option) -> EdgeList { - EdgeList { - edges: dfs_edges(graph, source, graph.graph.edge_count()), - } -} - -/// Return successors in a breadth-first-search from a source node. -/// -/// The return format is ``[(Parent Node, [Children Nodes])]`` in a bfs order -/// from the source node provided. -/// -/// :param PyDiGraph graph: The DAG to get the bfs_successors from -/// :param int node: The index of the dag node to get the bfs successors for -/// -/// :returns: A list of nodes's data and their children in bfs order. The -/// BFSSuccessors class that is returned is a custom container class that -/// implements the sequence protocol. This can be used as a python list -/// with index based access. -/// :rtype: BFSSuccessors -#[pyfunction] -#[pyo3(text_signature = "(graph, node, /)")] -fn bfs_successors( - py: Python, - graph: &digraph::PyDiGraph, - node: usize, -) -> iterators::BFSSuccessors { - let index = NodeIndex::new(node); - let mut bfs = Bfs::new(graph, index); - let mut out_list: Vec<(PyObject, Vec)> = - Vec::with_capacity(graph.node_count()); - while let Some(nx) = bfs.next(graph) { - let children = graph - .graph - .neighbors_directed(nx, petgraph::Direction::Outgoing); - let mut succesors: Vec = Vec::new(); - for succ in children { - succesors - .push(graph.graph.node_weight(succ).unwrap().clone_ref(py)); - } - if !succesors.is_empty() { - out_list.push(( - graph.graph.node_weight(nx).unwrap().clone_ref(py), - succesors, - )); - } - } - iterators::BFSSuccessors { - bfs_successors: out_list, - } -} - -/// Return the ancestors of a node in a graph. -/// -/// This differs from :meth:`PyDiGraph.predecessors` method in that -/// ``predecessors`` returns only nodes with a direct edge into the provided -/// node. While this function returns all nodes that have a path into the -/// provided node. -/// -/// :param PyDiGraph graph: The graph to get the descendants from -/// :param int node: The index of the graph node to get the ancestors for -/// -/// :returns: A list of node indexes of ancestors of provided node. -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, node, /)")] -fn ancestors(graph: &digraph::PyDiGraph, node: usize) -> HashSet { - let index = NodeIndex::new(node); - let mut out_set: HashSet = HashSet::new(); - let reverse_graph = Reversed(graph); - let res = algo::dijkstra(reverse_graph, index, None, |_| 1); - for n in res.keys() { - let n_int = n.index(); - out_set.insert(n_int); - } - out_set.remove(&node); - out_set -} - -/// Return the descendants of a node in a graph. -/// -/// This differs from :meth:`PyDiGraph.successors` method in that -/// ``successors``` returns only nodes with a direct edge out of the provided -/// node. While this function returns all nodes that have a path from the -/// provided node. -/// -/// :param PyDiGraph graph: The graph to get the descendants from -/// :param int node: The index of the graph node to get the descendants for -/// -/// :returns: A list of node indexes of descendants of provided node. -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, node, /)")] -fn descendants(graph: &digraph::PyDiGraph, node: usize) -> HashSet { - let index = NodeIndex::new(node); - let mut out_set: HashSet = HashSet::new(); - let res = algo::dijkstra(graph, index, None, |_| 1); - for n in res.keys() { - let n_int = n.index(); - out_set.insert(n_int); - } - out_set.remove(&node); - out_set -} - -/// Get the lexicographical topological sorted nodes from the provided DAG -/// -/// This function returns a list of nodes data in a graph lexicographically -/// topologically sorted using the provided key function. -/// -/// :param PyDiGraph dag: The DAG to get the topological sorted nodes from -/// :param callable key: key is a python function or other callable that -/// gets passed a single argument the node data from the graph and is -/// expected to return a string which will be used for sorting. -/// -/// :returns: A list of node's data lexicographically topologically sorted. -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(dag, key, /)")] -fn lexicographical_topological_sort( - py: Python, - dag: &digraph::PyDiGraph, - key: PyObject, -) -> PyResult { - let key_callable = |a: &PyObject| -> PyResult { - let res = key.call1(py, (a,))?; - Ok(res.to_object(py)) - }; - // HashMap of node_index indegree - let node_count = dag.node_count(); - let mut in_degree_map: HashMap = - HashMap::with_capacity(node_count); - for node in dag.graph.node_indices() { - in_degree_map.insert(node, dag.in_degree(node.index())); - } - - #[derive(Clone, Eq, PartialEq)] - struct State { - key: String, - node: NodeIndex, - } - - impl Ord for State { - fn cmp(&self, other: &State) -> Ordering { - // Notice that the we flip the ordering on costs. - // In case of a tie we compare positions - this step is necessary - // to make implementations of `PartialEq` and `Ord` consistent. - other - .key - .cmp(&self.key) - .then_with(|| other.node.index().cmp(&self.node.index())) - } - } - - // `PartialOrd` needs to be implemented as well. - impl PartialOrd for State { - fn partial_cmp(&self, other: &State) -> Option { - Some(self.cmp(other)) - } - } - let mut zero_indegree = BinaryHeap::with_capacity(node_count); - for (node, degree) in in_degree_map.iter() { - if *degree == 0 { - let map_key_raw = key_callable(&dag.graph[*node])?; - let map_key: String = map_key_raw.extract(py)?; - zero_indegree.push(State { - key: map_key, - node: *node, - }); - } - } - let mut out_list: Vec<&PyObject> = Vec::with_capacity(node_count); - let dir = petgraph::Direction::Outgoing; - while let Some(State { node, .. }) = zero_indegree.pop() { - let neighbors = dag.graph.neighbors_directed(node, dir); - for child in neighbors { - let child_degree = in_degree_map.get_mut(&child).unwrap(); - *child_degree -= 1; - if *child_degree == 0 { - let map_key_raw = key_callable(&dag.graph[child])?; - let map_key: String = map_key_raw.extract(py)?; - zero_indegree.push(State { - key: map_key, - node: child, - }); - in_degree_map.remove(&child); - } - } - out_list.push(&dag.graph[node]) - } - Ok(PyList::new(py, out_list).into()) -} - -/// Color a PyGraph using a largest_first strategy greedy graph coloring. -/// -/// :param PyGraph: The input PyGraph object to color -/// -/// :returns: A dictionary where keys are node indices and the value is -/// the color -/// :rtype: dict -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn graph_greedy_color( - py: Python, - graph: &graph::PyGraph, -) -> PyResult { - let mut colors: IndexMap = - IndexMap::with_hasher(RandomState::default()); - let mut node_vec: Vec = graph.graph.node_indices().collect(); - let mut sort_map: HashMap = - HashMap::with_capacity(graph.node_count()); - for k in node_vec.iter() { - sort_map.insert(*k, graph.graph.edges(*k).count()); - } - node_vec.par_sort_by_key(|k| Reverse(sort_map.get(k))); - for u_index in node_vec { - let mut neighbor_colors: HashSet = HashSet::new(); - for edge in graph.graph.edges(u_index) { - let target = edge.target().index(); - let existing_color = match colors.get(&target) { - Some(node) => node, - None => continue, - }; - neighbor_colors.insert(*existing_color); - } - let mut count: usize = 0; - loop { - if !neighbor_colors.contains(&count) { - break; - } - count += 1; - } - colors.insert(u_index.index(), count); - } - let out_dict = PyDict::new(py); - for (index, color) in colors { - out_dict.set_item(index, color)?; - } - Ok(out_dict.into()) -} - -/// Compute the length of the kth shortest path -/// -/// Computes the lengths of the kth shortest path from ``start`` to every -/// reachable node. -/// -/// Computes in :math:`O(k * (|E| + |V|*log(|V|)))` time (average). -/// -/// :param PyGraph graph: The graph to find the shortest paths in -/// :param int start: The node index to find the shortest paths from -/// :param int k: The kth shortest path to find the lengths of -/// :param edge_cost: A python callable that will receive an edge payload and -/// return a float for the cost of that eedge -/// :param int goal: An optional goal node index, if specified the output -/// dictionary -/// -/// :returns: A dict of lengths where the key is the destination node index and -/// the value is the length of the path. -/// :rtype: PathLengthMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, start, k, edge_cost, /, goal=None)")] -fn digraph_k_shortest_path_lengths( - py: Python, - graph: &digraph::PyDiGraph, - start: usize, - k: usize, - edge_cost: PyObject, - goal: Option, -) -> PyResult { - let out_goal = goal.map(NodeIndex::new); - let edge_cost_callable = |edge: &PyObject| -> PyResult { - let res = edge_cost.call1(py, (edge,))?; - res.extract(py) - }; - - let out_map = k_shortest_path::k_shortest_path( - graph, - NodeIndex::new(start), - out_goal, - k, - edge_cost_callable, - )?; - Ok(PathLengthMapping { - path_lengths: out_map - .iter() - .filter_map(|(k, v)| { - let k_int = k.index(); - if goal.is_some() && goal.unwrap() != k_int { - None - } else { - Some((k_int, *v)) - } - }) - .collect(), - }) -} - -/// Compute the length of the kth shortest path -/// -/// Computes the lengths of the kth shortest path from ``start`` to every -/// reachable node. -/// -/// Computes in :math:`O(k * (|E| + |V|*log(|V|)))` time (average). -/// -/// :param PyGraph graph: The graph to find the shortest paths in -/// :param int start: The node index to find the shortest paths from -/// :param int k: The kth shortest path to find the lengths of -/// :param edge_cost: A python callable that will receive an edge payload and -/// return a float for the cost of that eedge -/// :param int goal: An optional goal node index, if specified the output -/// dictionary -/// -/// :returns: A dict of lengths where the key is the destination node index and -/// the value is the length of the path. -/// :rtype: PathLengthMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, start, k, edge_cost, /, goal=None)")] -fn graph_k_shortest_path_lengths( - py: Python, - graph: &graph::PyGraph, - start: usize, - k: usize, - edge_cost: PyObject, - goal: Option, -) -> PyResult { - let out_goal = goal.map(NodeIndex::new); - let edge_cost_callable = |edge: &PyObject| -> PyResult { - let res = edge_cost.call1(py, (edge,))?; - res.extract(py) - }; - - let out_map = k_shortest_path::k_shortest_path( - graph, - NodeIndex::new(start), - out_goal, - k, - edge_cost_callable, - )?; - Ok(PathLengthMapping { - path_lengths: out_map - .iter() - .filter_map(|(k, v)| { - let k_int = k.index(); - if goal.is_some() && goal.unwrap() != k_int { - None - } else { - Some((k_int, *v)) - } - }) - .collect(), - }) -} - -fn _floyd_warshall( - py: Python, - graph: &StableGraph, - weight_fn: Option, - as_undirected: bool, - default_weight: f64, - parallel_threshold: usize, -) -> PyResult { - if graph.node_count() == 0 { - return Ok(AllPairsPathLengthMapping { - path_lengths: HashMap::new(), - }); - } else if graph.edge_count() == 0 { - return Ok(AllPairsPathLengthMapping { - path_lengths: graph - .node_indices() - .map(|i| { - ( - i.index(), - PathLengthMapping { - path_lengths: HashMap::new(), - }, - ) - }) - .collect(), - }); - } - let n = graph.node_bound(); - - // Allocate empty matrix - let mut mat: Vec> = vec![HashMap::new(); n]; - - // Set diagonal to 0 - for i in 0..n { - if let Some(row_i) = mat.get_mut(i) { - row_i.entry(i).or_insert(0.0); - } - } - - // Utility to set row_i[j] = min(row_i[j], m_ij) - macro_rules! insert_or_minimize { - ($row_i: expr, $j: expr, $m_ij: expr) => {{ - $row_i - .entry($j) - .and_modify(|e| { - if $m_ij < *e { - *e = $m_ij; - } - }) - .or_insert($m_ij); - }}; - } - - // Build adjacency matrix - for edge in graph.edge_references() { - let i = NodeIndexable::to_index(&graph, edge.source()); - let j = NodeIndexable::to_index(&graph, edge.target()); - let weight = edge.weight().clone(); - - let edge_weight = - weight_callable(py, &weight_fn, &weight, default_weight)?; - if let Some(row_i) = mat.get_mut(i) { - insert_or_minimize!(row_i, j, edge_weight); - } - if as_undirected { - if let Some(row_j) = mat.get_mut(j) { - insert_or_minimize!(row_j, i, edge_weight); - } - } - } - - // Perform the Floyd-Warshall algorithm. - // In each loop, this finds the shortest path from point i - // to point j using intermediate nodes 0..k - if n < parallel_threshold { - for k in 0..n { - let row_k = mat.get(k).cloned().unwrap_or_default(); - mat.iter_mut().for_each(|row_i| { - if let Some(m_ik) = row_i.get(&k).cloned() { - for (j, m_kj) in row_k.iter() { - let m_ikj = m_ik + *m_kj; - insert_or_minimize!(row_i, *j, m_ikj); - } - } - }) - } - } else { - for k in 0..n { - let row_k = mat.get(k).cloned().unwrap_or_default(); - mat.par_iter_mut().for_each(|row_i| { - if let Some(m_ik) = row_i.get(&k).cloned() { - for (j, m_kj) in row_k.iter() { - let m_ikj = m_ik + *m_kj; - insert_or_minimize!(row_i, *j, m_ikj); - } - } - }) - } - } - - // Convert to return format - let out_map: HashMap = graph - .node_indices() - .map(|i| { - let out_map = PathLengthMapping { - path_lengths: mat[i.index()] - .iter() - .map(|(k, v)| (*k, *v)) - .collect(), - }; - (i.index(), out_map) - }) - .collect(); - Ok(AllPairsPathLengthMapping { - path_lengths: out_map, - }) -} - -/// Find all-pairs shortest path lengths using Floyd's algorithm -/// -/// Floyd's algorithm is used for finding shortest paths in dense graphs -/// or graphs with negative weights (where Dijkstra's algorithm fails). -/// -/// This function is multithreaded and will launch a pool with threads equal -/// to the number of CPUs by default if the number of nodes in the graph is -/// above the value of ``parallel_threshold`` (it defaults to 300). -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads if parallelization was enabled. -/// -/// :param PyDiGraph graph: The directed graph to run Floyd's algorithm on -/// :param weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// digraph_floyd_warshall(graph, weight_fn= lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// digraph_floyd_warshall(graph, weight_fn=float) -/// -/// to cast the edge object as a float as the weight. -/// :param as_undirected: If set to true each directed edge will be treated as -/// bidirectional/undirected. -/// :param int parallel_threshold: The number of nodes to execute -/// the algorithm in parallel at. It defaults to 300, but this can -/// be tuned -/// -/// :return: A read-only dictionary of path lengths. The keys are source -/// node indices and the values are dicts of the target node and the length -/// of the shortest path to that node. For example:: -/// -/// { -/// 0: {0: 0.0, 1: 2.0, 2: 2.0}, -/// 1: {1: 0.0, 2: 1.0}, -/// 2: {0: 1.0, 2: 0.0}, -/// } -/// -/// :rtype: AllPairsPathLengthMapping -#[pyfunction( - parallel_threshold = "300", - as_undirected = "false", - default_weight = "1.0" -)] -#[pyo3( - text_signature = "(graph, /, weight_fn=None, as_undirected=False, default_weight=1.0, parallel_threshold=300)" -)] -fn digraph_floyd_warshall( - py: Python, - graph: &digraph::PyDiGraph, - weight_fn: Option, - as_undirected: bool, - default_weight: f64, - parallel_threshold: usize, -) -> PyResult { - _floyd_warshall( - py, - &graph.graph, - weight_fn, - as_undirected, - default_weight, - parallel_threshold, - ) -} - -/// Find all-pairs shortest path lengths using Floyd's algorithm -/// -/// Floyd's algorithm is used for finding shortest paths in dense graphs -/// or graphs with negative weights (where Dijkstra's algorithm fails). -/// -/// This function is multithreaded and will launch a pool with threads equal -/// to the number of CPUs by default if the number of nodes in the graph is -/// above the value of ``parallel_threshold`` (it defaults to 300). -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads if parallelization was enabled. -/// -/// :param PyGraph graph: The graph to run Floyd's algorithm on -/// :param weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// graph_floyd_warshall(graph, weight_fn= lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// graph_floyd_warshall(graph, weight_fn=float) -/// -/// to cast the edge object as a float as the weight. -/// :param int parallel_threshold: The number of nodes to execute -/// the algorithm in parallel at. It defaults to 300, but this can -/// be tuned -/// -/// :return: A read-only dictionary of path lengths. The keys are source -/// node indices and the values are dicts of the target node and the length -/// of the shortest path to that node. For example:: -/// -/// { -/// 0: {0: 0.0, 1: 2.0, 2: 2.0}, -/// 1: {1: 0.0, 2: 1.0}, -/// 2: {0: 1.0, 2: 0.0}, -/// } -/// -/// :rtype: AllPairsPathLengthMapping -#[pyfunction(parallel_threshold = "300", default_weight = "1.0")] -#[pyo3( - text_signature = "(graph, /, weight_fn=None, default_weight=1.0, parallel_threshold=300)" -)] -fn graph_floyd_warshall( - py: Python, - graph: &graph::PyGraph, - weight_fn: Option, - default_weight: f64, - parallel_threshold: usize, -) -> PyResult { - let as_undirected = true; - _floyd_warshall( - py, - &graph.graph, - weight_fn, - as_undirected, - default_weight, - parallel_threshold, - ) -} - -fn get_edge_iter_with_weights( +pub fn get_edge_iter_with_weights( graph: G, ) -> impl Iterator where @@ -1518,3522 +113,19 @@ where }) } -/// Find all-pairs shortest path lengths using Floyd's algorithm -/// -/// Floyd's algorithm is used for finding shortest paths in dense graphs -/// or graphs with negative weights (where Dijkstra's algorithm fails). -/// -/// This function is multithreaded and will launch a pool with threads equal -/// to the number of CPUs by default if the number of nodes in the graph is -/// above the value of ``parallel_threshold`` (it defaults to 300). -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads if parallelization was enabled. -/// -/// :param PyGraph graph: The graph to run Floyd's algorithm on -/// :param weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: float(x)) -/// -/// to cast the edge object as a float as the weight. -/// :param int parallel_threshold: The number of nodes to execute -/// the algorithm in parallel at. It defaults to 300, but this can -/// be tuned -/// -/// :returns: A matrix of shortest path distances between nodes. If there is no -/// path between two nodes then the corresponding matrix entry will be -/// ``np.inf``. -/// :rtype: numpy.ndarray -#[pyfunction(parallel_threshold = "300", default_weight = "1.0")] -#[pyo3( - text_signature = "(graph, /, weight_fn=None, default_weight=1.0, parallel_threshold=300)" -)] -fn graph_floyd_warshall_numpy( - py: Python, - graph: &graph::PyGraph, - weight_fn: Option, - default_weight: f64, - parallel_threshold: usize, -) -> PyResult { - let n = graph.node_count(); - // Allocate empty matrix - let mut mat = Array2::::from_elem((n, n), std::f64::INFINITY); - - // Build adjacency matrix - for (i, j, weight) in get_edge_iter_with_weights(graph) { - let edge_weight = - weight_callable(py, &weight_fn, &weight, default_weight)?; - mat[[i, j]] = mat[[i, j]].min(edge_weight); - mat[[j, i]] = mat[[j, i]].min(edge_weight); - } - - // 0 out the diagonal - for x in mat.diag_mut() { - *x = 0.0; - } - // Perform the Floyd-Warshall algorithm. - // In each loop, this finds the shortest path from point i - // to point j using intermediate nodes 0..k - if n < parallel_threshold { - for k in 0..n { - for i in 0..n { - for j in 0..n { - let d_ijk = mat[[i, k]] + mat[[k, j]]; - if d_ijk < mat[[i, j]] { - mat[[i, j]] = d_ijk; - } - } - } - } - } else { - for k in 0..n { - let row_k = mat.slice(s![k, ..]).to_owned(); - mat.axis_iter_mut(Axis(0)) - .into_par_iter() - .for_each(|mut row_i| { - let m_ik = row_i[k]; - row_i.iter_mut().zip(row_k.iter()).for_each( - |(m_ij, m_kj)| { - let d_ijk = m_ik + *m_kj; - if d_ijk < *m_ij { - *m_ij = d_ijk; - } - }, - ) - }) - } - } - Ok(mat.into_pyarray(py).into()) -} - -/// Find all-pairs shortest path lengths using Floyd's algorithm -/// -/// Floyd's algorithm is used for finding shortest paths in dense graphs -/// or graphs with negative weights (where Dijkstra's algorithm fails). -/// -/// This function is multithreaded and will launch a pool with threads equal -/// to the number of CPUs by default if the number of nodes in the graph is -/// above the value of ``parallel_threshold`` (it defaults to 300). -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads if parallelization was enabled. -/// -/// :param PyDiGraph graph: The directed graph to run Floyd's algorithm on -/// :param weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: float(x)) -/// -/// to cast the edge object as a float as the weight. -/// :param as_undirected: If set to true each directed edge will be treated as -/// bidirectional/undirected. -/// :param int parallel_threshold: The number of nodes to execute -/// the algorithm in parallel at. It defaults to 300, but this can -/// be tuned -/// -/// :returns: A matrix of shortest path distances between nodes. If there is no -/// path between two nodes then the corresponding matrix entry will be -/// ``np.inf``. -/// :rtype: numpy.ndarray -#[pyfunction( - parallel_threshold = "300", - as_undirected = "false", - default_weight = "1.0" -)] -#[pyo3( - text_signature = "(graph, /, weight_fn=None, as_undirected=False, default_weight=1.0, parallel_threshold=300)" -)] -fn digraph_floyd_warshall_numpy( - py: Python, - graph: &digraph::PyDiGraph, - weight_fn: Option, - as_undirected: bool, - default_weight: f64, - parallel_threshold: usize, -) -> PyResult { - let n = graph.node_count(); - - // Allocate empty matrix - let mut mat = Array2::::from_elem((n, n), std::f64::INFINITY); - - // Build adjacency matrix - for (i, j, weight) in get_edge_iter_with_weights(graph) { - let edge_weight = - weight_callable(py, &weight_fn, &weight, default_weight)?; - mat[[i, j]] = mat[[i, j]].min(edge_weight); - if as_undirected { - mat[[j, i]] = mat[[j, i]].min(edge_weight); - } - } - // 0 out the diagonal - for x in mat.diag_mut() { - *x = 0.0; - } - // Perform the Floyd-Warshall algorithm. - // In each loop, this finds the shortest path from point i - // to point j using intermediate nodes 0..k - if n < parallel_threshold { - for k in 0..n { - for i in 0..n { - for j in 0..n { - let d_ijk = mat[[i, k]] + mat[[k, j]]; - if d_ijk < mat[[i, j]] { - mat[[i, j]] = d_ijk; - } - } - } - } - } else { - for k in 0..n { - let row_k = mat.slice(s![k, ..]).to_owned(); - mat.axis_iter_mut(Axis(0)) - .into_par_iter() - .for_each(|mut row_i| { - let m_ik = row_i[k]; - row_i.iter_mut().zip(row_k.iter()).for_each( - |(m_ij, m_kj)| { - let d_ijk = m_ik + *m_kj; - if d_ijk < *m_ij { - *m_ij = d_ijk; - } - }, - ) - }) - } - } - Ok(mat.into_pyarray(py).into()) -} - -/// Collect runs that match a filter function -/// -/// A run is a path of nodes where there is only a single successor and all -/// nodes in the path match the given condition. Each node in the graph can -/// appear in only a single run. -/// -/// :param PyDiGraph graph: The graph to find runs in -/// :param filter_fn: The filter function to use for matching nodes. It takes -/// in one argument, the node data payload/weight object, and will return a -/// boolean whether the node matches the conditions or not. If it returns -/// ``False`` it will skip that node. -/// -/// :returns: a list of runs, where each run is a list of node data -/// payload/weight for the nodes in the run -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, filter)")] -fn collect_runs( - py: Python, - graph: &digraph::PyDiGraph, - filter_fn: PyObject, -) -> PyResult>> { - let mut out_list: Vec> = Vec::new(); - let mut seen: HashSet = - HashSet::with_capacity(graph.node_count()); - - let filter_node = |node: &PyObject| -> PyResult { - let res = filter_fn.call1(py, (node,))?; - res.extract(py) - }; - - let nodes = match algo::toposort(graph, None) { - Ok(nodes) => nodes, - Err(_err) => { - return Err(DAGHasCycle::new_err("Sort encountered a cycle")) - } - }; - for node in nodes { - if !filter_node(&graph.graph[node])? || seen.contains(&node) { - continue; - } - seen.insert(node); - let mut group: Vec = vec![graph.graph[node].clone_ref(py)]; - let mut successors: Vec = graph - .graph - .neighbors_directed(node, petgraph::Direction::Outgoing) - .collect(); - successors.dedup(); - - while successors.len() == 1 - && filter_node(&graph.graph[successors[0]])? - && !seen.contains(&successors[0]) - { - group.push(graph.graph[successors[0]].clone_ref(py)); - seen.insert(successors[0]); - successors = graph - .graph - .neighbors_directed( - successors[0], - petgraph::Direction::Outgoing, - ) - .collect(); - successors.dedup(); - } - if !group.is_empty() { - out_list.push(group); - } - } - Ok(out_list) -} - -/// Collect runs that match a filter function given edge colors -/// -/// A bicolor run is a list of group of nodes connected by edges of exactly -/// two colors. In addition, all nodes in the group must match the given -/// condition. Each node in the graph can appear in only a single group -/// in the bicolor run. -/// -/// :param PyDiGraph graph: The graph to find runs in -/// :param filter_fn: The filter function to use for matching nodes. It takes -/// in one argument, the node data payload/weight object, and will return a -/// boolean whether the node matches the conditions or not. -/// If it returns ``True``, it will continue the bicolor chain. -/// If it returns ``False``, it will stop the bicolor chain. -/// If it returns ``None`` it will skip that node. -/// :param color_fn: The function that gives the color of the edge. It takes -/// in one argument, the edge data payload/weight object, and will -/// return a non-negative integer, the edge color. If the color is None, -/// the edge is ignored. -/// -/// :returns: a list of groups with exactly two edge colors, where each group -/// is a list of node data payload/weight for the nodes in the bicolor run -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, filter_fn, color_fn)")] -fn collect_bicolor_runs( +fn weight_callable( py: Python, - graph: &digraph::PyDiGraph, - filter_fn: PyObject, - color_fn: PyObject, -) -> PyResult>> { - let mut pending_list: Vec> = Vec::new(); - let mut block_id: Vec> = Vec::new(); - let mut block_list: Vec> = Vec::new(); - - let filter_node = |node: &PyObject| -> PyResult> { - let res = filter_fn.call1(py, (node,))?; - res.extract(py) - }; - - let color_edge = |edge: &PyObject| -> PyResult> { - let res = color_fn.call1(py, (edge,))?; - res.extract(py) - }; - - let nodes = match algo::toposort(graph, None) { - Ok(nodes) => nodes, - Err(_err) => { - return Err(DAGHasCycle::new_err("Sort encountered a cycle")) - } - }; - - // Utility for ensuring pending_list has the color index - macro_rules! ensure_vector_has_index { - ($pending_list: expr, $block_id: expr, $color: expr) => { - if $color >= $pending_list.len() { - $pending_list.resize($color + 1, Vec::new()); - $block_id.resize($color + 1, None); - } - }; - } - - for node in nodes { - if let Some(is_match) = filter_node(&graph.graph[node])? { - let raw_edges = graph - .graph - .edges_directed(node, petgraph::Direction::Outgoing); - - // Remove all edges that do not yield errors from color_fn - let colors = raw_edges - .map(|edge| { - let edge_weight = edge.weight(); - color_edge(edge_weight) - }) - .collect::>>>()?; - - // Remove null edges from color_fn - let colors = colors.into_iter().flatten().collect::>(); - - if colors.len() <= 2 && is_match { - if colors.len() == 1 { - let c0 = colors[0]; - ensure_vector_has_index!(pending_list, block_id, c0); - if let Some(c0_block_id) = block_id[c0] { - block_list[c0_block_id] - .push(graph.graph[node].clone_ref(py)); - } else { - pending_list[c0].push(graph.graph[node].clone_ref(py)); - } - } else if colors.len() == 2 { - let c0 = colors[0]; - let c1 = colors[1]; - ensure_vector_has_index!(pending_list, block_id, c0); - ensure_vector_has_index!(pending_list, block_id, c1); - - if block_id[c0].is_some() - && block_id[c1].is_some() - && block_id[c0] == block_id[c1] - { - block_list[block_id[c0].unwrap_or_default()] - .push(graph.graph[node].clone_ref(py)); - } else { - let mut new_block: Vec = Vec::with_capacity( - pending_list[c0].len() + pending_list[c1].len() + 1, - ); - - // Clears pending lits and add to new block - new_block.append(&mut pending_list[c0]); - new_block.append(&mut pending_list[c1]); - - new_block.push(graph.graph[node].clone_ref(py)); - - // Create new block, assign its id to color pair - block_id[c0] = Some(block_list.len()); - block_id[c1] = Some(block_list.len()); - block_list.push(new_block); - } - } - } else { - for color in colors { - let color = color; - ensure_vector_has_index!(pending_list, block_id, color); - if let Some(color_block_id) = block_id[color] { - block_list[color_block_id] - .append(&mut pending_list[color]); - } - block_id[color] = None; - pending_list[color].clear(); - } - } + weight_fn: &Option, + weight: &PyObject, + default: f64, +) -> PyResult { + match weight_fn { + Some(weight_fn) => { + let res = weight_fn.call1(py, (weight,))?; + res.extract(py) } + None => Ok(default), } - - Ok(block_list) -} - -/// Return a list of layers -/// -/// A layer is a subgraph whose nodes are disjoint, i.e., -/// a layer has depth 1. The layers are constructed using a greedy algorithm. -/// -/// :param PyDiGraph graph: The DAG to get the layers from -/// :param list first_layer: A list of node ids for the first layer. This -/// will be the first layer in the output -/// -/// :returns: A list of layers, each layer is a list of node data -/// :rtype: list -/// -/// :raises InvalidNode: If a node index in ``first_layer`` is not in the graph -#[pyfunction] -#[pyo3(text_signature = "(dag, first_layer, /)")] -fn layers( - py: Python, - dag: &digraph::PyDiGraph, - first_layer: Vec, -) -> PyResult { - let mut output: Vec> = Vec::new(); - // Convert usize to NodeIndex - let mut first_layer_index: Vec = Vec::new(); - for index in first_layer { - first_layer_index.push(NodeIndex::new(index)); - } - - let mut cur_layer = first_layer_index; - let mut next_layer: Vec = Vec::new(); - let mut predecessor_count: HashMap = HashMap::new(); - - let mut layer_node_data: Vec<&PyObject> = Vec::new(); - for layer_node in &cur_layer { - let node_data = match dag.graph.node_weight(*layer_node) { - Some(data) => data, - None => { - return Err(InvalidNode::new_err(format!( - "An index input in 'first_layer' {} is not a valid node index in the graph", - layer_node.index()), - )) - } - }; - layer_node_data.push(node_data); - } - output.push(layer_node_data); - - // Iterate until there are no more - while !cur_layer.is_empty() { - for node in &cur_layer { - let children = dag - .graph - .neighbors_directed(*node, petgraph::Direction::Outgoing); - let mut used_indexes: HashSet = HashSet::new(); - for succ in children { - // Skip duplicate successors - if used_indexes.contains(&succ) { - continue; - } - used_indexes.insert(succ); - let mut multiplicity: usize = 0; - let raw_edges = dag - .graph - .edges_directed(*node, petgraph::Direction::Outgoing); - for edge in raw_edges { - if edge.target() == succ { - multiplicity += 1; - } - } - predecessor_count - .entry(succ) - .and_modify(|e| *e -= multiplicity) - .or_insert(dag.in_degree(succ.index()) - multiplicity); - if *predecessor_count.get(&succ).unwrap() == 0 { - next_layer.push(succ); - predecessor_count.remove(&succ); - } - } - } - let mut layer_node_data: Vec<&PyObject> = Vec::new(); - for layer_node in &next_layer { - layer_node_data.push(&dag[*layer_node]); - } - if !layer_node_data.is_empty() { - output.push(layer_node_data); - } - cur_layer = next_layer; - next_layer = Vec::new(); - } - Ok(PyList::new(py, output).into()) -} - -/// Get the distance matrix for a directed graph -/// -/// This differs from functions like digraph_floyd_warshall_numpy in that the -/// edge weight/data payload is not used and each edge is treated as a -/// distance of 1. -/// -/// This function is also multithreaded and will run in parallel if the number -/// of nodes in the graph is above the value of ``parallel_threshold`` (it -/// defaults to 300). If the function will be running in parallel the env var -/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used. -/// -/// :param PyDiGraph graph: The graph to get the distance matrix for -/// :param int parallel_threshold: The number of nodes to calculate the -/// the distance matrix in parallel at. It defaults to 300, but this can -/// be tuned -/// :param bool as_undirected: If set to ``True`` the input directed graph -/// will be treat as if each edge was bidirectional/undirected in the -/// output distance matrix. -/// -/// :returns: The distance matrix -/// :rtype: numpy.ndarray -#[pyfunction(parallel_threshold = "300", as_undirected = "false")] -#[pyo3( - text_signature = "(graph, /, parallel_threshold=300, as_undirected=False)" -)] -pub fn digraph_distance_matrix( - py: Python, - graph: &digraph::PyDiGraph, - parallel_threshold: usize, - as_undirected: bool, -) -> PyResult { - let n = graph.node_count(); - let mut matrix = Array2::::zeros((n, n)); - let bfs_traversal = |index: usize, mut row: ArrayViewMut1| { - let mut seen: HashMap = HashMap::with_capacity(n); - let start_index = NodeIndex::new(index); - let mut level = 0; - let mut next_level: HashSet = HashSet::new(); - next_level.insert(start_index); - while !next_level.is_empty() { - let this_level = next_level; - next_level = HashSet::new(); - let mut found: Vec = Vec::new(); - for v in this_level { - if !seen.contains_key(&v) { - seen.insert(v, level); - found.push(v); - row[[v.index()]] = level as f64; - } - } - if seen.len() == n { - return; - } - for node in found { - for v in graph - .graph - .neighbors_directed(node, petgraph::Direction::Outgoing) - { - next_level.insert(v); - } - if as_undirected { - for v in graph - .graph - .neighbors_directed(node, petgraph::Direction::Incoming) - { - next_level.insert(v); - } - } - } - level += 1 - } - }; - if n < parallel_threshold { - matrix - .axis_iter_mut(Axis(0)) - .enumerate() - .for_each(|(index, row)| bfs_traversal(index, row)); - } else { - // Parallelize by row and iterate from each row index in BFS order - matrix - .axis_iter_mut(Axis(0)) - .into_par_iter() - .enumerate() - .for_each(|(index, row)| bfs_traversal(index, row)); - } - Ok(matrix.into_pyarray(py).into()) -} - -/// Get the distance matrix for an undirected graph -/// -/// This differs from functions like digraph_floyd_warshall_numpy in that the -/// edge weight/data payload is not used and each edge is treated as a -/// distance of 1. -/// -/// This function is also multithreaded and will run in parallel if the number -/// of nodes in the graph is above the value of ``paralllel_threshold`` (it -/// defaults to 300). If the function will be running in parallel the env var -/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used. -/// -/// :param PyGraph graph: The graph to get the distance matrix for -/// :param int parallel_threshold: The number of nodes to calculate the -/// the distance matrix in parallel at. It defaults to 300, but this can -/// be tuned -/// -/// :returns: The distance matrix -/// :rtype: numpy.ndarray -#[pyfunction(parallel_threshold = "300")] -#[pyo3(text_signature = "(graph, /, parallel_threshold=300)")] -pub fn graph_distance_matrix( - py: Python, - graph: &graph::PyGraph, - parallel_threshold: usize, -) -> PyResult { - let n = graph.node_count(); - let mut matrix = Array2::::zeros((n, n)); - let bfs_traversal = |index: usize, mut row: ArrayViewMut1| { - let mut seen: HashMap = HashMap::with_capacity(n); - let start_index = NodeIndex::new(index); - let mut level = 0; - let mut next_level: HashSet = HashSet::new(); - next_level.insert(start_index); - while !next_level.is_empty() { - let this_level = next_level; - next_level = HashSet::new(); - let mut found: Vec = Vec::new(); - for v in this_level { - if !seen.contains_key(&v) { - seen.insert(v, level); - found.push(v); - row[[v.index()]] = level as f64; - } - } - if seen.len() == n { - return; - } - for node in found { - for v in graph.graph.neighbors(node) { - next_level.insert(v); - } - } - level += 1 - } - }; - if n < parallel_threshold { - matrix - .axis_iter_mut(Axis(0)) - .enumerate() - .for_each(|(index, row)| bfs_traversal(index, row)); - } else { - // Parallelize by row and iterate from each row index in BFS order - matrix - .axis_iter_mut(Axis(0)) - .into_par_iter() - .enumerate() - .for_each(|(index, row)| bfs_traversal(index, row)); - } - Ok(matrix.into_pyarray(py).into()) -} - -/// Return the adjacency matrix for a PyDiGraph object -/// -/// In the case where there are multiple edges between nodes the value in the -/// output matrix will be the sum of the edges' weights. -/// -/// :param PyDiGraph graph: The DiGraph used to generate the adjacency matrix -/// from -/// :param callable weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// dag_adjacency_matrix(dag, weight_fn: lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// dag_adjacency_matrix(dag, weight_fn: lambda x: float(x)) -/// -/// to cast the edge object as a float as the weight. If this is not -/// specified a default value (either ``default_weight`` or 1) will be used -/// for all edges. -/// :param float default_weight: If ``weight_fn`` is not used this can be -/// optionally used to specify a default weight to use for all edges. -/// -/// :return: The adjacency matrix for the input dag as a numpy array -/// :rtype: numpy.ndarray -#[pyfunction(default_weight = "1.0")] -#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0)")] -fn digraph_adjacency_matrix( - py: Python, - graph: &digraph::PyDiGraph, - weight_fn: Option, - default_weight: f64, -) -> PyResult { - let n = graph.node_count(); - let mut matrix = Array2::::zeros((n, n)); - for (i, j, weight) in get_edge_iter_with_weights(graph) { - let edge_weight = - weight_callable(py, &weight_fn, &weight, default_weight)?; - matrix[[i, j]] += edge_weight; - } - Ok(matrix.into_pyarray(py).into()) -} - -/// Return the adjacency matrix for a PyGraph class -/// -/// In the case where there are multiple edges between nodes the value in the -/// output matrix will be the sum of the edges' weights. -/// -/// :param PyGraph graph: The graph used to generate the adjacency matrix from -/// :param weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// graph_adjacency_matrix(graph, weight_fn: lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// graph_adjacency_matrix(graph, weight_fn: lambda x: float(x)) -/// -/// to cast the edge object as a float as the weight. If this is not -/// specified a default value (either ``default_weight`` or 1) will be used -/// for all edges. -/// :param float default_weight: If ``weight_fn`` is not used this can be -/// optionally used to specify a default weight to use for all edges. -/// -/// :return: The adjacency matrix for the input dag as a numpy array -/// :rtype: numpy.ndarray -#[pyfunction(default_weight = "1.0")] -#[pyo3(text_signature = "(graph, /, weight_fn=None, default_weight=1.0)")] -fn graph_adjacency_matrix( - py: Python, - graph: &graph::PyGraph, - weight_fn: Option, - default_weight: f64, -) -> PyResult { - let n = graph.node_count(); - let mut matrix = Array2::::zeros((n, n)); - for (i, j, weight) in get_edge_iter_with_weights(graph) { - let edge_weight = - weight_callable(py, &weight_fn, &weight, default_weight)?; - matrix[[i, j]] += edge_weight; - matrix[[j, i]] += edge_weight; - } - Ok(matrix.into_pyarray(py).into()) -} - -/// Return all simple paths between 2 nodes in a PyGraph object -/// -/// A simple path is a path with no repeated nodes. -/// -/// :param PyGraph graph: The graph to find the path in -/// :param int from: The node index to find the paths from -/// :param int to: The node index to find the paths to -/// :param int min_depth: The minimum depth of the path to include in the output -/// list of paths. By default all paths are included regardless of depth, -/// setting to 0 will behave like the default. -/// :param int cutoff: The maximum depth of path to include in the output list -/// of paths. By default includes all paths regardless of depth, setting to -/// 0 will behave like default. -/// -/// :returns: A list of lists where each inner list is a path of node indices -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, from, to, /, min=None, cutoff=None)")] -fn graph_all_simple_paths( - graph: &graph::PyGraph, - from: usize, - to: usize, - min_depth: Option, - cutoff: Option, -) -> PyResult>> { - let from_index = NodeIndex::new(from); - if !graph.graph.contains_node(from_index) { - return Err(InvalidNode::new_err( - "The input index for 'from' is not a valid node index", - )); - } - let to_index = NodeIndex::new(to); - if !graph.graph.contains_node(to_index) { - return Err(InvalidNode::new_err( - "The input index for 'to' is not a valid node index", - )); - } - let min_intermediate_nodes: usize = match min_depth { - Some(depth) => depth - 2, - None => 0, - }; - let cutoff_petgraph: Option = cutoff.map(|depth| depth - 2); - let result: Vec> = algo::all_simple_paths( - graph, - from_index, - to_index, - min_intermediate_nodes, - cutoff_petgraph, - ) - .map(|v: Vec| v.into_iter().map(|i| i.index()).collect()) - .collect(); - Ok(result) -} - -/// Return all simple paths between 2 nodes in a PyDiGraph object -/// -/// A simple path is a path with no repeated nodes. -/// -/// :param PyDiGraph graph: The graph to find the path in -/// :param int from: The node index to find the paths from -/// :param int to: The node index to find the paths to -/// :param int min_depth: The minimum depth of the path to include in the output -/// list of paths. By default all paths are included regardless of depth, -/// sett to 0 will behave like the default. -/// :param int cutoff: The maximum depth of path to include in the output list -/// of paths. By default includes all paths regardless of depth, setting to -/// 0 will behave like default. -/// -/// :returns: A list of lists where each inner list is a path -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, from, to, /, min_depth=None, cutoff=None)")] -fn digraph_all_simple_paths( - graph: &digraph::PyDiGraph, - from: usize, - to: usize, - min_depth: Option, - cutoff: Option, -) -> PyResult>> { - let from_index = NodeIndex::new(from); - if !graph.graph.contains_node(from_index) { - return Err(InvalidNode::new_err( - "The input index for 'from' is not a valid node index", - )); - } - let to_index = NodeIndex::new(to); - if !graph.graph.contains_node(to_index) { - return Err(InvalidNode::new_err( - "The input index for 'to' is not a valid node index", - )); - } - let min_intermediate_nodes: usize = match min_depth { - Some(depth) => depth - 2, - None => 0, - }; - let cutoff_petgraph: Option = cutoff.map(|depth| depth - 2); - let result: Vec> = algo::all_simple_paths( - graph, - from_index, - to_index, - min_intermediate_nodes, - cutoff_petgraph, - ) - .map(|v: Vec| v.into_iter().map(|i| i.index()).collect()) - .collect(); - Ok(result) -} - -fn weight_callable( - py: Python, - weight_fn: &Option, - weight: &PyObject, - default: f64, -) -> PyResult { - match weight_fn { - Some(weight_fn) => { - let res = weight_fn.call1(py, (weight,))?; - res.extract(py) - } - None => Ok(default), - } -} - -/// Find the shortest path from a node -/// -/// This function will generate the shortest path from a source node using -/// Dijkstra's algorithm. -/// -/// :param PyGraph graph: -/// :param int source: The node index to find paths from -/// :param int target: An optional target to find a path to -/// :param weight_fn: An optional weight function for an edge. It will accept -/// a single argument, the edge's weight object and will return a float which -/// will be used to represent the weight/cost of the edge -/// :param float default_weight: If ``weight_fn`` isn't specified this optional -/// float value will be used for the weight/cost of each edge. -/// :param bool as_undirected: If set to true the graph will be treated as -/// undirected for finding the shortest path. -/// -/// :return: Dictionary of paths. The keys are destination node indices and -/// the dict values are lists of node indices making the path. -/// :rtype: dict -#[pyfunction(default_weight = "1.0", as_undirected = "false")] -#[pyo3( - text_signature = "(graph, source, /, target=None weight_fn=None, default_weight=1.0)" -)] -pub fn graph_dijkstra_shortest_paths( - py: Python, - graph: &graph::PyGraph, - source: usize, - target: Option, - weight_fn: Option, - default_weight: f64, -) -> PyResult { - let start = NodeIndex::new(source); - let goal_index: Option = target.map(NodeIndex::new); - let mut paths: HashMap> = - HashMap::with_capacity(graph.node_count()); - dijkstra::dijkstra( - graph, - start, - goal_index, - |e| weight_callable(py, &weight_fn, e.weight(), default_weight), - Some(&mut paths), - )?; - - Ok(PathMapping { - paths: paths - .iter() - .filter_map(|(k, v)| { - let k_int = k.index(); - if k_int == source - || target.is_some() && target.unwrap() != k_int - { - None - } else { - Some(( - k.index(), - v.iter().map(|x| x.index()).collect::>(), - )) - } - }) - .collect(), - }) -} - -/// Find the shortest path from a node -/// -/// This function will generate the shortest path from a source node using -/// Dijkstra's algorithm. -/// -/// :param PyDiGraph graph: -/// :param int source: The node index to find paths from -/// :param int target: An optional target path to find the path -/// :param weight_fn: An optional weight function for an edge. It will accept -/// a single argument, the edge's weight object and will return a float which -/// will be used to represent the weight/cost of the edge -/// :param float default_weight: If ``weight_fn`` isn't specified this optional -/// float value will be used for the weight/cost of each edge. -/// :param bool as_undirected: If set to true the graph will be treated as -/// undirected for finding the shortest path. -/// -/// :return: Dictionary of paths. The keys are destination node indices and -/// the dict values are lists of node indices making the path. -/// :rtype: dict -#[pyfunction(default_weight = "1.0", as_undirected = "false")] -#[pyo3( - text_signature = "(graph, source, /, target=None weight_fn=None, default_weight=1.0, as_undirected=False)" -)] -pub fn digraph_dijkstra_shortest_paths( - py: Python, - graph: &digraph::PyDiGraph, - source: usize, - target: Option, - weight_fn: Option, - default_weight: f64, - as_undirected: bool, -) -> PyResult { - let start = NodeIndex::new(source); - let goal_index: Option = target.map(NodeIndex::new); - let mut paths: HashMap> = - HashMap::with_capacity(graph.node_count()); - if as_undirected { - dijkstra::dijkstra( - // TODO: Use petgraph undirected adapter after - // https://github.com/petgraph/petgraph/pull/318 is available in - // a petgraph release. - &graph.to_undirected(py, true, None)?, - start, - goal_index, - |e| weight_callable(py, &weight_fn, e.weight(), default_weight), - Some(&mut paths), - )?; - } else { - dijkstra::dijkstra( - graph, - start, - goal_index, - |e| weight_callable(py, &weight_fn, e.weight(), default_weight), - Some(&mut paths), - )?; - } - Ok(PathMapping { - paths: paths - .iter() - .filter_map(|(k, v)| { - let k_int = k.index(); - if k_int == source - || target.is_some() && target.unwrap() != k_int - { - None - } else { - Some(( - k_int, - v.iter().map(|x| x.index()).collect::>(), - )) - } - }) - .collect(), - }) -} - -/// Compute the lengths of the shortest paths for a PyGraph object using -/// Dijkstra's algorithm -/// -/// :param PyGraph graph: The input graph to use -/// :param int node: The node index to use as the source for finding the -/// shortest paths from -/// :param edge_cost_fn: A python callable that will take in 1 parameter, an -/// edge's data object and will return a float that represents the -/// cost/weight of that edge. It must be non-negative -/// :param int goal: An optional node index to use as the end of the path. -/// When specified the traversal will stop when the goal is reached and -/// the output dictionary will only have a single entry with the length -/// of the shortest path to the goal node. -/// -/// :returns: A dictionary of the shortest paths from the provided node where -/// the key is the node index of the end of the path and the value is the -/// cost/sum of the weights of path -/// :rtype: PathLengthMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, node, edge_cost_fn, /, goal=None)")] -fn graph_dijkstra_shortest_path_lengths( - py: Python, - graph: &graph::PyGraph, - node: usize, - edge_cost_fn: PyObject, - goal: Option, -) -> PyResult { - let edge_cost_callable = |a: &PyObject| -> PyResult { - let res = edge_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - raw.extract(py) - }; - - let start = NodeIndex::new(node); - let goal_index: Option = goal.map(NodeIndex::new); - - let res = dijkstra::dijkstra( - graph, - start, - goal_index, - |e| edge_cost_callable(e.weight()), - None, - )?; - Ok(PathLengthMapping { - path_lengths: res - .iter() - .filter_map(|(k, v)| { - let k_int = k.index(); - if k_int == node || goal.is_some() && goal.unwrap() != k_int { - None - } else { - Some((k_int, *v)) - } - }) - .collect(), - }) -} - -/// Compute the lengths of the shortest paths for a PyDiGraph object using -/// Dijkstra's algorithm -/// -/// :param PyDiGraph graph: The input graph to use -/// :param int node: The node index to use as the source for finding the -/// shortest paths from -/// :param edge_cost_fn: A python callable that will take in 1 parameter, an -/// edge's data object and will return a float that represents the -/// cost/weight of that edge. It must be non-negative -/// :param int goal: An optional node index to use as the end of the path. -/// When specified the traversal will stop when the goal is reached and -/// the output dictionary will only have a single entry with the length -/// of the shortest path to the goal node. -/// -/// :returns: A dictionary of the shortest paths from the provided node where -/// the key is the node index of the end of the path and the value is the -/// cost/sum of the weights of path -/// :rtype: PathLengthMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, node, edge_cost_fn, /, goal=None)")] -fn digraph_dijkstra_shortest_path_lengths( - py: Python, - graph: &digraph::PyDiGraph, - node: usize, - edge_cost_fn: PyObject, - goal: Option, -) -> PyResult { - let edge_cost_callable = |a: &PyObject| -> PyResult { - let res = edge_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - raw.extract(py) - }; - - let start = NodeIndex::new(node); - let goal_index: Option = goal.map(NodeIndex::new); - - let res = dijkstra::dijkstra( - graph, - start, - goal_index, - |e| edge_cost_callable(e.weight()), - None, - )?; - Ok(PathLengthMapping { - path_lengths: res - .iter() - .filter_map(|(k, v)| { - let k_int = k.index(); - if k_int == node || goal.is_some() && goal.unwrap() != k_int { - None - } else { - Some((k_int, *v)) - } - }) - .collect(), - }) -} - -fn _all_pairs_dijkstra_path_lengths( - py: Python, - graph: &StableGraph, - edge_cost_fn: PyObject, -) -> PyResult { - if graph.node_count() == 0 { - return Ok(AllPairsPathLengthMapping { - path_lengths: HashMap::new(), - }); - } else if graph.edge_count() == 0 { - return Ok(AllPairsPathLengthMapping { - path_lengths: graph - .node_indices() - .map(|i| { - ( - i.index(), - PathLengthMapping { - path_lengths: HashMap::new(), - }, - ) - }) - .collect(), - }); - } - let edge_cost_callable = |a: &PyObject| -> PyResult { - let res = edge_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - raw.extract(py) - }; - let mut edge_weights: Vec> = - Vec::with_capacity(graph.edge_bound()); - for index in 0..=graph.edge_bound() { - let raw_weight = graph.edge_weight(EdgeIndex::new(index)); - match raw_weight { - Some(weight) => { - edge_weights.push(Some(edge_cost_callable(weight)?)) - } - None => edge_weights.push(None), - }; - } - let edge_cost = |e: EdgeIndex| -> PyResult { - match edge_weights[e.index()] { - Some(weight) => Ok(weight), - None => Err(PyIndexError::new_err("No edge found for index")), - } - }; - let node_indices: Vec = graph.node_indices().collect(); - let out_map: HashMap = node_indices - .into_par_iter() - .map(|x| { - let out_map = PathLengthMapping { - path_lengths: dijkstra::dijkstra( - graph, - x, - None, - |e| edge_cost(e.id()), - None, - ) - .unwrap() - .iter() - .filter_map(|(index, cost)| { - if *index == x { - None - } else { - Some((index.index(), *cost)) - } - }) - .collect(), - }; - (x.index(), out_map) - }) - .collect(); - Ok(AllPairsPathLengthMapping { - path_lengths: out_map, - }) -} - -pub fn _all_pairs_dijkstra_shortest_paths( - py: Python, - graph: &StableGraph, - edge_cost_fn: PyObject, - distances: Option<&mut HashMap>>, -) -> PyResult { - if graph.node_count() == 0 { - return Ok(AllPairsPathMapping { - paths: HashMap::new(), - }); - } else if graph.edge_count() == 0 { - return Ok(AllPairsPathMapping { - paths: graph - .node_indices() - .map(|i| { - ( - i.index(), - PathMapping { - paths: HashMap::new(), - }, - ) - }) - .collect(), - }); - } - let edge_cost_callable = |a: &PyObject| -> PyResult { - let res = edge_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - raw.extract(py) - }; - let mut edge_weights: Vec> = - Vec::with_capacity(graph.edge_bound()); - for index in 0..=graph.edge_bound() { - let raw_weight = graph.edge_weight(EdgeIndex::new(index)); - match raw_weight { - Some(weight) => { - edge_weights.push(Some(edge_cost_callable(weight)?)) - } - None => edge_weights.push(None), - }; - } - let edge_cost = |e: EdgeIndex| -> PyResult { - match edge_weights[e.index()] { - Some(weight) => Ok(weight), - None => Err(PyIndexError::new_err("No edge found for index")), - } - }; - let node_indices: Vec = graph.node_indices().collect(); - let temp_distances: RwLock>> = - if distances.is_some() { - RwLock::new(HashMap::with_capacity(graph.node_count())) - } else { - // Avoid extra allocation if HashMap isn't used - RwLock::new(HashMap::new()) - }; - let out_map = AllPairsPathMapping { - paths: node_indices - .into_par_iter() - .map(|x| { - let mut paths: HashMap> = - HashMap::with_capacity(graph.node_count()); - let distance = dijkstra::dijkstra( - graph, - x, - None, - |e| edge_cost(e.id()), - Some(&mut paths), - ) - .unwrap(); - if distances.is_some() { - temp_distances.write().unwrap().insert(x.index(), distance); - } - let index = x.index(); - let out_paths = PathMapping { - paths: paths - .iter() - .filter_map(|path_mapping| { - let path_index = path_mapping.0.index(); - if index != path_index { - Some(( - path_index, - path_mapping - .1 - .iter() - .map(|x| x.index()) - .collect(), - )) - } else { - None - } - }) - .collect(), - }; - (index, out_paths) - }) - .collect(), - }; - if let Some(x) = distances { - *x = temp_distances.read().unwrap().clone() - }; - Ok(out_map) -} - -/// Calculate the the shortest length from all nodes in a -/// :class:`~retworkx.PyDiGraph` object -/// -/// This function will generate the shortest path from a source node using -/// Dijkstra's algorithm. This function is multithreaded and will run -/// launch a thread pool with threads equal to the number of CPUs by default. -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads. -/// -/// :param graph: The input :class:`~retworkx.PyDiGraph` to use -/// :param edge_cost_fn: A callable object that acts as a weight function for -/// an edge. It will accept a single positional argument, the edge's weight -/// object and will return a float which will be used to represent the -/// weight/cost of the edge -/// -/// :return: A read-only dictionary of path lengths. The keys are source -/// node indices and the values are dicts of the target node and the length -/// of the shortest path to that node. For example:: -/// -/// { -/// 0: {1: 2.0, 2: 2.0}, -/// 1: {2: 1.0}, -/// 2: {0: 1.0}, -/// } -/// -/// :rtype: AllPairsPathLengthMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] -pub fn digraph_all_pairs_dijkstra_path_lengths( - py: Python, - graph: &digraph::PyDiGraph, - edge_cost_fn: PyObject, -) -> PyResult { - _all_pairs_dijkstra_path_lengths(py, &graph.graph, edge_cost_fn) -} - -/// Find the shortest path from all nodes in a :class:`~retworkx.PyDiGraph` -/// object -/// -/// This function will generate the shortest path from a source node using -/// Dijkstra's algorithm. This function is multithreaded and will run -/// launch a thread pool with threads equal to the number of CPUs by default. -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads. -/// -/// :param graph: The input :class:`~retworkx.PyDiGraph` object to use -/// :param edge_cost_fn: A callable object that acts as a weight function for -/// an edge. It will accept a single positional argument, the edge's weight -/// object and will return a float which will be used to represent the -/// weight/cost of the edge -/// -/// :return: A read-only dictionary of paths. The keys are source node indices -/// and the values are dicts of the target node and the list of the -/// node indices making up the shortest path to that node. For example:: -/// -/// { -/// 0: {1: [0, 1], 2: [0, 1, 2]}, -/// 1: {2: [1, 2]}, -/// 2: {0: [2, 0]}, -/// } -/// -/// :rtype: AllPairsPathMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] -pub fn digraph_all_pairs_dijkstra_shortest_paths( - py: Python, - graph: &digraph::PyDiGraph, - edge_cost_fn: PyObject, -) -> PyResult { - _all_pairs_dijkstra_shortest_paths(py, &graph.graph, edge_cost_fn, None) -} - -/// Calculate the the shortest length from all nodes in a -/// :class:`~retworkx.PyGraph` object -/// -/// This function will generate the shortest path from a source node using -/// Dijkstra's algorithm. -/// -/// :param graph: The input :class:`~retworkx.PyGraph` to use -/// :param edge_cost_fn: A callable object that acts as a weight function for -/// an edge. It will accept a single positional argument, the edge's weight -/// object and will return a float which will be used to represent the -/// weight/cost of the edge -/// -/// :return: A read-only dictionary of path lengths. The keys are source -/// node indices and the values are dicts of the target node and the length -/// of the shortest path to that node. For example:: -/// -/// { -/// 0: {1: 2.0, 2: 2.0}, -/// 1: {2: 1.0}, -/// 2: {0: 1.0}, -/// } -/// -/// :rtype: AllPairsPathLengthMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] -pub fn graph_all_pairs_dijkstra_path_lengths( - py: Python, - graph: &graph::PyGraph, - edge_cost_fn: PyObject, -) -> PyResult { - _all_pairs_dijkstra_path_lengths(py, &graph.graph, edge_cost_fn) -} - -/// Find the shortest path from all nodes in a :class:`~retworkx.PyGraph` -/// object -/// -/// This function will generate the shortest path from a source node using -/// Dijkstra's algorithm. -/// -/// :param graph: The input :class:`~retworkx.PyGraph` object to use -/// :param edge_cost_fn: A callable object that acts as a weight function for -/// an edge. It will accept a single positional argument, the edge's weight -/// object and will return a float which will be used to represent the -/// weight/cost of the edge -/// -/// :return: A read-only dictionary of paths. The keys are destination node -/// indices and the values are dicts of the target node and the list of the -/// node indices making up the shortest path to that node. For example:: -/// -/// { -/// 0: {1: [0, 1], 2: [0, 1, 2]}, -/// 1: {2: [1, 2]}, -/// 2: {0: [2, 0]}, -/// } -/// -/// :rtype: AllPairsPathMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] -pub fn graph_all_pairs_dijkstra_shortest_paths( - py: Python, - graph: &graph::PyGraph, - edge_cost_fn: PyObject, -) -> PyResult { - _all_pairs_dijkstra_shortest_paths(py, &graph.graph, edge_cost_fn, None) -} - -/// Compute the A* shortest path for a PyGraph -/// -/// :param PyGraph graph: The input graph to use -/// :param int node: The node index to compute the path from -/// :param goal_fn: A python callable that will take in 1 parameter, a node's data -/// object and will return a boolean which will be True if it is the finish -/// node. -/// :param edge_cost_fn: A python callable that will take in 1 parameter, an edge's -/// data object and will return a float that represents the cost of that -/// edge. It must be non-negative. -/// :param estimate_cost_fn: A python callable that will take in 1 parameter, a -/// node's data object and will return a float which represents the estimated -/// cost for the next node. The return must be non-negative. For the -/// algorithm to find the actual shortest path, it should be admissible, -/// meaning that it should never overestimate the actual cost to get to the -/// nearest goal node. -/// -/// :returns: The computed shortest path between node and finish as a list -/// of node indices. -/// :rtype: NodeIndices -#[pyfunction] -#[pyo3(text_signature = "(graph, node, goal_fn, edge_cost, estimate_cost, /)")] -fn graph_astar_shortest_path( - py: Python, - graph: &graph::PyGraph, - node: usize, - goal_fn: PyObject, - edge_cost_fn: PyObject, - estimate_cost_fn: PyObject, -) -> PyResult { - let goal_fn_callable = |a: &PyObject| -> PyResult { - let res = goal_fn.call1(py, (a,))?; - let raw = res.to_object(py); - let output: bool = raw.extract(py)?; - Ok(output) - }; - - let edge_cost_callable = |a: &PyObject| -> PyResult { - let res = edge_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - let output: f64 = raw.extract(py)?; - Ok(output) - }; - - let estimate_cost_callable = |a: &PyObject| -> PyResult { - let res = estimate_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - let output: f64 = raw.extract(py)?; - Ok(output) - }; - let start = NodeIndex::new(node); - - let astar_res = astar::astar( - graph, - start, - |f| goal_fn_callable(graph.graph.node_weight(f).unwrap()), - |e| edge_cost_callable(e.weight()), - |estimate| { - estimate_cost_callable(graph.graph.node_weight(estimate).unwrap()) - }, - )?; - let path = match astar_res { - Some(path) => path, - None => { - return Err(NoPathFound::new_err( - "No path found that satisfies goal_fn", - )) - } - }; - Ok(NodeIndices { - nodes: path.1.into_iter().map(|x| x.index()).collect(), - }) -} - -/// Compute the A* shortest path for a PyDiGraph -/// -/// :param PyDiGraph graph: The input graph to use -/// :param int node: The node index to compute the path from -/// :param goal_fn: A python callable that will take in 1 parameter, a node's -/// data object and will return a boolean which will be True if it is the -/// finish node. -/// :param edge_cost_fn: A python callable that will take in 1 parameter, an -/// edge's data object and will return a float that represents the cost of -/// that edge. It must be non-negative. -/// :param estimate_cost_fn: A python callable that will take in 1 parameter, a -/// node's data object and will return a float which represents the -/// estimated cost for the next node. The return must be non-negative. For -/// the algorithm to find the actual shortest path, it should be -/// admissible, meaning that it should never overestimate the actual cost -/// to get to the nearest goal node. -/// -/// :return: The computed shortest path between node and finish as a list -/// of node indices. -/// :rtype: NodeIndices -#[pyfunction] -#[pyo3(text_signature = "(graph, node, goal_fn, edge_cost, estimate_cost, /)")] -fn digraph_astar_shortest_path( - py: Python, - graph: &digraph::PyDiGraph, - node: usize, - goal_fn: PyObject, - edge_cost_fn: PyObject, - estimate_cost_fn: PyObject, -) -> PyResult { - let goal_fn_callable = |a: &PyObject| -> PyResult { - let res = goal_fn.call1(py, (a,))?; - let raw = res.to_object(py); - let output: bool = raw.extract(py)?; - Ok(output) - }; - - let edge_cost_callable = |a: &PyObject| -> PyResult { - let res = edge_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - let output: f64 = raw.extract(py)?; - Ok(output) - }; - - let estimate_cost_callable = |a: &PyObject| -> PyResult { - let res = estimate_cost_fn.call1(py, (a,))?; - let raw = res.to_object(py); - let output: f64 = raw.extract(py)?; - Ok(output) - }; - let start = NodeIndex::new(node); - - let astar_res = astar::astar( - graph, - start, - |f| goal_fn_callable(graph.graph.node_weight(f).unwrap()), - |e| edge_cost_callable(e.weight()), - |estimate| { - estimate_cost_callable(graph.graph.node_weight(estimate).unwrap()) - }, - )?; - let path = match astar_res { - Some(path) => path, - None => { - return Err(NoPathFound::new_err( - "No path found that satisfies goal_fn", - )) - } - }; - Ok(NodeIndices { - nodes: path.1.into_iter().map(|x| x.index()).collect(), - }) -} - -/// Return a :math:`G_{np}` directed random graph, also known as an -/// Erdős-Rényi graph or a binomial graph. -/// -/// For number of nodes :math:`n` and probability :math:`p`, the :math:`G_{n,p}` -/// graph algorithm creates :math:`n` nodes, and for all the :math:`n (n - 1)` possible edges, -/// each edge is created independently with probability :math:`p`. -/// In general, for any probability :math:`p`, the expected number of edges returned -/// is :math:`m = p n (n - 1)`. If :math:`p = 0` or :math:`p = 1`, the returned -/// graph is not random and will always be an empty or a complete graph respectively. -/// An empty graph has zero edges and a complete directed graph has :math:`n (n - 1)` edges. -/// The run time is :math:`O(n + m)` where :math:`m` is the expected number of edges mentioned above. -/// When :math:`p = 0`, run time always reduces to :math:`O(n)`, as the lower bound. -/// When :math:`p = 1`, run time always goes to :math:`O(n + n (n - 1))`, as the upper bound. -/// For other probabilities, this algorithm [1]_ runs in :math:`O(n + m)` time. -/// -/// For :math:`0 < p < 1`, the algorithm is based on the implementation of the networkx function -/// ``fast_gnp_random_graph`` [2]_ -/// -/// :param int num_nodes: The number of nodes to create in the graph -/// :param float probability: The probability of creating an edge between two nodes -/// :param int seed: An optional seed to use for the random number generator -/// -/// :return: A PyDiGraph object -/// :rtype: PyDiGraph -/// -/// .. [1] Vladimir Batagelj and Ulrik Brandes, -/// "Efficient generation of large random networks", -/// Phys. Rev. E, 71, 036113, 2005. -/// .. [2] https://github.com/networkx/networkx/blob/networkx-2.4/networkx/generators/random_graphs.py#L49-L120 -#[pyfunction] -#[pyo3(text_signature = "(num_nodes, probability, seed=None, /)")] -pub fn directed_gnp_random_graph( - py: Python, - num_nodes: isize, - probability: f64, - seed: Option, -) -> PyResult { - if num_nodes <= 0 { - return Err(PyValueError::new_err("num_nodes must be > 0")); - } - let mut rng: Pcg64 = match seed { - Some(seed) => Pcg64::seed_from_u64(seed), - None => Pcg64::from_entropy(), - }; - let mut inner_graph = StableDiGraph::::new(); - for x in 0..num_nodes { - inner_graph.add_node(x.to_object(py)); - } - if !(0.0..=1.0).contains(&probability) { - return Err(PyValueError::new_err( - "Probability out of range, must be 0 <= p <= 1", - )); - } - if probability > 0.0 { - if (probability - 1.0).abs() < std::f64::EPSILON { - for u in 0..num_nodes { - for v in 0..num_nodes { - if u != v { - // exclude self-loops - let u_index = NodeIndex::new(u as usize); - let v_index = NodeIndex::new(v as usize); - inner_graph.add_edge(u_index, v_index, py.None()); - } - } - } - } else { - let mut v: isize = 0; - let mut w: isize = -1; - let lp: f64 = (1.0 - probability).ln(); - - let between = Uniform::new(0.0, 1.0); - while v < num_nodes { - let random: f64 = between.sample(&mut rng); - let lr: f64 = (1.0 - random).ln(); - let ratio: isize = (lr / lp) as isize; - w = w + 1 + ratio; - // avoid self loops - if v == w { - w += 1; - } - while v < num_nodes && num_nodes <= w { - w -= v; - v += 1; - // avoid self loops - if v == w { - w -= v; - v += 1; - } - } - if v < num_nodes { - let v_index = NodeIndex::new(v as usize); - let w_index = NodeIndex::new(w as usize); - inner_graph.add_edge(v_index, w_index, py.None()); - } - } - } - } - - let graph = digraph::PyDiGraph { - graph: inner_graph, - cycle_state: algo::DfsSpace::default(), - check_cycle: false, - node_removed: false, - multigraph: true, - }; - Ok(graph) -} - -/// Return a :math:`G_{np}` random undirected graph, also known as an -/// Erdős-Rényi graph or a binomial graph. -/// -/// For number of nodes :math:`n` and probability :math:`p`, the :math:`G_{n,p}` -/// graph algorithm creates :math:`n` nodes, and for all the :math:`n (n - 1)/2` possible edges, -/// each edge is created independently with probability :math:`p`. -/// In general, for any probability :math:`p`, the expected number of edges returned -/// is :math:`m = p n (n - 1)/2`. If :math:`p = 0` or :math:`p = 1`, the returned -/// graph is not random and will always be an empty or a complete graph respectively. -/// An empty graph has zero edges and a complete undirected graph has :math:`n (n - 1)/2` edges. -/// The run time is :math:`O(n + m)` where :math:`m` is the expected number of edges mentioned above. -/// When :math:`p = 0`, run time always reduces to :math:`O(n)`, as the lower bound. -/// When :math:`p = 1`, run time always goes to :math:`O(n + n (n - 1)/2)`, as the upper bound. -/// For other probabilities, this algorithm [1]_ runs in :math:`O(n + m)` time. -/// -/// For :math:`0 < p < 1`, the algorithm is based on the implementation of the networkx function -/// ``fast_gnp_random_graph`` [2]_ -/// -/// :param int num_nodes: The number of nodes to create in the graph -/// :param float probability: The probability of creating an edge between two nodes -/// :param int seed: An optional seed to use for the random number generator -/// -/// :return: A PyGraph object -/// :rtype: PyGraph -/// -/// .. [1] Vladimir Batagelj and Ulrik Brandes, -/// "Efficient generation of large random networks", -/// Phys. Rev. E, 71, 036113, 2005. -/// .. [2] https://github.com/networkx/networkx/blob/networkx-2.4/networkx/generators/random_graphs.py#L49-L120 -#[pyfunction] -#[pyo3(text_signature = "(num_nodes, probability, seed=None, /)")] -pub fn undirected_gnp_random_graph( - py: Python, - num_nodes: isize, - probability: f64, - seed: Option, -) -> PyResult { - if num_nodes <= 0 { - return Err(PyValueError::new_err("num_nodes must be > 0")); - } - let mut rng: Pcg64 = match seed { - Some(seed) => Pcg64::seed_from_u64(seed), - None => Pcg64::from_entropy(), - }; - let mut inner_graph = StableUnGraph::::default(); - for x in 0..num_nodes { - inner_graph.add_node(x.to_object(py)); - } - if !(0.0..=1.0).contains(&probability) { - return Err(PyValueError::new_err( - "Probability out of range, must be 0 <= p <= 1", - )); - } - if probability > 0.0 { - if (probability - 1.0).abs() < std::f64::EPSILON { - for u in 0..num_nodes { - for v in u + 1..num_nodes { - let u_index = NodeIndex::new(u as usize); - let v_index = NodeIndex::new(v as usize); - inner_graph.add_edge(u_index, v_index, py.None()); - } - } - } else { - let mut v: isize = 1; - let mut w: isize = -1; - let lp: f64 = (1.0 - probability).ln(); - - let between = Uniform::new(0.0, 1.0); - while v < num_nodes { - let random: f64 = between.sample(&mut rng); - let lr = (1.0 - random).ln(); - let ratio: isize = (lr / lp) as isize; - w = w + 1 + ratio; - while w >= v && v < num_nodes { - w -= v; - v += 1; - } - if v < num_nodes { - let v_index = NodeIndex::new(v as usize); - let w_index = NodeIndex::new(w as usize); - inner_graph.add_edge(v_index, w_index, py.None()); - } - } - } - } - - let graph = graph::PyGraph { - graph: inner_graph, - node_removed: false, - multigraph: true, - }; - Ok(graph) -} - -/// Return a :math:`G_{nm}` of a directed graph -/// -/// Generates a random directed graph out of all the possible graphs with :math:`n` nodes and -/// :math:`m` edges. The generated graph will not be a multigraph and will not have self loops. -/// -/// For :math:`n` nodes, the maximum edges that can be returned is :math:`n (n - 1)`. -/// Passing :math:`m` higher than that will still return the maximum number of edges. -/// If :math:`m = 0`, the returned graph will always be empty (no edges). -/// When a seed is provided, the results are reproducible. Passing a seed when :math:`m = 0` -/// or :math:`m >= n (n - 1)` has no effect, as the result will always be an empty or a complete graph respectively. -/// -/// This algorithm has a time complexity of :math:`O(n + m)` -/// -/// :param int num_nodes: The number of nodes to create in the graph -/// :param int num_edges: The number of edges to create in the graph -/// :param int seed: An optional seed to use for the random number generator -/// -/// :return: A PyDiGraph object -/// :rtype: PyDiGraph -/// -#[pyfunction] -#[pyo3(text_signature = "(num_nodes, num_edges, seed=None, /)")] -pub fn directed_gnm_random_graph( - py: Python, - num_nodes: isize, - num_edges: isize, - seed: Option, -) -> PyResult { - if num_nodes <= 0 { - return Err(PyValueError::new_err("num_nodes must be > 0")); - } - if num_edges < 0 { - return Err(PyValueError::new_err("num_edges must be >= 0")); - } - let mut rng: Pcg64 = match seed { - Some(seed) => Pcg64::seed_from_u64(seed), - None => Pcg64::from_entropy(), - }; - let mut inner_graph = StableDiGraph::::new(); - for x in 0..num_nodes { - inner_graph.add_node(x.to_object(py)); - } - // if number of edges to be created is >= max, - // avoid randomly missed trials and directly add edges between every node - if num_edges >= num_nodes * (num_nodes - 1) { - for u in 0..num_nodes { - for v in 0..num_nodes { - // avoid self-loops - if u != v { - let u_index = NodeIndex::new(u as usize); - let v_index = NodeIndex::new(v as usize); - inner_graph.add_edge(u_index, v_index, py.None()); - } - } - } - } else { - let mut created_edges: isize = 0; - let between = Uniform::new(0, num_nodes); - while created_edges < num_edges { - let u = between.sample(&mut rng); - let v = between.sample(&mut rng); - let u_index = NodeIndex::new(u as usize); - let v_index = NodeIndex::new(v as usize); - // avoid self-loops and multi-graphs - if u != v && inner_graph.find_edge(u_index, v_index).is_none() { - inner_graph.add_edge(u_index, v_index, py.None()); - created_edges += 1; - } - } - } - let graph = digraph::PyDiGraph { - graph: inner_graph, - cycle_state: algo::DfsSpace::default(), - check_cycle: false, - node_removed: false, - multigraph: true, - }; - Ok(graph) -} - -/// Return a :math:`G_{nm}` of an undirected graph -/// -/// Generates a random undirected graph out of all the possible graphs with :math:`n` nodes and -/// :math:`m` edges. The generated graph will not be a multigraph and will not have self loops. -/// -/// For :math:`n` nodes, the maximum edges that can be returned is :math:`n (n - 1)/2`. -/// Passing :math:`m` higher than that will still return the maximum number of edges. -/// If :math:`m = 0`, the returned graph will always be empty (no edges). -/// When a seed is provided, the results are reproducible. Passing a seed when :math:`m = 0` -/// or :math:`m >= n (n - 1)/2` has no effect, as the result will always be an empty or a complete graph respectively. -/// -/// This algorithm has a time complexity of :math:`O(n + m)` -/// -/// :param int num_nodes: The number of nodes to create in the graph -/// :param int num_edges: The number of edges to create in the graph -/// :param int seed: An optional seed to use for the random number generator -/// -/// :return: A PyGraph object -/// :rtype: PyGraph - -#[pyfunction] -#[pyo3(text_signature = "(num_nodes, probability, seed=None, /)")] -pub fn undirected_gnm_random_graph( - py: Python, - num_nodes: isize, - num_edges: isize, - seed: Option, -) -> PyResult { - if num_nodes <= 0 { - return Err(PyValueError::new_err("num_nodes must be > 0")); - } - if num_edges < 0 { - return Err(PyValueError::new_err("num_edges must be >= 0")); - } - let mut rng: Pcg64 = match seed { - Some(seed) => Pcg64::seed_from_u64(seed), - None => Pcg64::from_entropy(), - }; - let mut inner_graph = StableUnGraph::::default(); - for x in 0..num_nodes { - inner_graph.add_node(x.to_object(py)); - } - // if number of edges to be created is >= max, - // avoid randomly missed trials and directly add edges between every node - if num_edges >= num_nodes * (num_nodes - 1) / 2 { - for u in 0..num_nodes { - for v in u + 1..num_nodes { - let u_index = NodeIndex::new(u as usize); - let v_index = NodeIndex::new(v as usize); - inner_graph.add_edge(u_index, v_index, py.None()); - } - } - } else { - let mut created_edges: isize = 0; - let between = Uniform::new(0, num_nodes); - while created_edges < num_edges { - let u = between.sample(&mut rng); - let v = between.sample(&mut rng); - let u_index = NodeIndex::new(u as usize); - let v_index = NodeIndex::new(v as usize); - // avoid self-loops and multi-graphs - if u != v && inner_graph.find_edge(u_index, v_index).is_none() { - inner_graph.add_edge(u_index, v_index, py.None()); - created_edges += 1; - } - } - } - let graph = graph::PyGraph { - graph: inner_graph, - node_removed: false, - multigraph: true, - }; - Ok(graph) -} - -#[inline] -fn pnorm(x: f64, p: f64) -> f64 { - if p == 1.0 || p == std::f64::INFINITY { - x.abs() - } else if p == 2.0 { - x * x - } else { - x.abs().powf(p) - } -} - -fn distance(x: &[f64], y: &[f64], p: f64) -> f64 { - let it = x.iter().zip(y.iter()).map(|(xi, yi)| pnorm(xi - yi, p)); - - if p == std::f64::INFINITY { - it.fold(-1.0, |max, x| if x > max { x } else { max }) - } else { - it.sum() - } -} - -/// Returns a random geometric graph in the unit cube of dimensions `dim`. -/// -/// The random geometric graph model places `num_nodes` nodes uniformly at -/// random in the unit cube. Two nodes are joined by an edge if the -/// distance between the nodes is at most `radius`. -/// -/// Each node has a node attribute ``'pos'`` that stores the -/// position of that node in Euclidean space as provided by the -/// ``pos`` keyword argument or, if ``pos`` was not provided, as -/// generated by this function. -/// -/// :param int num_nodes: The number of nodes to create in the graph -/// :param float radius: Distance threshold value -/// :param int dim: Dimension of node positions. Default: 2 -/// :param list pos: Optional list with node positions as values -/// :param float p: Which Minkowski distance metric to use. `p` has to meet the condition -/// ``1 <= p <= infinity``. -/// If this argument is not specified, the :math:`L^2` metric -/// (the Euclidean distance metric), p = 2 is used. -/// :param int seed: An optional seed to use for the random number generator -/// -/// :return: A PyGraph object -/// :rtype: PyGraph -#[pyfunction(dim = "2", p = "2.0")] -#[pyo3( - text_signature = "(num_nodes, radius, /, dim=2, pos=None, p=2.0, seed=None)" -)] -pub fn random_geometric_graph( - py: Python, - num_nodes: usize, - radius: f64, - dim: usize, - pos: Option>>, - p: f64, - seed: Option, -) -> PyResult { - if num_nodes == 0 { - return Err(PyValueError::new_err("num_nodes must be > 0")); - } - - let mut inner_graph = StableUnGraph::::default(); - - let radius_p = pnorm(radius, p); - let mut rng: Pcg64 = match seed { - Some(seed) => Pcg64::seed_from_u64(seed), - None => Pcg64::from_entropy(), - }; - - let dist = Uniform::new(0.0, 1.0); - let pos = pos.unwrap_or_else(|| { - (0..num_nodes) - .map(|_| (0..dim).map(|_| dist.sample(&mut rng)).collect()) - .collect() - }); - - if num_nodes != pos.len() { - return Err(PyValueError::new_err( - "number of elements in pos and num_nodes must be equal", - )); - } - - for pval in pos.iter() { - let pos_dict = PyDict::new(py); - pos_dict.set_item("pos", pval.to_object(py))?; - - inner_graph.add_node(pos_dict.into()); - } - - for u in 0..(num_nodes - 1) { - for v in (u + 1)..num_nodes { - if distance(&pos[u], &pos[v], p) < radius_p { - inner_graph.add_edge( - NodeIndex::new(u), - NodeIndex::new(v), - py.None(), - ); - } - } - } - - let graph = graph::PyGraph { - graph: inner_graph, - node_removed: false, - multigraph: true, - }; - Ok(graph) -} - -/// Return a list of cycles which form a basis for cycles of a given PyGraph -/// -/// A basis for cycles of a graph is a minimal collection of -/// cycles such that any cycle in the graph can be written -/// as a sum of cycles in the basis. Here summation of cycles -/// is defined as the exclusive or of the edges. -/// -/// This is adapted from algorithm CACM 491 [1]_. -/// -/// :param PyGraph graph: The graph to find the cycle basis in -/// :param int root: Optional index for starting node for basis -/// -/// :returns: A list of cycle lists. Each list is a list of node ids which -/// forms a cycle (loop) in the input graph -/// :rtype: list -/// -/// .. [1] Paton, K. An algorithm for finding a fundamental set of -/// cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. -#[pyfunction] -#[pyo3(text_signature = "(graph, /, root=None)")] -pub fn cycle_basis( - graph: &graph::PyGraph, - root: Option, -) -> Vec> { - let mut root_node = root; - let mut graph_nodes: HashSet = - graph.graph.node_indices().collect(); - let mut cycles: Vec> = Vec::new(); - while !graph_nodes.is_empty() { - let temp_value: NodeIndex; - // If root_node is not set get an arbitrary node from the set of graph - // nodes we've not "examined" - let root_index = match root_node { - Some(root_value) => NodeIndex::new(root_value), - None => { - temp_value = *graph_nodes.iter().next().unwrap(); - graph_nodes.remove(&temp_value); - temp_value - } - }; - // Stack (ie "pushdown list") of vertices already in the spanning tree - let mut stack: Vec = vec![root_index]; - // Map of node index to predecessor node index - let mut pred: HashMap = HashMap::new(); - pred.insert(root_index, root_index); - // Set of examined nodes during this iteration - let mut used: HashMap> = HashMap::new(); - used.insert(root_index, HashSet::new()); - // Walk the spanning tree - while !stack.is_empty() { - // Use the last element added so that cycles are easier to find - let z = stack.pop().unwrap(); - for neighbor in graph.graph.neighbors(z) { - // A new node was encountered: - if !used.contains_key(&neighbor) { - pred.insert(neighbor, z); - stack.push(neighbor); - let mut temp_set: HashSet = HashSet::new(); - temp_set.insert(z); - used.insert(neighbor, temp_set); - // A self loop: - } else if z == neighbor { - let cycle: Vec = vec![z.index()]; - cycles.push(cycle); - // A cycle was found: - } else if !used.get(&z).unwrap().contains(&neighbor) { - let pn = used.get(&neighbor).unwrap(); - let mut cycle: Vec = vec![neighbor, z]; - let mut p = pred.get(&z).unwrap(); - while !pn.contains(p) { - cycle.push(*p); - p = pred.get(p).unwrap(); - } - cycle.push(*p); - cycles.push(cycle.iter().map(|x| x.index()).collect()); - let neighbor_set = used.get_mut(&neighbor).unwrap(); - neighbor_set.insert(z); - } - } - } - let mut temp_hashset: HashSet = HashSet::new(); - for key in pred.keys() { - temp_hashset.insert(*key); - } - graph_nodes = graph_nodes.difference(&temp_hashset).copied().collect(); - root_node = None; - } - cycles -} - -/// Compute a maximum-weighted matching for a :class:`~retworkx.PyGraph` -/// -/// A matching is a subset of edges in which no node occurs more than once. -/// The weight of a matching is the sum of the weights of its edges. -/// A maximal matching cannot add more edges and still be a matching. -/// The cardinality of a matching is the number of matched edges. -/// -/// This function takes time :math:`O(n^3)` where ``n`` is the number of nodes -/// in the graph. -/// -/// This method is based on the "blossom" method for finding augmenting -/// paths and the "primal-dual" method for finding a matching of maximum -/// weight, both methods invented by Jack Edmonds [1]_. -/// -/// :param PyGraph graph: The undirected graph to compute the max weight -/// matching for. Expects to have no parallel edges (multigraphs are -/// untested currently). -/// :param bool max_cardinality: If True, compute the maximum-cardinality -/// matching with maximum weight among all maximum-cardinality matchings. -/// Defaults False. -/// :param callable weight_fn: An optional callable that will be passed a -/// single argument the edge object for each edge in the graph. It is -/// expected to return an ``int`` weight for that edge. For example, -/// if the weights are all integers you can use: ``lambda x: x``. If not -/// specified the value for ``default_weight`` will be used for all -/// edge weights. -/// :param int default_weight: The ``int`` value to use for all edge weights -/// in the graph if ``weight_fn`` is not specified. Defaults to ``1``. -/// :param bool verify_optimum: A boolean flag to run a check that the found -/// solution is optimum. If set to true an exception will be raised if -/// the found solution is not optimum. This is mostly useful for testing. -/// -/// :returns: A set of tuples ofthe matching, Note that only a single -/// direction will be listed in the output, for example: -/// ``{(0, 1),}``. -/// :rtype: set -/// -/// .. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs", -/// Zvi Galil, ACM Computing Surveys, 1986. -/// -#[pyfunction( - max_cardinality = "false", - default_weight = 1, - verify_optimum = "false" -)] -#[pyo3( - text_signature = "(graph, /, max_cardinality=False, weight_fn=None, default_weight=1, verify_optimum=False)" -)] -pub fn max_weight_matching( - py: Python, - graph: &graph::PyGraph, - max_cardinality: bool, - weight_fn: Option, - default_weight: i128, - verify_optimum: bool, -) -> PyResult> { - max_weight_matching::max_weight_matching( - py, - graph, - max_cardinality, - weight_fn, - default_weight, - verify_optimum, - ) -} - -/// Compute the strongly connected components for a directed graph -/// -/// This function is implemented using Kosaraju's algorithm -/// -/// :param PyDiGraph graph: The input graph to find the strongly connected -/// components for. -/// -/// :return: A list of list of node ids for strongly connected components -/// :rtype: list -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -pub fn strongly_connected_components( - graph: &digraph::PyDiGraph, -) -> Vec> { - algo::kosaraju_scc(graph) - .iter() - .map(|x| x.iter().map(|id| id.index()).collect()) - .collect() -} - -/// Return the first cycle encountered during DFS of a given PyDiGraph, -/// empty list is returned if no cycle is found -/// -/// :param PyDiGraph graph: The graph to find the cycle in -/// :param int source: Optional index to find a cycle for. If not specified an -/// arbitrary node will be selected from the graph. -/// -/// :returns: A list describing the cycle. The index of node ids which -/// forms a cycle (loop) in the input graph -/// :rtype: EdgeList -#[pyfunction] -#[pyo3(text_signature = "(graph, /, source=None)")] -pub fn digraph_find_cycle( - graph: &digraph::PyDiGraph, - source: Option, -) -> EdgeList { - let mut graph_nodes: HashSet = - graph.graph.node_indices().collect(); - let mut cycle: Vec<(usize, usize)> = - Vec::with_capacity(graph.graph.edge_count()); - let temp_value: NodeIndex; - // If source is not set get an arbitrary node from the set of graph - // nodes we've not "examined" - let source_index = match source { - Some(source_value) => NodeIndex::new(source_value), - None => { - temp_value = *graph_nodes.iter().next().unwrap(); - graph_nodes.remove(&temp_value); - temp_value - } - }; - - // Stack (ie "pushdown list") of vertices already in the spanning tree - let mut stack: Vec = vec![source_index]; - // map to store parent of a node - let mut pred: HashMap = HashMap::new(); - // a node is in the visiting set if at least one of its child is unexamined - let mut visiting = HashSet::new(); - // a node is in visited set if all of its children have been examined - let mut visited = HashSet::new(); - while !stack.is_empty() { - let mut z = *stack.last().unwrap(); - visiting.insert(z); - - let children = graph - .graph - .neighbors_directed(z, petgraph::Direction::Outgoing); - - for child in children { - //cycle is found - if visiting.contains(&child) { - cycle.push((z.index(), child.index())); - //backtrack - loop { - if z == child { - cycle.reverse(); - break; - } - cycle.push((pred[&z].index(), z.index())); - z = pred[&z]; - } - return EdgeList { edges: cycle }; - } - //if an unexplored node is encountered - if !visited.contains(&child) { - stack.push(child); - pred.insert(child, z); - } - } - - let top = *stack.last().unwrap(); - //if no further children and explored, move to visited - if top.index() == z.index() { - stack.pop(); - visiting.remove(&z); - visited.insert(z); - } - } - EdgeList { edges: cycle } -} - -fn _inner_is_matching( - graph: &graph::PyGraph, - matching: &HashSet<(usize, usize)>, -) -> bool { - let has_edge = |e: &(usize, usize)| -> bool { - graph - .graph - .contains_edge(NodeIndex::new(e.0), NodeIndex::new(e.1)) - }; - - if !matching.iter().all(|e| has_edge(e)) { - return false; - } - let mut found: HashSet = HashSet::with_capacity(2 * matching.len()); - for (v1, v2) in matching { - if found.contains(v1) || found.contains(v2) { - return false; - } - found.insert(*v1); - found.insert(*v2); - } - true -} - -/// Check if matching is valid for graph -/// -/// A *matching* in a graph is a set of edges in which no two distinct -/// edges share a common endpoint. -/// -/// :param PyDiGraph graph: The graph to check if the matching is valid for -/// :param set matching: A set of node index tuples for each edge in the -/// matching. -/// -/// :returns: Whether the provided matching is a valid matching for the graph -/// :rtype: bool -#[pyfunction] -#[pyo3(text_signature = "(graph, matching, /)")] -pub fn is_matching( - graph: &graph::PyGraph, - matching: HashSet<(usize, usize)>, -) -> bool { - _inner_is_matching(graph, &matching) -} - -/// Check if a matching is a maximal (**not** maximum) matching for a graph -/// -/// A *maximal matching* in a graph is a matching in which adding any -/// edge would cause the set to no longer be a valid matching. -/// -/// .. note:: -/// -/// This is not checking for a *maximum* (globally optimal) matching, but -/// a *maximal* (locally optimal) matching. -/// -/// :param PyDiGraph graph: The graph to check if the matching is maximal for. -/// :param set matching: A set of node index tuples for each edge in the -/// matching. -/// -/// :returns: Whether the provided matching is a valid matching and whether it -/// is maximal or not. -/// :rtype: bool -#[pyfunction] -#[pyo3(text_signature = "(graph, matching, /)")] -pub fn is_maximal_matching( - graph: &graph::PyGraph, - matching: HashSet<(usize, usize)>, -) -> bool { - if !_inner_is_matching(graph, &matching) { - return false; - } - let edge_list: HashSet<[usize; 2]> = graph - .edge_references() - .map(|edge| { - let mut tmp_array = [edge.source().index(), edge.target().index()]; - tmp_array.sort_unstable(); - tmp_array - }) - .collect(); - let matched_edges: HashSet<[usize; 2]> = matching - .iter() - .map(|edge| { - let mut tmp_array = [edge.0, edge.1]; - tmp_array.sort_unstable(); - tmp_array - }) - .collect(); - let mut unmatched_edges = edge_list.difference(&matched_edges); - unmatched_edges.all(|e| { - let mut tmp_set = matching.clone(); - tmp_set.insert((e[0], e[1])); - !_inner_is_matching(graph, &tmp_set) - }) -} - -fn _graph_triangles(graph: &graph::PyGraph, node: usize) -> (usize, usize) { - let mut triangles: usize = 0; - - let index = NodeIndex::new(node); - let mut neighbors: HashSet = - graph.graph.neighbors(index).collect(); - neighbors.remove(&index); - - for nodev in &neighbors { - triangles += graph - .graph - .neighbors(*nodev) - .filter(|&x| (x != *nodev) && neighbors.contains(&x)) - .count(); - } - - let d: usize = neighbors.len(); - let triples: usize = match d { - 0 => 0, - _ => (d * (d - 1)) / 2, - }; - - (triangles / 2, triples) -} - -/// Compute the transitivity of an undirected graph. -/// -/// The transitivity of a graph is defined as: -/// -/// .. math:: -/// `c=3 \times \frac{\text{number of triangles}}{\text{number of connected triples}}` -/// -/// A “connected triple” means a single vertex with -/// edges running to an unordered pair of others. -/// -/// This function is multithreaded and will run -/// launch a thread pool with threads equal to the number of CPUs by default. -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads. -/// -/// .. note:: -/// -/// The function implicitly assumes that there are no parallel edges -/// or self loops. It may produce incorrect/unexpected results if the -/// input graph has self loops or parallel edges. -/// -/// :param PyGraph graph: Graph to be used. -/// -/// :returns: Transitivity. -/// :rtype: float -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn graph_transitivity(graph: &graph::PyGraph) -> f64 { - let node_indices: Vec = graph.graph.node_indices().collect(); - let (triangles, triples) = node_indices - .par_iter() - .map(|node| _graph_triangles(graph, node.index())) - .reduce( - || (0, 0), - |(sumx, sumy), (resx, resy)| (sumx + resx, sumy + resy), - ); - - match triangles { - 0 => 0.0, - _ => triangles as f64 / triples as f64, - } -} - -fn _digraph_triangles( - graph: &digraph::PyDiGraph, - node: usize, -) -> (usize, usize) { - let mut triangles: usize = 0; - - let index = NodeIndex::new(node); - let mut out_neighbors: HashSet = graph - .graph - .neighbors_directed(index, petgraph::Direction::Outgoing) - .collect(); - out_neighbors.remove(&index); - - let mut in_neighbors: HashSet = graph - .graph - .neighbors_directed(index, petgraph::Direction::Incoming) - .collect(); - in_neighbors.remove(&index); - - let neighbors = out_neighbors.iter().chain(in_neighbors.iter()); - - for nodev in neighbors { - triangles += graph - .graph - .neighbors_directed(*nodev, petgraph::Direction::Outgoing) - .chain( - graph - .graph - .neighbors_directed(*nodev, petgraph::Direction::Incoming), - ) - .map(|x| { - let mut res: usize = 0; - - if (x != *nodev) && out_neighbors.contains(&x) { - res += 1; - } - if (x != *nodev) && in_neighbors.contains(&x) { - res += 1; - } - res - }) - .sum::(); - } - - let din: usize = in_neighbors.len(); - let dout: usize = out_neighbors.len(); - - let dtot = dout + din; - let dbil: usize = out_neighbors.intersection(&in_neighbors).count(); - let triples: usize = match dtot { - 0 => 0, - _ => dtot * (dtot - 1) - 2 * dbil, - }; - - (triangles / 2, triples) -} - -/// Compute the transitivity of a directed graph. -/// -/// The transitivity of a directed graph is defined in [Fag]_, Eq.8: -/// -/// .. math:: -/// `c=3 \times \frac{\text{number of triangles}}{\text{number of all possible triangles}}` -/// -/// A triangle is a connected triple of nodes. -/// Different edge orientations counts as different triangles. -/// -/// This function is multithreaded and will run -/// launch a thread pool with threads equal to the number of CPUs by default. -/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` -/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would -/// limit the thread pool to 4 threads. -/// -/// .. note:: -/// -/// The function implicitly assumes that there are no parallel edges -/// or self loops. It may produce incorrect/unexpected results if the -/// input graph has self loops or parallel edges. -/// -/// :param PyDiGraph graph: Directed graph to be used. -/// -/// :returns: Transitivity. -/// :rtype: float -/// -/// .. [Fag] Clustering in complex directed networks by G. Fagiolo, -/// Physical Review E, 76(2), 026107 (2007) -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn digraph_transitivity(graph: &digraph::PyDiGraph) -> f64 { - let node_indices: Vec = graph.graph.node_indices().collect(); - let (triangles, triples) = node_indices - .par_iter() - .map(|node| _digraph_triangles(graph, node.index())) - .reduce( - || (0, 0), - |(sumx, sumy), (resx, resy)| (sumx + resx, sumy + resy), - ); - - match triangles { - 0 => 0.0, - _ => triangles as f64 / triples as f64, - } -} - -pub fn _core_number( - py: Python, - graph: &StableGraph, -) -> PyResult -where - Ty: EdgeType, -{ - let node_num = graph.node_count(); - if node_num == 0 { - return Ok(PyDict::new(py).into()); - } - - let mut cores: HashMap = HashMap::with_capacity(node_num); - let mut node_vec: Vec = graph.node_indices().collect(); - let mut degree_map: HashMap = - HashMap::with_capacity(node_num); - let mut nbrs: HashMap> = - HashMap::with_capacity(node_num); - let mut node_pos: HashMap = - HashMap::with_capacity(node_num); - - for k in node_vec.iter() { - let k_nbrs: HashSet = - graph.neighbors_undirected(*k).collect(); - let k_deg = k_nbrs.len(); - - nbrs.insert(*k, k_nbrs); - cores.insert(*k, k_deg); - degree_map.insert(*k, k_deg); - } - node_vec.par_sort_by_key(|k| degree_map.get(k)); - - let mut bin_boundaries: Vec = - Vec::with_capacity(degree_map[&node_vec[node_num - 1]] + 1); - bin_boundaries.push(0); - let mut curr_degree = 0; - for (i, v) in node_vec.iter().enumerate() { - node_pos.insert(*v, i); - let v_degree = degree_map[v]; - if v_degree > curr_degree { - for _ in 0..v_degree - curr_degree { - bin_boundaries.push(i); - } - curr_degree = v_degree; - } - } - - for v_ind in 0..node_vec.len() { - let v = node_vec[v_ind]; - let v_nbrs = nbrs[&v].clone(); - for u in v_nbrs { - if cores[&u] > cores[&v] { - nbrs.get_mut(&u).unwrap().remove(&v); - let pos = node_pos[&u]; - let bin_start = bin_boundaries[cores[&u]]; - *node_pos.get_mut(&u).unwrap() = bin_start; - *node_pos.get_mut(&node_vec[bin_start]).unwrap() = pos; - node_vec.swap(bin_start, pos); - bin_boundaries[cores[&u]] += 1; - *cores.get_mut(&u).unwrap() -= 1; - } - } - } - - let out_dict = PyDict::new(py); - for (v_index, core) in cores { - out_dict.set_item(v_index.index(), core)?; - } - Ok(out_dict.into()) -} - -/// Return the core number for each node in the graph. -/// -/// A k-core is a maximal subgraph that contains nodes of degree k or more. -/// -/// .. note:: -/// -/// The function implicitly assumes that there are no parallel edges -/// or self loops. It may produce incorrect/unexpected results if the -/// input graph has self loops or parallel edges. -/// -/// :param PyGraph: The graph to get core numbers -/// -/// :returns: A dictionary keyed by node index to the core number -/// :rtype: dict -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -pub fn graph_core_number( - py: Python, - graph: &graph::PyGraph, -) -> PyResult { - _core_number(py, &graph.graph) -} - -/// Return the core number for each node in the directed graph. -/// -/// A k-core is a maximal subgraph that contains nodes of degree k or more. -/// For directed graphs, the degree is calculated as in_degree + out_degree. -/// -/// .. note:: -/// -/// The function implicitly assumes that there are no parallel edges -/// or self loops. It may produce incorrect/unexpected results if the -/// input graph has self loops or parallel edges. -/// -/// :param PyDiGraph: The directed graph to get core numbers -/// -/// :returns: A dictionary keyed by node index to the core number -/// :rtype: dict -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -pub fn digraph_core_number( - py: Python, - graph: &digraph::PyDiGraph, -) -> PyResult { - _core_number(py, &graph.graph) -} - -/// Find the edges in the minimum spanning tree or forest of a graph -/// using Kruskal's algorithm. -/// -/// :param PyGraph graph: Undirected graph -/// :param weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// minimum_spanning_edges(graph, weight_fn: lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// minimum_spanning_edges(graph, weight_fn: float) -/// -/// to cast the edge object as a float as the weight. -/// :param float default_weight: If ``weight_fn`` isn't specified this optional -/// float value will be used for the weight/cost of each edge. -/// -/// :returns: The :math:`N - |c|` edges of the Minimum Spanning Tree (or Forest, if :math:`|c| > 1`) -/// where :math:`N` is the number of nodes and :math:`|c|` is the number of connected components of the graph -/// :rtype: WeightedEdgeList -#[pyfunction(weight_fn = "None", default_weight = "1.0")] -#[pyo3(text_signature = "(graph, weight_fn=None, default_weight=1.0)")] -pub fn minimum_spanning_edges( - py: Python, - graph: &graph::PyGraph, - weight_fn: Option, - default_weight: f64, -) -> PyResult { - let mut subgraphs = UnionFind::::new(graph.graph.node_bound()); - - let mut edge_list: Vec<(f64, EdgeReference)> = - Vec::with_capacity(graph.graph.edge_count()); - for edge in graph.edge_references() { - let weight = - weight_callable(py, &weight_fn, edge.weight(), default_weight)?; - if weight.is_nan() { - return Err(PyValueError::new_err("NaN found as an edge weight")); - } - edge_list.push((weight, edge)); - } - - edge_list.par_sort_unstable_by(|a, b| { - let weight_a = a.0; - let weight_b = b.0; - weight_a.partial_cmp(&weight_b).unwrap_or(Ordering::Less) - }); - - let mut answer: Vec<(usize, usize, PyObject)> = Vec::new(); - for float_edge_pair in edge_list.iter() { - let edge = float_edge_pair.1; - let u = edge.source().index(); - let v = edge.target().index(); - if subgraphs.union(u, v) { - let w = edge.weight().clone_ref(py); - answer.push((u, v, w)); - } - } - - Ok(WeightedEdgeList { edges: answer }) -} - -/// Find the minimum spanning tree or forest of a graph -/// using Kruskal's algorithm. -/// -/// :param PyGraph graph: Undirected graph -/// :param weight_fn: A callable object (function, lambda, etc) which -/// will be passed the edge object and expected to return a ``float``. This -/// tells retworkx/rust how to extract a numerical weight as a ``float`` -/// for edge object. Some simple examples are:: -/// -/// minimum_spanning_tree(graph, weight_fn: lambda x: 1) -/// -/// to return a weight of 1 for all edges. Also:: -/// -/// minimum_spanning_tree(graph, weight_fn: float) -/// -/// to cast the edge object as a float as the weight. -/// :param float default_weight: If ``weight_fn`` isn't specified this optional -/// float value will be used for the weight/cost of each edge. -/// -/// :returns: A Minimum Spanning Tree (or Forest, if the graph is not connected). -/// -/// :rtype: PyGraph -/// -/// .. note:: -/// -/// The new graph will keep the same node indexes, but edge indexes might differ. -#[pyfunction(weight_fn = "None", default_weight = "1.0")] -#[pyo3(text_signature = "(graph, weight_fn=None, default_weight=1.0)")] -pub fn minimum_spanning_tree( - py: Python, - graph: &graph::PyGraph, - weight_fn: Option, - default_weight: f64, -) -> PyResult { - let mut spanning_tree = (*graph).clone(); - spanning_tree.graph.clear_edges(); - - for edge in minimum_spanning_edges(py, graph, weight_fn, default_weight)? - .edges - .iter() - { - spanning_tree.add_edge(edge.0, edge.1, edge.2.clone_ref(py))?; - } - - Ok(spanning_tree) -} - -/// Compute the complement of a graph. -/// -/// :param PyGraph graph: The graph to be used. -/// -/// :returns: The complement of the graph. -/// :rtype: PyGraph -/// -/// .. note:: -/// -/// Parallel edges and self-loops are never created, -/// even if the :attr:`~retworkx.PyGraph.multigraph` -/// attribute is set to ``True`` -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn graph_complement( - py: Python, - graph: &graph::PyGraph, -) -> PyResult { - let mut complement_graph = graph.clone(); // keep same node indexes - complement_graph.graph.clear_edges(); - - for node_a in graph.graph.node_indices() { - let old_neighbors: HashSet = - graph.graph.neighbors(node_a).collect(); - for node_b in graph.graph.node_indices() { - if node_a != node_b - && !old_neighbors.contains(&node_b) - && (!complement_graph.multigraph - || !complement_graph - .has_edge(node_a.index(), node_b.index())) - { - // avoid creating parallel edges in multigraph - complement_graph.add_edge( - node_a.index(), - node_b.index(), - py.None(), - )?; - } - } - } - Ok(complement_graph) -} - -/// Compute the complement of a graph. -/// -/// :param PyDiGraph graph: The graph to be used. -/// -/// :returns: The complement of the graph. -/// :rtype: :class:`~retworkx.PyDiGraph` -/// -/// .. note:: -/// -/// Parallel edges and self-loops are never created, -/// even if the :attr:`~retworkx.PyDiGraph.multigraph` -/// attribute is set to ``True`` -#[pyfunction] -#[pyo3(text_signature = "(graph, /)")] -fn digraph_complement( - py: Python, - graph: &digraph::PyDiGraph, -) -> PyResult { - let mut complement_graph = graph.clone(); // keep same node indexes - complement_graph.graph.clear_edges(); - - for node_a in graph.graph.node_indices() { - let old_neighbors: HashSet = graph - .graph - .neighbors_directed(node_a, petgraph::Direction::Outgoing) - .collect(); - for node_b in graph.graph.node_indices() { - if node_a != node_b && !old_neighbors.contains(&node_b) { - complement_graph.add_edge( - node_a.index(), - node_b.index(), - py.None(), - )?; - } - } - } - - Ok(complement_graph) -} - -#[allow(clippy::too_many_arguments)] -fn _spring_layout( - py: Python, - graph: &StableGraph, - pos: Option>, - fixed: Option>, - k: Option, - repulsive_exponent: Option, - adaptive_cooling: Option, - num_iter: Option, - tol: Option, - weight_fn: Option, - default_weight: f64, - scale: Option, - center: Option, - seed: Option, -) -> PyResult -where - Ty: EdgeType, -{ - if fixed.is_some() && pos.is_none() { - return Err(PyValueError::new_err("`fixed` specified but `pos` not.")); - } - - let mut rng: Pcg64 = match seed { - Some(seed) => Pcg64::seed_from_u64(seed), - None => Pcg64::from_entropy(), - }; - - let dist = Uniform::new(0.0, 1.0); - - let pos = pos.unwrap_or_default(); - let mut vpos: Vec = (0..graph.node_bound()) - .map(|_| [dist.sample(&mut rng), dist.sample(&mut rng)]) - .collect(); - for (n, p) in pos.into_iter() { - vpos[n] = p; - } - - let fixed = fixed.unwrap_or_default(); - let k = k.unwrap_or(1.0 / (graph.node_count() as f64).sqrt()); - let f_a = layout::AttractiveForce::new(k); - let f_r = layout::RepulsiveForce::new(k, repulsive_exponent.unwrap_or(2)); - - let num_iter = num_iter.unwrap_or(50); - let tol = tol.unwrap_or(1e-6); - let step = 0.1; - - let mut weights: HashMap<(usize, usize), f64> = - HashMap::with_capacity(2 * graph.edge_count()); - for e in graph.edge_references() { - let w = weight_callable(py, &weight_fn, e.weight(), default_weight)?; - let source = e.source().index(); - let target = e.target().index(); - - weights.insert((source, target), w); - weights.insert((target, source), w); - } - - let pos = match adaptive_cooling { - Some(false) => { - let cs = layout::LinearCoolingScheme::new(step, num_iter); - layout::evolve( - graph, vpos, fixed, f_a, f_r, cs, num_iter, tol, weights, - scale, center, - ) - } - _ => { - let cs = layout::AdaptiveCoolingScheme::new(step); - layout::evolve( - graph, vpos, fixed, f_a, f_r, cs, num_iter, tol, weights, - scale, center, - ) - } - }; - - Ok(Pos2DMapping { - pos_map: graph - .node_indices() - .map(|n| { - let n = n.index(); - (n, pos[n]) - }) - .collect(), - }) -} - -/// Position nodes using Fruchterman-Reingold force-directed algorithm. -/// -/// The algorithm simulates a force-directed representation of the network -/// treating edges as springs holding nodes close, while treating nodes -/// as repelling objects, sometimes called an anti-gravity force. -/// Simulation continues until the positions are close to an equilibrium. -/// -/// :param PyGraph graph: Graph to be used. -/// :param dict pos: -/// Initial node positions as a dictionary with node ids as keys and values -/// as a coordinate list. If ``None``, then use random initial positions. (``default=None``) -/// :param set fixed: Nodes to keep fixed at initial position. -/// Error raised if fixed specified and ``pos`` is not. (``default=None``) -/// :param float k: -/// Optimal distance between nodes. If ``None`` the distance is set to -/// :math:`\frac{1}{\sqrt{n}}` where :math:`n` is the number of nodes. Increase this value -/// to move nodes farther apart. (``default=None``) -/// :param int repulsive_exponent: -/// Repulsive force exponent. (``default=2``) -/// :param bool adaptive_cooling: -/// Use an adaptive cooling scheme. If set to ``False``, -/// a linear cooling scheme is used. (``default=True``) -/// :param int num_iter: -/// Maximum number of iterations. (``default=50``) -/// :param float tol: -/// Threshold for relative error in node position changes. -/// The iteration stops if the error is below this threshold. -/// (``default = 1e-6``) -/// :param weight_fn: An optional weight function for an edge. It will accept -/// a single argument, the edge's weight object and will return a float -/// which will be used to represent the weight of the edge. -/// :param float (default=1) default_weight: If ``weight_fn`` isn't specified -/// this optional float value will be used for the weight/cost of each edge -/// :param float|None scale: Scale factor for positions. -/// Not used unless fixed is None. If scale is ``None``, no re-scaling is -/// performed. (``default=1.0``) -/// :param list center: Coordinate pair around which to center -/// the layout. Not used unless fixed is ``None``. (``default=None``) -/// :param int seed: An optional seed to use for the random number generator -/// -/// :returns: A dictionary of positions keyed by node id. -/// :rtype: dict -#[pyfunction] -#[pyo3( - text_signature = "(graph, pos=None, fixed=None, k=None, repulsive_exponent=2, adaptive_cooling=True, - num_iter=50, tol=1e-6, weight_fn=None, default_weight=1, scale=1, - center=None, seed=None, /)" -)] -#[allow(clippy::too_many_arguments)] -pub fn graph_spring_layout( - py: Python, - graph: &graph::PyGraph, - pos: Option>, - fixed: Option>, - k: Option, - repulsive_exponent: Option, - adaptive_cooling: Option, - num_iter: Option, - tol: Option, - weight_fn: Option, - default_weight: f64, - scale: Option, - center: Option, - seed: Option, -) -> PyResult { - _spring_layout( - py, - &graph.graph, - pos, - fixed, - k, - repulsive_exponent, - adaptive_cooling, - num_iter, - tol, - weight_fn, - default_weight, - scale, - center, - seed, - ) -} - -/// Position nodes using Fruchterman-Reingold force-directed algorithm. -/// -/// The algorithm simulates a force-directed representation of the network -/// treating edges as springs holding nodes close, while treating nodes -/// as repelling objects, sometimes called an anti-gravity force. -/// Simulation continues until the positions are close to an equilibrium. -/// -/// :param PyGraph graph: Graph to be used. -/// :param dict pos: -/// Initial node positions as a dictionary with node ids as keys and values -/// as a coordinate list. If ``None``, then use random initial positions. (``default=None``) -/// :param set fixed: Nodes to keep fixed at initial position. -/// Error raised if fixed specified and ``pos`` is not. (``default=None``) -/// :param float k: -/// Optimal distance between nodes. If ``None`` the distance is set to -/// :math:`\frac{1}{\sqrt{n}}` where :math:`n` is the number of nodes. Increase this value -/// to move nodes farther apart. (``default=None``) -/// :param int repulsive_exponent: -/// Repulsive force exponent. (``default=2``) -/// :param bool adaptive_cooling: -/// Use an adaptive cooling scheme. If set to ``False``, -/// a linear cooling scheme is used. (``default=True``) -/// :param int num_iter: -/// Maximum number of iterations. (``default=50``) -/// :param float tol: -/// Threshold for relative error in node position changes. -/// The iteration stops if the error is below this threshold. -/// (``default = 1e-6``) -/// :param weight_fn: An optional weight function for an edge. It will accept -/// a single argument, the edge's weight object and will return a float -/// which will be used to represent the weight of the edge. -/// :param float (default=1) default_weight: If ``weight_fn`` isn't specified -/// this optional float value will be used for the weight/cost of each edge -/// :param float|None scale: Scale factor for positions. -/// Not used unless fixed is None. If scale is ``None``, no re-scaling is -/// performed. (``default=1.0``) -/// :param list center: Coordinate pair around which to center -/// the layout. Not used unless fixed is ``None``. (``default=None``) -/// :param int seed: An optional seed to use for the random number generator -/// -/// :returns: A dictionary of positions keyed by node id. -/// :rtype: dict -#[pyfunction] -#[pyo3( - text_signature = "(graph, pos=None, fixed=None, k=None, repulsive_exponent=2, adaptive_cooling=True, - num_iter=50, tol=1e-6, weight_fn=None, default_weight=1, scale=1, - center=None, seed=None, /)" -)] -#[allow(clippy::too_many_arguments)] -pub fn digraph_spring_layout( - py: Python, - graph: &digraph::PyDiGraph, - pos: Option>, - fixed: Option>, - k: Option, - repulsive_exponent: Option, - adaptive_cooling: Option, - num_iter: Option, - tol: Option, - weight_fn: Option, - default_weight: f64, - scale: Option, - center: Option, - seed: Option, -) -> PyResult { - _spring_layout( - py, - &graph.graph, - pos, - fixed, - k, - repulsive_exponent, - adaptive_cooling, - num_iter, - tol, - weight_fn, - default_weight, - scale, - center, - seed, - ) -} - -fn _random_layout( - graph: &StableGraph, - center: Option<[f64; 2]>, - seed: Option, -) -> Pos2DMapping { - let mut rng: Pcg64 = match seed { - Some(seed) => Pcg64::seed_from_u64(seed), - None => Pcg64::from_entropy(), - }; - - Pos2DMapping { - pos_map: graph - .node_indices() - .map(|n| { - let random_tuple: [f64; 2] = rng.gen(); - match center { - Some(center) => ( - n.index(), - [ - random_tuple[0] + center[0], - random_tuple[1] + center[1], - ], - ), - None => (n.index(), random_tuple), - } - }) - .collect(), - } -} - -/// Generate a random layout -/// -/// :param PyGraph graph: The graph to generate the layout for -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// :param int seed: An optional seed to set for the random number generator. -/// -/// :returns: The random layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, / center=None, seed=None)")] -pub fn graph_random_layout( - graph: &graph::PyGraph, - center: Option<[f64; 2]>, - seed: Option, -) -> Pos2DMapping { - _random_layout(&graph.graph, center, seed) -} - -/// Generate a random layout -/// -/// :param PyDiGraph graph: The graph to generate the layout for -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// :param int seed: An optional seed to set for the random number generator. -/// -/// :returns: The random layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, / center=None, seed=None)")] -pub fn digraph_random_layout( - graph: &digraph::PyDiGraph, - center: Option<[f64; 2]>, - seed: Option, -) -> Pos2DMapping { - _random_layout(&graph.graph, center, seed) -} - -/// Generate a bipartite layout of the graph -/// -/// :param PyGraph graph: The graph to generate the layout for -/// :param set first_nodes: The set of node indexes on the left (or top if -/// horitontal is true) -/// :param bool horizontal: An optional bool specifying the orientation of the -/// layout -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// :param float aspect_ratio: An optional number for the ratio of the width to -/// the height of the layout. -/// -/// :returns: The bipartite layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, first_nodes, /, horitontal=False, scale=1, - center=None, aspect_ratio=1.33333333333333)")] -pub fn graph_bipartite_layout( - graph: &graph::PyGraph, - first_nodes: HashSet, - horizontal: Option, - scale: Option, - center: Option, - aspect_ratio: Option, -) -> Pos2DMapping { - layout::bipartite_layout( - &graph.graph, - first_nodes, - horizontal, - scale, - center, - aspect_ratio, - ) -} - -/// Generate a bipartite layout of the graph -/// -/// :param PyDiGraph graph: The graph to generate the layout for -/// :param set first_nodes: The set of node indexes on the left (or top if -/// horizontal is true) -/// :param bool horizontal: An optional bool specifying the orientation of the -/// layout -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// :param float aspect_ratio: An optional number for the ratio of the width to -/// the height of the layout. -/// -/// :returns: The bipartite layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, first_nodes, /, horitontal=False, scale=1, - center=None, aspect_ratio=1.33333333333333)")] -pub fn digraph_bipartite_layout( - graph: &digraph::PyDiGraph, - first_nodes: HashSet, - horizontal: Option, - scale: Option, - center: Option, - aspect_ratio: Option, -) -> Pos2DMapping { - layout::bipartite_layout( - &graph.graph, - first_nodes, - horizontal, - scale, - center, - aspect_ratio, - ) -} - -/// Generate a circular layout of the graph -/// -/// :param PyGraph graph: The graph to generate the layout for -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// -/// :returns: The circular layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, /, scale=1, center=None)")] -pub fn graph_circular_layout( - graph: &graph::PyGraph, - scale: Option, - center: Option, -) -> Pos2DMapping { - layout::circular_layout(&graph.graph, scale, center) -} - -/// Generate a circular layout of the graph -/// -/// :param PyDiGraph graph: The graph to generate the layout for -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// -/// :returns: The circular layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, /, scale=1, center=None)")] -pub fn digraph_circular_layout( - graph: &digraph::PyDiGraph, - scale: Option, - center: Option, -) -> Pos2DMapping { - layout::circular_layout(&graph.graph, scale, center) -} - -/// Generate a shell layout of the graph -/// -/// :param PyGraph graph: The graph to generate the layout for -/// :param list nlist: The list of lists of indexes which represents each shell -/// :param float rotate: Angle (in radians) by which to rotate the starting -/// position of each shell relative to the starting position of the -/// previous shell -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// -/// :returns: The shell layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3( - text_signature = "(graph, /, nlist=None, rotate=None, scale=1, center=None)" -)] -pub fn graph_shell_layout( - graph: &graph::PyGraph, - nlist: Option>>, - rotate: Option, - scale: Option, - center: Option, -) -> Pos2DMapping { - layout::shell_layout(&graph.graph, nlist, rotate, scale, center) -} - -/// Generate a shell layout of the graph -/// -/// :param PyDiGraph graph: The graph to generate the layout for -/// :param list nlist: The list of lists of indexes which represents each shell -/// :param float rotate: Angle by which to rotate the starting position of each shell -/// relative to the starting position of the previous shell (in radians) -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// -/// :returns: The shell layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3( - text_signature = "(graph, /, nlist=None, rotate=None, scale=1, center=None)" -)] -pub fn digraph_shell_layout( - graph: &digraph::PyDiGraph, - nlist: Option>>, - rotate: Option, - scale: Option, - center: Option, -) -> Pos2DMapping { - layout::shell_layout(&graph.graph, nlist, rotate, scale, center) -} - -/// Generate a spiral layout of the graph -/// -/// :param PyGraph graph: The graph to generate the layout for -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// :param float resolution: The compactness of the spiral layout returned. -/// Lower values result in more compressed spiral layouts. -/// :param bool equidistant: If true, nodes will be plotted equidistant from -/// each other. -/// -/// :returns: The spiral layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, /, scale=1, center=None, resolution=0.35, - equidistant=False)")] -pub fn graph_spiral_layout( - graph: &graph::PyGraph, - scale: Option, - center: Option, - resolution: Option, - equidistant: Option, -) -> Pos2DMapping { - layout::spiral_layout(&graph.graph, scale, center, resolution, equidistant) -} - -/// Generate a spiral layout of the graph -/// -/// :param PyDiGraph graph: The graph to generate the layout for -/// :param float scale: An optional scaling factor to scale positions -/// :param tuple center: An optional center position. This is a 2 tuple of two -/// ``float`` values for the center position -/// :param float resolution: The compactness of the spiral layout returned. -/// Lower values result in more compressed spiral layouts. -/// :param bool equidistant: If true, nodes will be plotted equidistant from -/// each other. -/// -/// :returns: The spiral layout of the graph. -/// :rtype: Pos2DMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, /, scale=1, center=None, resolution=0.35, - equidistant=False)")] -pub fn digraph_spiral_layout( - graph: &digraph::PyDiGraph, - scale: Option, - center: Option, - resolution: Option, - equidistant: Option, -) -> Pos2DMapping { - layout::spiral_layout(&graph.graph, scale, center, resolution, equidistant) -} - -fn _num_shortest_paths_unweighted( - graph: &StableGraph, - source: usize, -) -> PyResult> { - let mut out_map: Vec = - vec![0.to_biguint().unwrap(); graph.node_bound()]; - let node_index = NodeIndex::new(source); - if graph.node_weight(node_index).is_none() { - return Err(PyIndexError::new_err(format!( - "No node found for index {}", - source - ))); - } - let mut bfs = Bfs::new(&graph, node_index); - let mut distance: Vec> = vec![None; graph.node_bound()]; - distance[node_index.index()] = Some(0); - out_map[source] = 1.to_biguint().unwrap(); - while let Some(current) = bfs.next(graph) { - let dist_plus_one = distance[current.index()].unwrap_or_default() + 1; - let count_current = out_map[current.index()].clone(); - for neighbor_index in - graph.neighbors_directed(current, petgraph::Direction::Outgoing) - { - let neighbor: usize = neighbor_index.index(); - if distance[neighbor].is_none() { - distance[neighbor] = Some(dist_plus_one); - out_map[neighbor] = count_current.clone(); - } else if distance[neighbor] == Some(dist_plus_one) { - out_map[neighbor] += &count_current; - } - } - } - - // Do not count paths to source in output - distance[source] = None; - out_map[source] = 0.to_biguint().unwrap(); - - // Return only nodes that are reachable in the graph - Ok(out_map - .into_iter() - .zip(distance.iter()) - .enumerate() - .filter_map(|(index, (count, dist))| { - if dist.is_some() { - Some((index, count)) - } else { - None - } - }) - .collect()) -} - -/// Get the number of unweighted shortest paths from a source node -/// -/// :param PyDiGraph graph: The graph to find the number of shortest paths on -/// :param int source: The source node to find the shortest paths from -/// -/// :returns: A mapping of target node indices to the number of shortest paths -/// from ``source`` to that node. If there is no path from ``source`` to -/// a node in the graph that node will not be preset in the output mapping. -/// :rtype: NodesCountMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, source, /)")] -pub fn digraph_num_shortest_paths_unweighted( - graph: &digraph::PyDiGraph, - source: usize, -) -> PyResult { - Ok(NodesCountMapping { - map: _num_shortest_paths_unweighted(&graph.graph, source)?, - }) -} - -/// Get the number of unweighted shortest paths from a source node -/// -/// :param PyGraph graph: The graph to find the number of shortest paths on -/// :param int source: The source node to find the shortest paths from -/// -/// :returns: A mapping of target node indices to the number of shortest paths -/// from ``source`` to that node. If there is no path from ``source`` to -/// a node in the graph that node will not be preset in the output mapping. -/// :rtype: NumPathsMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, source, /)")] -pub fn graph_num_shortest_paths_unweighted( - graph: &graph::PyGraph, - source: usize, -) -> PyResult { - Ok(NodesCountMapping { - map: _num_shortest_paths_unweighted(&graph.graph, source)?, - }) } // The provided node is invalid. diff --git a/src/max_weight_matching.rs b/src/matching/max_weight_matching.rs similarity index 100% rename from src/max_weight_matching.rs rename to src/matching/max_weight_matching.rs diff --git a/src/matching/mod.rs b/src/matching/mod.rs new file mode 100644 index 0000000000..edb29ebceb --- /dev/null +++ b/src/matching/mod.rs @@ -0,0 +1,185 @@ +// 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. + +#![allow(clippy::float_cmp)] + +mod max_weight_matching; +use crate::graph; + +use hashbrown::HashSet; + +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::visit::IntoEdgeReferences; + +/// Compute a maximum-weighted matching for a :class:`~retworkx.PyGraph` +/// +/// A matching is a subset of edges in which no node occurs more than once. +/// The weight of a matching is the sum of the weights of its edges. +/// A maximal matching cannot add more edges and still be a matching. +/// The cardinality of a matching is the number of matched edges. +/// +/// This function takes time :math:`O(n^3)` where ``n`` is the number of nodes +/// in the graph. +/// +/// This method is based on the "blossom" method for finding augmenting +/// paths and the "primal-dual" method for finding a matching of maximum +/// weight, both methods invented by Jack Edmonds [1]_. +/// +/// :param PyGraph graph: The undirected graph to compute the max weight +/// matching for. Expects to have no parallel edges (multigraphs are +/// untested currently). +/// :param bool max_cardinality: If True, compute the maximum-cardinality +/// matching with maximum weight among all maximum-cardinality matchings. +/// Defaults False. +/// :param callable weight_fn: An optional callable that will be passed a +/// single argument the edge object for each edge in the graph. It is +/// expected to return an ``int`` weight for that edge. For example, +/// if the weights are all integers you can use: ``lambda x: x``. If not +/// specified the value for ``default_weight`` will be used for all +/// edge weights. +/// :param int default_weight: The ``int`` value to use for all edge weights +/// in the graph if ``weight_fn`` is not specified. Defaults to ``1``. +/// :param bool verify_optimum: A boolean flag to run a check that the found +/// solution is optimum. If set to true an exception will be raised if +/// the found solution is not optimum. This is mostly useful for testing. +/// +/// :returns: A set of tuples ofthe matching, Note that only a single +/// direction will be listed in the output, for example: +/// ``{(0, 1),}``. +/// :rtype: set +/// +/// .. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs", +/// Zvi Galil, ACM Computing Surveys, 1986. +/// +#[pyfunction( + max_cardinality = "false", + default_weight = 1, + verify_optimum = "false" +)] +#[pyo3( + text_signature = "(graph, /, max_cardinality=False, weight_fn=None, default_weight=1, verify_optimum=False)" +)] +pub fn max_weight_matching( + py: Python, + graph: &graph::PyGraph, + max_cardinality: bool, + weight_fn: Option, + default_weight: i128, + verify_optimum: bool, +) -> PyResult> { + max_weight_matching::max_weight_matching( + py, + graph, + max_cardinality, + weight_fn, + default_weight, + verify_optimum, + ) +} + +fn _inner_is_matching( + graph: &graph::PyGraph, + matching: &HashSet<(usize, usize)>, +) -> bool { + let has_edge = |e: &(usize, usize)| -> bool { + graph + .graph + .contains_edge(NodeIndex::new(e.0), NodeIndex::new(e.1)) + }; + + if !matching.iter().all(|e| has_edge(e)) { + return false; + } + let mut found: HashSet = HashSet::with_capacity(2 * matching.len()); + for (v1, v2) in matching { + if found.contains(v1) || found.contains(v2) { + return false; + } + found.insert(*v1); + found.insert(*v2); + } + true +} + +/// Check if matching is valid for graph +/// +/// A *matching* in a graph is a set of edges in which no two distinct +/// edges share a common endpoint. +/// +/// :param PyDiGraph graph: The graph to check if the matching is valid for +/// :param set matching: A set of node index tuples for each edge in the +/// matching. +/// +/// :returns: Whether the provided matching is a valid matching for the graph +/// :rtype: bool +#[pyfunction] +#[pyo3(text_signature = "(graph, matching, /)")] +pub fn is_matching( + graph: &graph::PyGraph, + matching: HashSet<(usize, usize)>, +) -> bool { + _inner_is_matching(graph, &matching) +} + +/// Check if a matching is a maximal (**not** maximum) matching for a graph +/// +/// A *maximal matching* in a graph is a matching in which adding any +/// edge would cause the set to no longer be a valid matching. +/// +/// .. note:: +/// +/// This is not checking for a *maximum* (globally optimal) matching, but +/// a *maximal* (locally optimal) matching. +/// +/// :param PyDiGraph graph: The graph to check if the matching is maximal for. +/// :param set matching: A set of node index tuples for each edge in the +/// matching. +/// +/// :returns: Whether the provided matching is a valid matching and whether it +/// is maximal or not. +/// :rtype: bool +#[pyfunction] +#[pyo3(text_signature = "(graph, matching, /)")] +pub fn is_maximal_matching( + graph: &graph::PyGraph, + matching: HashSet<(usize, usize)>, +) -> bool { + if !_inner_is_matching(graph, &matching) { + return false; + } + let edge_list: HashSet<[usize; 2]> = graph + .edge_references() + .map(|edge| { + let mut tmp_array = [edge.source().index(), edge.target().index()]; + tmp_array.sort_unstable(); + tmp_array + }) + .collect(); + let matched_edges: HashSet<[usize; 2]> = matching + .iter() + .map(|edge| { + let mut tmp_array = [edge.0, edge.1]; + tmp_array.sort_unstable(); + tmp_array + }) + .collect(); + let mut unmatched_edges = edge_list.difference(&matched_edges); + unmatched_edges.all(|e| { + let mut tmp_set = matching.clone(); + tmp_set.insert((e[0], e[1])); + !_inner_is_matching(graph, &tmp_set) + }) +} diff --git a/src/random_graph.rs b/src/random_graph.rs new file mode 100644 index 0000000000..c04a576f33 --- /dev/null +++ b/src/random_graph.rs @@ -0,0 +1,494 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use crate::{digraph, graph}; + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::Python; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; + +use rand::distributions::{Distribution, Uniform}; +use rand::prelude::*; +use rand_pcg::Pcg64; + +/// Return a :math:`G_{np}` directed random graph, also known as an +/// Erdős-Rényi graph or a binomial graph. +/// +/// For number of nodes :math:`n` and probability :math:`p`, the :math:`G_{n,p}` +/// graph algorithm creates :math:`n` nodes, and for all the :math:`n (n - 1)` possible edges, +/// each edge is created independently with probability :math:`p`. +/// In general, for any probability :math:`p`, the expected number of edges returned +/// is :math:`m = p n (n - 1)`. If :math:`p = 0` or :math:`p = 1`, the returned +/// graph is not random and will always be an empty or a complete graph respectively. +/// An empty graph has zero edges and a complete directed graph has :math:`n (n - 1)` edges. +/// The run time is :math:`O(n + m)` where :math:`m` is the expected number of edges mentioned above. +/// When :math:`p = 0`, run time always reduces to :math:`O(n)`, as the lower bound. +/// When :math:`p = 1`, run time always goes to :math:`O(n + n (n - 1))`, as the upper bound. +/// For other probabilities, this algorithm [1]_ runs in :math:`O(n + m)` time. +/// +/// For :math:`0 < p < 1`, the algorithm is based on the implementation of the networkx function +/// ``fast_gnp_random_graph`` [2]_ +/// +/// :param int num_nodes: The number of nodes to create in the graph +/// :param float probability: The probability of creating an edge between two nodes +/// :param int seed: An optional seed to use for the random number generator +/// +/// :return: A PyDiGraph object +/// :rtype: PyDiGraph +/// +/// .. [1] Vladimir Batagelj and Ulrik Brandes, +/// "Efficient generation of large random networks", +/// Phys. Rev. E, 71, 036113, 2005. +/// .. [2] https://github.com/networkx/networkx/blob/networkx-2.4/networkx/generators/random_graphs.py#L49-L120 +#[pyfunction] +#[pyo3(text_signature = "(num_nodes, probability, seed=None, /)")] +pub fn directed_gnp_random_graph( + py: Python, + num_nodes: isize, + probability: f64, + seed: Option, +) -> PyResult { + if num_nodes <= 0 { + return Err(PyValueError::new_err("num_nodes must be > 0")); + } + let mut rng: Pcg64 = match seed { + Some(seed) => Pcg64::seed_from_u64(seed), + None => Pcg64::from_entropy(), + }; + let mut inner_graph = StableDiGraph::::new(); + for x in 0..num_nodes { + inner_graph.add_node(x.to_object(py)); + } + if !(0.0..=1.0).contains(&probability) { + return Err(PyValueError::new_err( + "Probability out of range, must be 0 <= p <= 1", + )); + } + if probability > 0.0 { + if (probability - 1.0).abs() < std::f64::EPSILON { + for u in 0..num_nodes { + for v in 0..num_nodes { + if u != v { + // exclude self-loops + let u_index = NodeIndex::new(u as usize); + let v_index = NodeIndex::new(v as usize); + inner_graph.add_edge(u_index, v_index, py.None()); + } + } + } + } else { + let mut v: isize = 0; + let mut w: isize = -1; + let lp: f64 = (1.0 - probability).ln(); + + let between = Uniform::new(0.0, 1.0); + while v < num_nodes { + let random: f64 = between.sample(&mut rng); + let lr: f64 = (1.0 - random).ln(); + let ratio: isize = (lr / lp) as isize; + w = w + 1 + ratio; + // avoid self loops + if v == w { + w += 1; + } + while v < num_nodes && num_nodes <= w { + w -= v; + v += 1; + // avoid self loops + if v == w { + w -= v; + v += 1; + } + } + if v < num_nodes { + let v_index = NodeIndex::new(v as usize); + let w_index = NodeIndex::new(w as usize); + inner_graph.add_edge(v_index, w_index, py.None()); + } + } + } + } + + let graph = digraph::PyDiGraph { + graph: inner_graph, + cycle_state: algo::DfsSpace::default(), + check_cycle: false, + node_removed: false, + multigraph: true, + }; + Ok(graph) +} + +/// Return a :math:`G_{np}` random undirected graph, also known as an +/// Erdős-Rényi graph or a binomial graph. +/// +/// For number of nodes :math:`n` and probability :math:`p`, the :math:`G_{n,p}` +/// graph algorithm creates :math:`n` nodes, and for all the :math:`n (n - 1)/2` possible edges, +/// each edge is created independently with probability :math:`p`. +/// In general, for any probability :math:`p`, the expected number of edges returned +/// is :math:`m = p n (n - 1)/2`. If :math:`p = 0` or :math:`p = 1`, the returned +/// graph is not random and will always be an empty or a complete graph respectively. +/// An empty graph has zero edges and a complete undirected graph has :math:`n (n - 1)/2` edges. +/// The run time is :math:`O(n + m)` where :math:`m` is the expected number of edges mentioned above. +/// When :math:`p = 0`, run time always reduces to :math:`O(n)`, as the lower bound. +/// When :math:`p = 1`, run time always goes to :math:`O(n + n (n - 1)/2)`, as the upper bound. +/// For other probabilities, this algorithm [1]_ runs in :math:`O(n + m)` time. +/// +/// For :math:`0 < p < 1`, the algorithm is based on the implementation of the networkx function +/// ``fast_gnp_random_graph`` [2]_ +/// +/// :param int num_nodes: The number of nodes to create in the graph +/// :param float probability: The probability of creating an edge between two nodes +/// :param int seed: An optional seed to use for the random number generator +/// +/// :return: A PyGraph object +/// :rtype: PyGraph +/// +/// .. [1] Vladimir Batagelj and Ulrik Brandes, +/// "Efficient generation of large random networks", +/// Phys. Rev. E, 71, 036113, 2005. +/// .. [2] https://github.com/networkx/networkx/blob/networkx-2.4/networkx/generators/random_graphs.py#L49-L120 +#[pyfunction] +#[pyo3(text_signature = "(num_nodes, probability, seed=None, /)")] +pub fn undirected_gnp_random_graph( + py: Python, + num_nodes: isize, + probability: f64, + seed: Option, +) -> PyResult { + if num_nodes <= 0 { + return Err(PyValueError::new_err("num_nodes must be > 0")); + } + let mut rng: Pcg64 = match seed { + Some(seed) => Pcg64::seed_from_u64(seed), + None => Pcg64::from_entropy(), + }; + let mut inner_graph = StableUnGraph::::default(); + for x in 0..num_nodes { + inner_graph.add_node(x.to_object(py)); + } + if !(0.0..=1.0).contains(&probability) { + return Err(PyValueError::new_err( + "Probability out of range, must be 0 <= p <= 1", + )); + } + if probability > 0.0 { + if (probability - 1.0).abs() < std::f64::EPSILON { + for u in 0..num_nodes { + for v in u + 1..num_nodes { + let u_index = NodeIndex::new(u as usize); + let v_index = NodeIndex::new(v as usize); + inner_graph.add_edge(u_index, v_index, py.None()); + } + } + } else { + let mut v: isize = 1; + let mut w: isize = -1; + let lp: f64 = (1.0 - probability).ln(); + + let between = Uniform::new(0.0, 1.0); + while v < num_nodes { + let random: f64 = between.sample(&mut rng); + let lr = (1.0 - random).ln(); + let ratio: isize = (lr / lp) as isize; + w = w + 1 + ratio; + while w >= v && v < num_nodes { + w -= v; + v += 1; + } + if v < num_nodes { + let v_index = NodeIndex::new(v as usize); + let w_index = NodeIndex::new(w as usize); + inner_graph.add_edge(v_index, w_index, py.None()); + } + } + } + } + + let graph = graph::PyGraph { + graph: inner_graph, + node_removed: false, + multigraph: true, + }; + Ok(graph) +} + +/// Return a :math:`G_{nm}` of a directed graph +/// +/// Generates a random directed graph out of all the possible graphs with :math:`n` nodes and +/// :math:`m` edges. The generated graph will not be a multigraph and will not have self loops. +/// +/// For :math:`n` nodes, the maximum edges that can be returned is :math:`n (n - 1)`. +/// Passing :math:`m` higher than that will still return the maximum number of edges. +/// If :math:`m = 0`, the returned graph will always be empty (no edges). +/// When a seed is provided, the results are reproducible. Passing a seed when :math:`m = 0` +/// or :math:`m >= n (n - 1)` has no effect, as the result will always be an empty or a complete graph respectively. +/// +/// This algorithm has a time complexity of :math:`O(n + m)` +/// +/// :param int num_nodes: The number of nodes to create in the graph +/// :param int num_edges: The number of edges to create in the graph +/// :param int seed: An optional seed to use for the random number generator +/// +/// :return: A PyDiGraph object +/// :rtype: PyDiGraph +/// +#[pyfunction] +#[pyo3(text_signature = "(num_nodes, num_edges, seed=None, /)")] +pub fn directed_gnm_random_graph( + py: Python, + num_nodes: isize, + num_edges: isize, + seed: Option, +) -> PyResult { + if num_nodes <= 0 { + return Err(PyValueError::new_err("num_nodes must be > 0")); + } + if num_edges < 0 { + return Err(PyValueError::new_err("num_edges must be >= 0")); + } + let mut rng: Pcg64 = match seed { + Some(seed) => Pcg64::seed_from_u64(seed), + None => Pcg64::from_entropy(), + }; + let mut inner_graph = StableDiGraph::::new(); + for x in 0..num_nodes { + inner_graph.add_node(x.to_object(py)); + } + // if number of edges to be created is >= max, + // avoid randomly missed trials and directly add edges between every node + if num_edges >= num_nodes * (num_nodes - 1) { + for u in 0..num_nodes { + for v in 0..num_nodes { + // avoid self-loops + if u != v { + let u_index = NodeIndex::new(u as usize); + let v_index = NodeIndex::new(v as usize); + inner_graph.add_edge(u_index, v_index, py.None()); + } + } + } + } else { + let mut created_edges: isize = 0; + let between = Uniform::new(0, num_nodes); + while created_edges < num_edges { + let u = between.sample(&mut rng); + let v = between.sample(&mut rng); + let u_index = NodeIndex::new(u as usize); + let v_index = NodeIndex::new(v as usize); + // avoid self-loops and multi-graphs + if u != v && inner_graph.find_edge(u_index, v_index).is_none() { + inner_graph.add_edge(u_index, v_index, py.None()); + created_edges += 1; + } + } + } + let graph = digraph::PyDiGraph { + graph: inner_graph, + cycle_state: algo::DfsSpace::default(), + check_cycle: false, + node_removed: false, + multigraph: true, + }; + Ok(graph) +} + +/// Return a :math:`G_{nm}` of an undirected graph +/// +/// Generates a random undirected graph out of all the possible graphs with :math:`n` nodes and +/// :math:`m` edges. The generated graph will not be a multigraph and will not have self loops. +/// +/// For :math:`n` nodes, the maximum edges that can be returned is :math:`n (n - 1)/2`. +/// Passing :math:`m` higher than that will still return the maximum number of edges. +/// If :math:`m = 0`, the returned graph will always be empty (no edges). +/// When a seed is provided, the results are reproducible. Passing a seed when :math:`m = 0` +/// or :math:`m >= n (n - 1)/2` has no effect, as the result will always be an empty or a complete graph respectively. +/// +/// This algorithm has a time complexity of :math:`O(n + m)` +/// +/// :param int num_nodes: The number of nodes to create in the graph +/// :param int num_edges: The number of edges to create in the graph +/// :param int seed: An optional seed to use for the random number generator +/// +/// :return: A PyGraph object +/// :rtype: PyGraph + +#[pyfunction] +#[pyo3(text_signature = "(num_nodes, probability, seed=None, /)")] +pub fn undirected_gnm_random_graph( + py: Python, + num_nodes: isize, + num_edges: isize, + seed: Option, +) -> PyResult { + if num_nodes <= 0 { + return Err(PyValueError::new_err("num_nodes must be > 0")); + } + if num_edges < 0 { + return Err(PyValueError::new_err("num_edges must be >= 0")); + } + let mut rng: Pcg64 = match seed { + Some(seed) => Pcg64::seed_from_u64(seed), + None => Pcg64::from_entropy(), + }; + let mut inner_graph = StableUnGraph::::default(); + for x in 0..num_nodes { + inner_graph.add_node(x.to_object(py)); + } + // if number of edges to be created is >= max, + // avoid randomly missed trials and directly add edges between every node + if num_edges >= num_nodes * (num_nodes - 1) / 2 { + for u in 0..num_nodes { + for v in u + 1..num_nodes { + let u_index = NodeIndex::new(u as usize); + let v_index = NodeIndex::new(v as usize); + inner_graph.add_edge(u_index, v_index, py.None()); + } + } + } else { + let mut created_edges: isize = 0; + let between = Uniform::new(0, num_nodes); + while created_edges < num_edges { + let u = between.sample(&mut rng); + let v = between.sample(&mut rng); + let u_index = NodeIndex::new(u as usize); + let v_index = NodeIndex::new(v as usize); + // avoid self-loops and multi-graphs + if u != v && inner_graph.find_edge(u_index, v_index).is_none() { + inner_graph.add_edge(u_index, v_index, py.None()); + created_edges += 1; + } + } + } + let graph = graph::PyGraph { + graph: inner_graph, + node_removed: false, + multigraph: true, + }; + Ok(graph) +} + +#[inline] +fn pnorm(x: f64, p: f64) -> f64 { + if p == 1.0 || p == std::f64::INFINITY { + x.abs() + } else if p == 2.0 { + x * x + } else { + x.abs().powf(p) + } +} + +fn distance(x: &[f64], y: &[f64], p: f64) -> f64 { + let it = x.iter().zip(y.iter()).map(|(xi, yi)| pnorm(xi - yi, p)); + + if p == std::f64::INFINITY { + it.fold(-1.0, |max, x| if x > max { x } else { max }) + } else { + it.sum() + } +} + +/// Returns a random geometric graph in the unit cube of dimensions `dim`. +/// +/// The random geometric graph model places `num_nodes` nodes uniformly at +/// random in the unit cube. Two nodes are joined by an edge if the +/// distance between the nodes is at most `radius`. +/// +/// Each node has a node attribute ``'pos'`` that stores the +/// position of that node in Euclidean space as provided by the +/// ``pos`` keyword argument or, if ``pos`` was not provided, as +/// generated by this function. +/// +/// :param int num_nodes: The number of nodes to create in the graph +/// :param float radius: Distance threshold value +/// :param int dim: Dimension of node positions. Default: 2 +/// :param list pos: Optional list with node positions as values +/// :param float p: Which Minkowski distance metric to use. `p` has to meet the condition +/// ``1 <= p <= infinity``. +/// If this argument is not specified, the :math:`L^2` metric +/// (the Euclidean distance metric), p = 2 is used. +/// :param int seed: An optional seed to use for the random number generator +/// +/// :return: A PyGraph object +/// :rtype: PyGraph +#[pyfunction(dim = "2", p = "2.0")] +#[pyo3( + text_signature = "(num_nodes, radius, /, dim=2, pos=None, p=2.0, seed=None)" +)] +pub fn random_geometric_graph( + py: Python, + num_nodes: usize, + radius: f64, + dim: usize, + pos: Option>>, + p: f64, + seed: Option, +) -> PyResult { + if num_nodes == 0 { + return Err(PyValueError::new_err("num_nodes must be > 0")); + } + + let mut inner_graph = StableUnGraph::::default(); + + let radius_p = pnorm(radius, p); + let mut rng: Pcg64 = match seed { + Some(seed) => Pcg64::seed_from_u64(seed), + None => Pcg64::from_entropy(), + }; + + let dist = Uniform::new(0.0, 1.0); + let pos = pos.unwrap_or_else(|| { + (0..num_nodes) + .map(|_| (0..dim).map(|_| dist.sample(&mut rng)).collect()) + .collect() + }); + + if num_nodes != pos.len() { + return Err(PyValueError::new_err( + "number of elements in pos and num_nodes must be equal", + )); + } + + for pval in pos.iter() { + let pos_dict = PyDict::new(py); + pos_dict.set_item("pos", pval.to_object(py))?; + + inner_graph.add_node(pos_dict.into()); + } + + for u in 0..(num_nodes - 1) { + for v in (u + 1)..num_nodes { + if distance(&pos[u], &pos[v], p) < radius_p { + inner_graph.add_edge( + NodeIndex::new(u), + NodeIndex::new(v), + py.None(), + ); + } + } + } + + let graph = graph::PyGraph { + graph: inner_graph, + node_removed: false, + multigraph: true, + }; + Ok(graph) +} diff --git a/src/shortest_path/all_pairs_dijkstra.rs b/src/shortest_path/all_pairs_dijkstra.rs new file mode 100644 index 0000000000..9a255ee1c8 --- /dev/null +++ b/src/shortest_path/all_pairs_dijkstra.rs @@ -0,0 +1,215 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use hashbrown::HashMap; + +use super::dijkstra; + +use std::sync::RwLock; + +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::visit::EdgeIndexable; +use petgraph::EdgeType; + +use rayon::prelude::*; + +use crate::iterators::{ + AllPairsPathLengthMapping, AllPairsPathMapping, PathLengthMapping, + PathMapping, +}; + +pub fn all_pairs_dijkstra_path_lengths( + py: Python, + graph: &StableGraph, + edge_cost_fn: PyObject, +) -> PyResult { + if graph.node_count() == 0 { + return Ok(AllPairsPathLengthMapping { + path_lengths: HashMap::new(), + }); + } else if graph.edge_count() == 0 { + return Ok(AllPairsPathLengthMapping { + path_lengths: graph + .node_indices() + .map(|i| { + ( + i.index(), + PathLengthMapping { + path_lengths: HashMap::new(), + }, + ) + }) + .collect(), + }); + } + let edge_cost_callable = |a: &PyObject| -> PyResult { + let res = edge_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + raw.extract(py) + }; + let mut edge_weights: Vec> = + Vec::with_capacity(graph.edge_bound()); + for index in 0..=graph.edge_bound() { + let raw_weight = graph.edge_weight(EdgeIndex::new(index)); + match raw_weight { + Some(weight) => { + edge_weights.push(Some(edge_cost_callable(weight)?)) + } + None => edge_weights.push(None), + }; + } + let edge_cost = |e: EdgeIndex| -> PyResult { + match edge_weights[e.index()] { + Some(weight) => Ok(weight), + None => Err(PyIndexError::new_err("No edge found for index")), + } + }; + let node_indices: Vec = graph.node_indices().collect(); + let out_map: HashMap = node_indices + .into_par_iter() + .map(|x| { + let out_map = PathLengthMapping { + path_lengths: dijkstra::dijkstra( + graph, + x, + None, + |e| edge_cost(e.id()), + None, + ) + .unwrap() + .iter() + .filter_map(|(index, cost)| { + if *index == x { + None + } else { + Some((index.index(), *cost)) + } + }) + .collect(), + }; + (x.index(), out_map) + }) + .collect(); + Ok(AllPairsPathLengthMapping { + path_lengths: out_map, + }) +} + +pub fn all_pairs_dijkstra_shortest_paths( + py: Python, + graph: &StableGraph, + edge_cost_fn: PyObject, + distances: Option<&mut HashMap>>, +) -> PyResult { + if graph.node_count() == 0 { + return Ok(AllPairsPathMapping { + paths: HashMap::new(), + }); + } else if graph.edge_count() == 0 { + return Ok(AllPairsPathMapping { + paths: graph + .node_indices() + .map(|i| { + ( + i.index(), + PathMapping { + paths: HashMap::new(), + }, + ) + }) + .collect(), + }); + } + let edge_cost_callable = |a: &PyObject| -> PyResult { + let res = edge_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + raw.extract(py) + }; + let mut edge_weights: Vec> = + Vec::with_capacity(graph.edge_bound()); + for index in 0..=graph.edge_bound() { + let raw_weight = graph.edge_weight(EdgeIndex::new(index)); + match raw_weight { + Some(weight) => { + edge_weights.push(Some(edge_cost_callable(weight)?)) + } + None => edge_weights.push(None), + }; + } + let edge_cost = |e: EdgeIndex| -> PyResult { + match edge_weights[e.index()] { + Some(weight) => Ok(weight), + None => Err(PyIndexError::new_err("No edge found for index")), + } + }; + let node_indices: Vec = graph.node_indices().collect(); + let temp_distances: RwLock>> = + if distances.is_some() { + RwLock::new(HashMap::with_capacity(graph.node_count())) + } else { + // Avoid extra allocation if HashMap isn't used + RwLock::new(HashMap::new()) + }; + let out_map = AllPairsPathMapping { + paths: node_indices + .into_par_iter() + .map(|x| { + let mut paths: HashMap> = + HashMap::with_capacity(graph.node_count()); + let distance = dijkstra::dijkstra( + graph, + x, + None, + |e| edge_cost(e.id()), + Some(&mut paths), + ) + .unwrap(); + if distances.is_some() { + temp_distances.write().unwrap().insert(x.index(), distance); + } + let index = x.index(); + let out_paths = PathMapping { + paths: paths + .iter() + .filter_map(|path_mapping| { + let path_index = path_mapping.0.index(); + if index != path_index { + Some(( + path_index, + path_mapping + .1 + .iter() + .map(|x| x.index()) + .collect(), + )) + } else { + None + } + }) + .collect(), + }; + (index, out_paths) + }) + .collect(), + }; + if let Some(x) = distances { + *x = temp_distances.read().unwrap().clone() + }; + Ok(out_map) +} diff --git a/src/astar.rs b/src/shortest_path/astar.rs similarity index 100% rename from src/astar.rs rename to src/shortest_path/astar.rs diff --git a/src/dijkstra.rs b/src/shortest_path/dijkstra.rs similarity index 99% rename from src/dijkstra.rs rename to src/shortest_path/dijkstra.rs index 3271ee8c35..f901b22826 100644 --- a/src/dijkstra.rs +++ b/src/shortest_path/dijkstra.rs @@ -28,7 +28,7 @@ use petgraph::visit::{EdgeRef, IntoEdges, VisitMap, Visitable}; use pyo3::prelude::*; -use crate::astar::MinScored; +use super::astar::MinScored; /// \[Generic\] Dijkstra's shortest path algorithm. /// diff --git a/src/shortest_path/floyd_warshall.rs b/src/shortest_path/floyd_warshall.rs new file mode 100644 index 0000000000..d5d57054f4 --- /dev/null +++ b/src/shortest_path/floyd_warshall.rs @@ -0,0 +1,146 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use hashbrown::HashMap; + +use super::weight_callable; + +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::prelude::*; +use petgraph::visit::{IntoEdgeReferences, NodeIndexable}; +use petgraph::EdgeType; + +use rayon::prelude::*; + +use crate::iterators::{AllPairsPathLengthMapping, PathLengthMapping}; + +pub fn floyd_warshall( + py: Python, + graph: &StableGraph, + weight_fn: Option, + as_undirected: bool, + default_weight: f64, + parallel_threshold: usize, +) -> PyResult { + if graph.node_count() == 0 { + return Ok(AllPairsPathLengthMapping { + path_lengths: HashMap::new(), + }); + } else if graph.edge_count() == 0 { + return Ok(AllPairsPathLengthMapping { + path_lengths: graph + .node_indices() + .map(|i| { + ( + i.index(), + PathLengthMapping { + path_lengths: HashMap::new(), + }, + ) + }) + .collect(), + }); + } + let n = graph.node_bound(); + + // Allocate empty matrix + let mut mat: Vec> = vec![HashMap::new(); n]; + + // Set diagonal to 0 + for i in 0..n { + if let Some(row_i) = mat.get_mut(i) { + row_i.entry(i).or_insert(0.0); + } + } + + // Utility to set row_i[j] = min(row_i[j], m_ij) + macro_rules! insert_or_minimize { + ($row_i: expr, $j: expr, $m_ij: expr) => {{ + $row_i + .entry($j) + .and_modify(|e| { + if $m_ij < *e { + *e = $m_ij; + } + }) + .or_insert($m_ij); + }}; + } + + // Build adjacency matrix + for edge in graph.edge_references() { + let i = NodeIndexable::to_index(&graph, edge.source()); + let j = NodeIndexable::to_index(&graph, edge.target()); + let weight = edge.weight().clone(); + + let edge_weight = + weight_callable(py, &weight_fn, &weight, default_weight)?; + if let Some(row_i) = mat.get_mut(i) { + insert_or_minimize!(row_i, j, edge_weight); + } + if as_undirected { + if let Some(row_j) = mat.get_mut(j) { + insert_or_minimize!(row_j, i, edge_weight); + } + } + } + + // Perform the Floyd-Warshall algorithm. + // In each loop, this finds the shortest path from point i + // to point j using intermediate nodes 0..k + if n < parallel_threshold { + for k in 0..n { + let row_k = mat.get(k).cloned().unwrap_or_default(); + mat.iter_mut().for_each(|row_i| { + if let Some(m_ik) = row_i.get(&k).cloned() { + for (j, m_kj) in row_k.iter() { + let m_ikj = m_ik + *m_kj; + insert_or_minimize!(row_i, *j, m_ikj); + } + } + }) + } + } else { + for k in 0..n { + let row_k = mat.get(k).cloned().unwrap_or_default(); + mat.par_iter_mut().for_each(|row_i| { + if let Some(m_ik) = row_i.get(&k).cloned() { + for (j, m_kj) in row_k.iter() { + let m_ikj = m_ik + *m_kj; + insert_or_minimize!(row_i, *j, m_ikj); + } + } + }) + } + } + + // Convert to return format + let out_map: HashMap = graph + .node_indices() + .map(|i| { + let out_map = PathLengthMapping { + path_lengths: mat[i.index()] + .iter() + .map(|(k, v)| (*k, *v)) + .collect(), + }; + (i.index(), out_map) + }) + .collect(); + Ok(AllPairsPathLengthMapping { + path_lengths: out_map, + }) +} diff --git a/src/k_shortest_path.rs b/src/shortest_path/k_shortest_path.rs similarity index 99% rename from src/k_shortest_path.rs rename to src/shortest_path/k_shortest_path.rs index 1367dd48ae..1cb595add7 100644 --- a/src/k_shortest_path.rs +++ b/src/shortest_path/k_shortest_path.rs @@ -28,7 +28,7 @@ use petgraph::visit::{ use pyo3::prelude::*; -use crate::astar::MinScored; +use super::astar::MinScored; /// k'th shortest path algorithm. /// diff --git a/src/shortest_path/mod.rs b/src/shortest_path/mod.rs new file mode 100644 index 0000000000..9e6f6437fa --- /dev/null +++ b/src/shortest_path/mod.rs @@ -0,0 +1,1247 @@ +// 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. + +#![allow(clippy::float_cmp)] + +pub mod all_pairs_dijkstra; +mod astar; +mod dijkstra; +mod floyd_warshall; +mod k_shortest_path; +mod num_shortest_path; + +use hashbrown::{HashMap, HashSet}; + +use super::weight_callable; +use crate::{digraph, get_edge_iter_with_weights, graph, NoPathFound}; + +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::graph::NodeIndex; +use petgraph::visit::NodeCount; + +use ndarray::prelude::*; +use numpy::IntoPyArray; +use rayon::prelude::*; + +use crate::iterators::{ + AllPairsPathLengthMapping, AllPairsPathMapping, NodeIndices, + NodesCountMapping, PathLengthMapping, PathMapping, +}; + +/// Find the shortest path from a node +/// +/// This function will generate the shortest path from a source node using +/// Dijkstra's algorithm. +/// +/// :param PyGraph graph: +/// :param int source: The node index to find paths from +/// :param int target: An optional target to find a path to +/// :param weight_fn: An optional weight function for an edge. It will accept +/// a single argument, the edge's weight object and will return a float which +/// will be used to represent the weight/cost of the edge +/// :param float default_weight: If ``weight_fn`` isn't specified this optional +/// float value will be used for the weight/cost of each edge. +/// :param bool as_undirected: If set to true the graph will be treated as +/// undirected for finding the shortest path. +/// +/// :return: Dictionary of paths. The keys are destination node indices and +/// the dict values are lists of node indices making the path. +/// :rtype: dict +#[pyfunction(default_weight = "1.0", as_undirected = "false")] +#[pyo3( + text_signature = "(graph, source, /, target=None weight_fn=None, default_weight=1.0)" +)] +pub fn graph_dijkstra_shortest_paths( + py: Python, + graph: &graph::PyGraph, + source: usize, + target: Option, + weight_fn: Option, + default_weight: f64, +) -> PyResult { + let start = NodeIndex::new(source); + let goal_index: Option = target.map(NodeIndex::new); + let mut paths: HashMap> = + HashMap::with_capacity(graph.node_count()); + dijkstra::dijkstra( + graph, + start, + goal_index, + |e| weight_callable(py, &weight_fn, e.weight(), default_weight), + Some(&mut paths), + )?; + + Ok(PathMapping { + paths: paths + .iter() + .filter_map(|(k, v)| { + let k_int = k.index(); + if k_int == source + || target.is_some() && target.unwrap() != k_int + { + None + } else { + Some(( + k.index(), + v.iter().map(|x| x.index()).collect::>(), + )) + } + }) + .collect(), + }) +} + +/// Find the shortest path from a node +/// +/// This function will generate the shortest path from a source node using +/// Dijkstra's algorithm. +/// +/// :param PyDiGraph graph: +/// :param int source: The node index to find paths from +/// :param int target: An optional target path to find the path +/// :param weight_fn: An optional weight function for an edge. It will accept +/// a single argument, the edge's weight object and will return a float which +/// will be used to represent the weight/cost of the edge +/// :param float default_weight: If ``weight_fn`` isn't specified this optional +/// float value will be used for the weight/cost of each edge. +/// :param bool as_undirected: If set to true the graph will be treated as +/// undirected for finding the shortest path. +/// +/// :return: Dictionary of paths. The keys are destination node indices and +/// the dict values are lists of node indices making the path. +/// :rtype: dict +#[pyfunction(default_weight = "1.0", as_undirected = "false")] +#[pyo3( + text_signature = "(graph, source, /, target=None weight_fn=None, default_weight=1.0, as_undirected=False)" +)] +pub fn digraph_dijkstra_shortest_paths( + py: Python, + graph: &digraph::PyDiGraph, + source: usize, + target: Option, + weight_fn: Option, + default_weight: f64, + as_undirected: bool, +) -> PyResult { + let start = NodeIndex::new(source); + let goal_index: Option = target.map(NodeIndex::new); + let mut paths: HashMap> = + HashMap::with_capacity(graph.node_count()); + if as_undirected { + dijkstra::dijkstra( + // TODO: Use petgraph undirected adapter after + // https://github.com/petgraph/petgraph/pull/318 is available in + // a petgraph release. + &graph.to_undirected(py, true, None)?, + start, + goal_index, + |e| weight_callable(py, &weight_fn, e.weight(), default_weight), + Some(&mut paths), + )?; + } else { + dijkstra::dijkstra( + graph, + start, + goal_index, + |e| weight_callable(py, &weight_fn, e.weight(), default_weight), + Some(&mut paths), + )?; + } + Ok(PathMapping { + paths: paths + .iter() + .filter_map(|(k, v)| { + let k_int = k.index(); + if k_int == source + || target.is_some() && target.unwrap() != k_int + { + None + } else { + Some(( + k_int, + v.iter().map(|x| x.index()).collect::>(), + )) + } + }) + .collect(), + }) +} + +/// Compute the lengths of the shortest paths for a PyGraph object using +/// Dijkstra's algorithm +/// +/// :param PyGraph graph: The input graph to use +/// :param int node: The node index to use as the source for finding the +/// shortest paths from +/// :param edge_cost_fn: A python callable that will take in 1 parameter, an +/// edge's data object and will return a float that represents the +/// cost/weight of that edge. It must be non-negative +/// :param int goal: An optional node index to use as the end of the path. +/// When specified the traversal will stop when the goal is reached and +/// the output dictionary will only have a single entry with the length +/// of the shortest path to the goal node. +/// +/// :returns: A dictionary of the shortest paths from the provided node where +/// the key is the node index of the end of the path and the value is the +/// cost/sum of the weights of path +/// :rtype: PathLengthMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, node, edge_cost_fn, /, goal=None)")] +pub fn graph_dijkstra_shortest_path_lengths( + py: Python, + graph: &graph::PyGraph, + node: usize, + edge_cost_fn: PyObject, + goal: Option, +) -> PyResult { + let edge_cost_callable = |a: &PyObject| -> PyResult { + let res = edge_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + raw.extract(py) + }; + + let start = NodeIndex::new(node); + let goal_index: Option = goal.map(NodeIndex::new); + + let res = dijkstra::dijkstra( + graph, + start, + goal_index, + |e| edge_cost_callable(e.weight()), + None, + )?; + Ok(PathLengthMapping { + path_lengths: res + .iter() + .filter_map(|(k, v)| { + let k_int = k.index(); + if k_int == node || goal.is_some() && goal.unwrap() != k_int { + None + } else { + Some((k_int, *v)) + } + }) + .collect(), + }) +} + +/// Compute the lengths of the shortest paths for a PyDiGraph object using +/// Dijkstra's algorithm +/// +/// :param PyDiGraph graph: The input graph to use +/// :param int node: The node index to use as the source for finding the +/// shortest paths from +/// :param edge_cost_fn: A python callable that will take in 1 parameter, an +/// edge's data object and will return a float that represents the +/// cost/weight of that edge. It must be non-negative +/// :param int goal: An optional node index to use as the end of the path. +/// When specified the traversal will stop when the goal is reached and +/// the output dictionary will only have a single entry with the length +/// of the shortest path to the goal node. +/// +/// :returns: A dictionary of the shortest paths from the provided node where +/// the key is the node index of the end of the path and the value is the +/// cost/sum of the weights of path +/// :rtype: PathLengthMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, node, edge_cost_fn, /, goal=None)")] +pub fn digraph_dijkstra_shortest_path_lengths( + py: Python, + graph: &digraph::PyDiGraph, + node: usize, + edge_cost_fn: PyObject, + goal: Option, +) -> PyResult { + let edge_cost_callable = |a: &PyObject| -> PyResult { + let res = edge_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + raw.extract(py) + }; + + let start = NodeIndex::new(node); + let goal_index: Option = goal.map(NodeIndex::new); + + let res = dijkstra::dijkstra( + graph, + start, + goal_index, + |e| edge_cost_callable(e.weight()), + None, + )?; + Ok(PathLengthMapping { + path_lengths: res + .iter() + .filter_map(|(k, v)| { + let k_int = k.index(); + if k_int == node || goal.is_some() && goal.unwrap() != k_int { + None + } else { + Some((k_int, *v)) + } + }) + .collect(), + }) +} + +/// Calculate the the shortest length from all nodes in a +/// :class:`~retworkx.PyDiGraph` object +/// +/// This function will generate the shortest path from a source node using +/// Dijkstra's algorithm. This function is multithreaded and will run +/// launch a thread pool with threads equal to the number of CPUs by default. +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads. +/// +/// :param graph: The input :class:`~retworkx.PyDiGraph` to use +/// :param edge_cost_fn: A callable object that acts as a weight function for +/// an edge. It will accept a single positional argument, the edge's weight +/// object and will return a float which will be used to represent the +/// weight/cost of the edge +/// +/// :return: A read-only dictionary of path lengths. The keys are source +/// node indices and the values are dicts of the target node and the length +/// of the shortest path to that node. For example:: +/// +/// { +/// 0: {1: 2.0, 2: 2.0}, +/// 1: {2: 1.0}, +/// 2: {0: 1.0}, +/// } +/// +/// :rtype: AllPairsPathLengthMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn digraph_all_pairs_dijkstra_path_lengths( + py: Python, + graph: &digraph::PyDiGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_dijkstra::all_pairs_dijkstra_path_lengths( + py, + &graph.graph, + edge_cost_fn, + ) +} + +/// Find the shortest path from all nodes in a :class:`~retworkx.PyDiGraph` +/// object +/// +/// This function will generate the shortest path from a source node using +/// Dijkstra's algorithm. This function is multithreaded and will run +/// launch a thread pool with threads equal to the number of CPUs by default. +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads. +/// +/// :param graph: The input :class:`~retworkx.PyDiGraph` object to use +/// :param edge_cost_fn: A callable object that acts as a weight function for +/// an edge. It will accept a single positional argument, the edge's weight +/// object and will return a float which will be used to represent the +/// weight/cost of the edge +/// +/// :return: A read-only dictionary of paths. The keys are source node indices +/// and the values are dicts of the target node and the list of the +/// node indices making up the shortest path to that node. For example:: +/// +/// { +/// 0: {1: [0, 1], 2: [0, 1, 2]}, +/// 1: {2: [1, 2]}, +/// 2: {0: [2, 0]}, +/// } +/// +/// :rtype: AllPairsPathMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn digraph_all_pairs_dijkstra_shortest_paths( + py: Python, + graph: &digraph::PyDiGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_dijkstra::all_pairs_dijkstra_shortest_paths( + py, + &graph.graph, + edge_cost_fn, + None, + ) +} + +/// Calculate the the shortest length from all nodes in a +/// :class:`~retworkx.PyGraph` object +/// +/// This function will generate the shortest path from a source node using +/// Dijkstra's algorithm. +/// +/// :param graph: The input :class:`~retworkx.PyGraph` to use +/// :param edge_cost_fn: A callable object that acts as a weight function for +/// an edge. It will accept a single positional argument, the edge's weight +/// object and will return a float which will be used to represent the +/// weight/cost of the edge +/// +/// :return: A read-only dictionary of path lengths. The keys are source +/// node indices and the values are dicts of the target node and the length +/// of the shortest path to that node. For example:: +/// +/// { +/// 0: {1: 2.0, 2: 2.0}, +/// 1: {2: 1.0}, +/// 2: {0: 1.0}, +/// } +/// +/// :rtype: AllPairsPathLengthMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn graph_all_pairs_dijkstra_path_lengths( + py: Python, + graph: &graph::PyGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_dijkstra::all_pairs_dijkstra_path_lengths( + py, + &graph.graph, + edge_cost_fn, + ) +} + +/// Find the shortest path from all nodes in a :class:`~retworkx.PyGraph` +/// object +/// +/// This function will generate the shortest path from a source node using +/// Dijkstra's algorithm. +/// +/// :param graph: The input :class:`~retworkx.PyGraph` object to use +/// :param edge_cost_fn: A callable object that acts as a weight function for +/// an edge. It will accept a single positional argument, the edge's weight +/// object and will return a float which will be used to represent the +/// weight/cost of the edge +/// +/// :return: A read-only dictionary of paths. The keys are destination node +/// indices and the values are dicts of the target node and the list of the +/// node indices making up the shortest path to that node. For example:: +/// +/// { +/// 0: {1: [0, 1], 2: [0, 1, 2]}, +/// 1: {2: [1, 2]}, +/// 2: {0: [2, 0]}, +/// } +/// +/// :rtype: AllPairsPathMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] +pub fn graph_all_pairs_dijkstra_shortest_paths( + py: Python, + graph: &graph::PyGraph, + edge_cost_fn: PyObject, +) -> PyResult { + all_pairs_dijkstra::all_pairs_dijkstra_shortest_paths( + py, + &graph.graph, + edge_cost_fn, + None, + ) +} + +/// Compute the A* shortest path for a PyDiGraph +/// +/// :param PyDiGraph graph: The input graph to use +/// :param int node: The node index to compute the path from +/// :param goal_fn: A python callable that will take in 1 parameter, a node's +/// data object and will return a boolean which will be True if it is the +/// finish node. +/// :param edge_cost_fn: A python callable that will take in 1 parameter, an +/// edge's data object and will return a float that represents the cost of +/// that edge. It must be non-negative. +/// :param estimate_cost_fn: A python callable that will take in 1 parameter, a +/// node's data object and will return a float which represents the +/// estimated cost for the next node. The return must be non-negative. For +/// the algorithm to find the actual shortest path, it should be +/// admissible, meaning that it should never overestimate the actual cost +/// to get to the nearest goal node. +/// +/// :return: The computed shortest path between node and finish as a list +/// of node indices. +/// :rtype: NodeIndices +#[pyfunction] +#[pyo3(text_signature = "(graph, node, goal_fn, edge_cost, estimate_cost, /)")] +fn digraph_astar_shortest_path( + py: Python, + graph: &digraph::PyDiGraph, + node: usize, + goal_fn: PyObject, + edge_cost_fn: PyObject, + estimate_cost_fn: PyObject, +) -> PyResult { + let goal_fn_callable = |a: &PyObject| -> PyResult { + let res = goal_fn.call1(py, (a,))?; + let raw = res.to_object(py); + let output: bool = raw.extract(py)?; + Ok(output) + }; + + let edge_cost_callable = |a: &PyObject| -> PyResult { + let res = edge_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + let output: f64 = raw.extract(py)?; + Ok(output) + }; + + let estimate_cost_callable = |a: &PyObject| -> PyResult { + let res = estimate_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + let output: f64 = raw.extract(py)?; + Ok(output) + }; + let start = NodeIndex::new(node); + + let astar_res = astar::astar( + graph, + start, + |f| goal_fn_callable(graph.graph.node_weight(f).unwrap()), + |e| edge_cost_callable(e.weight()), + |estimate| { + estimate_cost_callable(graph.graph.node_weight(estimate).unwrap()) + }, + )?; + let path = match astar_res { + Some(path) => path, + None => { + return Err(NoPathFound::new_err( + "No path found that satisfies goal_fn", + )) + } + }; + Ok(NodeIndices { + nodes: path.1.into_iter().map(|x| x.index()).collect(), + }) +} + +/// Compute the A* shortest path for a PyGraph +/// +/// :param PyGraph graph: The input graph to use +/// :param int node: The node index to compute the path from +/// :param goal_fn: A python callable that will take in 1 parameter, a node's data +/// object and will return a boolean which will be True if it is the finish +/// node. +/// :param edge_cost_fn: A python callable that will take in 1 parameter, an edge's +/// data object and will return a float that represents the cost of that +/// edge. It must be non-negative. +/// :param estimate_cost_fn: A python callable that will take in 1 parameter, a +/// node's data object and will return a float which represents the estimated +/// cost for the next node. The return must be non-negative. For the +/// algorithm to find the actual shortest path, it should be admissible, +/// meaning that it should never overestimate the actual cost to get to the +/// nearest goal node. +/// +/// :returns: The computed shortest path between node and finish as a list +/// of node indices. +/// :rtype: NodeIndices +#[pyfunction] +#[pyo3(text_signature = "(graph, node, goal_fn, edge_cost, estimate_cost, /)")] +fn graph_astar_shortest_path( + py: Python, + graph: &graph::PyGraph, + node: usize, + goal_fn: PyObject, + edge_cost_fn: PyObject, + estimate_cost_fn: PyObject, +) -> PyResult { + let goal_fn_callable = |a: &PyObject| -> PyResult { + let res = goal_fn.call1(py, (a,))?; + let raw = res.to_object(py); + let output: bool = raw.extract(py)?; + Ok(output) + }; + + let edge_cost_callable = |a: &PyObject| -> PyResult { + let res = edge_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + let output: f64 = raw.extract(py)?; + Ok(output) + }; + + let estimate_cost_callable = |a: &PyObject| -> PyResult { + let res = estimate_cost_fn.call1(py, (a,))?; + let raw = res.to_object(py); + let output: f64 = raw.extract(py)?; + Ok(output) + }; + let start = NodeIndex::new(node); + + let astar_res = astar::astar( + graph, + start, + |f| goal_fn_callable(graph.graph.node_weight(f).unwrap()), + |e| edge_cost_callable(e.weight()), + |estimate| { + estimate_cost_callable(graph.graph.node_weight(estimate).unwrap()) + }, + )?; + let path = match astar_res { + Some(path) => path, + None => { + return Err(NoPathFound::new_err( + "No path found that satisfies goal_fn", + )) + } + }; + Ok(NodeIndices { + nodes: path.1.into_iter().map(|x| x.index()).collect(), + }) +} + +/// Compute the length of the kth shortest path +/// +/// Computes the lengths of the kth shortest path from ``start`` to every +/// reachable node. +/// +/// Computes in :math:`O(k * (|E| + |V|*log(|V|)))` time (average). +/// +/// :param PyGraph graph: The graph to find the shortest paths in +/// :param int start: The node index to find the shortest paths from +/// :param int k: The kth shortest path to find the lengths of +/// :param edge_cost: A python callable that will receive an edge payload and +/// return a float for the cost of that eedge +/// :param int goal: An optional goal node index, if specified the output +/// dictionary +/// +/// :returns: A dict of lengths where the key is the destination node index and +/// the value is the length of the path. +/// :rtype: PathLengthMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, start, k, edge_cost, /, goal=None)")] +fn digraph_k_shortest_path_lengths( + py: Python, + graph: &digraph::PyDiGraph, + start: usize, + k: usize, + edge_cost: PyObject, + goal: Option, +) -> PyResult { + let out_goal = goal.map(NodeIndex::new); + let edge_cost_callable = |edge: &PyObject| -> PyResult { + let res = edge_cost.call1(py, (edge,))?; + res.extract(py) + }; + + let out_map = k_shortest_path::k_shortest_path( + graph, + NodeIndex::new(start), + out_goal, + k, + edge_cost_callable, + )?; + Ok(PathLengthMapping { + path_lengths: out_map + .iter() + .filter_map(|(k, v)| { + let k_int = k.index(); + if goal.is_some() && goal.unwrap() != k_int { + None + } else { + Some((k_int, *v)) + } + }) + .collect(), + }) +} + +/// Compute the length of the kth shortest path +/// +/// Computes the lengths of the kth shortest path from ``start`` to every +/// reachable node. +/// +/// Computes in :math:`O(k * (|E| + |V|*log(|V|)))` time (average). +/// +/// :param PyGraph graph: The graph to find the shortest paths in +/// :param int start: The node index to find the shortest paths from +/// :param int k: The kth shortest path to find the lengths of +/// :param edge_cost: A python callable that will receive an edge payload and +/// return a float for the cost of that eedge +/// :param int goal: An optional goal node index, if specified the output +/// dictionary +/// +/// :returns: A dict of lengths where the key is the destination node index and +/// the value is the length of the path. +/// :rtype: PathLengthMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, start, k, edge_cost, /, goal=None)")] +pub fn graph_k_shortest_path_lengths( + py: Python, + graph: &graph::PyGraph, + start: usize, + k: usize, + edge_cost: PyObject, + goal: Option, +) -> PyResult { + let out_goal = goal.map(NodeIndex::new); + let edge_cost_callable = |edge: &PyObject| -> PyResult { + let res = edge_cost.call1(py, (edge,))?; + res.extract(py) + }; + + let out_map = k_shortest_path::k_shortest_path( + graph, + NodeIndex::new(start), + out_goal, + k, + edge_cost_callable, + )?; + Ok(PathLengthMapping { + path_lengths: out_map + .iter() + .filter_map(|(k, v)| { + let k_int = k.index(); + if goal.is_some() && goal.unwrap() != k_int { + None + } else { + Some((k_int, *v)) + } + }) + .collect(), + }) +} + +/// Find all-pairs shortest path lengths using Floyd's algorithm +/// +/// Floyd's algorithm is used for finding shortest paths in dense graphs +/// or graphs with negative weights (where Dijkstra's algorithm fails). +/// +/// This function is multithreaded and will launch a pool with threads equal +/// to the number of CPUs by default if the number of nodes in the graph is +/// above the value of ``parallel_threshold`` (it defaults to 300). +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads if parallelization was enabled. +/// +/// :param PyDiGraph graph: The directed graph to run Floyd's algorithm on +/// :param weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// digraph_floyd_warshall(graph, weight_fn= lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// digraph_floyd_warshall(graph, weight_fn=float) +/// +/// to cast the edge object as a float as the weight. +/// :param as_undirected: If set to true each directed edge will be treated as +/// bidirectional/undirected. +/// :param int parallel_threshold: The number of nodes to execute +/// the algorithm in parallel at. It defaults to 300, but this can +/// be tuned +/// +/// :return: A read-only dictionary of path lengths. The keys are source +/// node indices and the values are dicts of the target node and the length +/// of the shortest path to that node. For example:: +/// +/// { +/// 0: {0: 0.0, 1: 2.0, 2: 2.0}, +/// 1: {1: 0.0, 2: 1.0}, +/// 2: {0: 1.0, 2: 0.0}, +/// } +/// +/// :rtype: AllPairsPathLengthMapping +#[pyfunction( + parallel_threshold = "300", + as_undirected = "false", + default_weight = "1.0" +)] +#[pyo3( + text_signature = "(graph, /, weight_fn=None, as_undirected=False, default_weight=1.0, parallel_threshold=300)" +)] +fn digraph_floyd_warshall( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: Option, + as_undirected: bool, + default_weight: f64, + parallel_threshold: usize, +) -> PyResult { + floyd_warshall::floyd_warshall( + py, + &graph.graph, + weight_fn, + as_undirected, + default_weight, + parallel_threshold, + ) +} + +/// Find all-pairs shortest path lengths using Floyd's algorithm +/// +/// Floyd's algorithm is used for finding shortest paths in dense graphs +/// or graphs with negative weights (where Dijkstra's algorithm fails). +/// +/// This function is multithreaded and will launch a pool with threads equal +/// to the number of CPUs by default if the number of nodes in the graph is +/// above the value of ``parallel_threshold`` (it defaults to 300). +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads if parallelization was enabled. +/// +/// :param PyGraph graph: The graph to run Floyd's algorithm on +/// :param weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// graph_floyd_warshall(graph, weight_fn= lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// graph_floyd_warshall(graph, weight_fn=float) +/// +/// to cast the edge object as a float as the weight. +/// :param int parallel_threshold: The number of nodes to execute +/// the algorithm in parallel at. It defaults to 300, but this can +/// be tuned +/// +/// :return: A read-only dictionary of path lengths. The keys are source +/// node indices and the values are dicts of the target node and the length +/// of the shortest path to that node. For example:: +/// +/// { +/// 0: {0: 0.0, 1: 2.0, 2: 2.0}, +/// 1: {1: 0.0, 2: 1.0}, +/// 2: {0: 1.0, 2: 0.0}, +/// } +/// +/// :rtype: AllPairsPathLengthMapping +#[pyfunction(parallel_threshold = "300", default_weight = "1.0")] +#[pyo3( + text_signature = "(graph, /, weight_fn=None, default_weight=1.0, parallel_threshold=300)" +)] +pub fn graph_floyd_warshall( + py: Python, + graph: &graph::PyGraph, + weight_fn: Option, + default_weight: f64, + parallel_threshold: usize, +) -> PyResult { + let as_undirected = true; + floyd_warshall::floyd_warshall( + py, + &graph.graph, + weight_fn, + as_undirected, + default_weight, + parallel_threshold, + ) +} + +/// Find all-pairs shortest path lengths using Floyd's algorithm +/// +/// Floyd's algorithm is used for finding shortest paths in dense graphs +/// or graphs with negative weights (where Dijkstra's algorithm fails). +/// +/// This function is multithreaded and will launch a pool with threads equal +/// to the number of CPUs by default if the number of nodes in the graph is +/// above the value of ``parallel_threshold`` (it defaults to 300). +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads if parallelization was enabled. +/// +/// :param PyGraph graph: The graph to run Floyd's algorithm on +/// :param weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: float(x)) +/// +/// to cast the edge object as a float as the weight. +/// :param int parallel_threshold: The number of nodes to execute +/// the algorithm in parallel at. It defaults to 300, but this can +/// be tuned +/// +/// :returns: A matrix of shortest path distances between nodes. If there is no +/// path between two nodes then the corresponding matrix entry will be +/// ``np.inf``. +/// :rtype: numpy.ndarray +#[pyfunction(parallel_threshold = "300", default_weight = "1.0")] +#[pyo3( + text_signature = "(graph, /, weight_fn=None, default_weight=1.0, parallel_threshold=300)" +)] +pub fn graph_floyd_warshall_numpy( + py: Python, + graph: &graph::PyGraph, + weight_fn: Option, + default_weight: f64, + parallel_threshold: usize, +) -> PyResult { + let n = graph.node_count(); + // Allocate empty matrix + let mut mat = Array2::::from_elem((n, n), std::f64::INFINITY); + + // Build adjacency matrix + for (i, j, weight) in get_edge_iter_with_weights(graph) { + let edge_weight = + weight_callable(py, &weight_fn, &weight, default_weight)?; + mat[[i, j]] = mat[[i, j]].min(edge_weight); + mat[[j, i]] = mat[[j, i]].min(edge_weight); + } + + // 0 out the diagonal + for x in mat.diag_mut() { + *x = 0.0; + } + // Perform the Floyd-Warshall algorithm. + // In each loop, this finds the shortest path from point i + // to point j using intermediate nodes 0..k + if n < parallel_threshold { + for k in 0..n { + for i in 0..n { + for j in 0..n { + let d_ijk = mat[[i, k]] + mat[[k, j]]; + if d_ijk < mat[[i, j]] { + mat[[i, j]] = d_ijk; + } + } + } + } + } else { + for k in 0..n { + let row_k = mat.slice(s![k, ..]).to_owned(); + mat.axis_iter_mut(Axis(0)) + .into_par_iter() + .for_each(|mut row_i| { + let m_ik = row_i[k]; + row_i.iter_mut().zip(row_k.iter()).for_each( + |(m_ij, m_kj)| { + let d_ijk = m_ik + *m_kj; + if d_ijk < *m_ij { + *m_ij = d_ijk; + } + }, + ) + }) + } + } + Ok(mat.into_pyarray(py).into()) +} + +/// Find all-pairs shortest path lengths using Floyd's algorithm +/// +/// Floyd's algorithm is used for finding shortest paths in dense graphs +/// or graphs with negative weights (where Dijkstra's algorithm fails). +/// +/// This function is multithreaded and will launch a pool with threads equal +/// to the number of CPUs by default if the number of nodes in the graph is +/// above the value of ``parallel_threshold`` (it defaults to 300). +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads if parallelization was enabled. +/// +/// :param PyDiGraph graph: The directed graph to run Floyd's algorithm on +/// :param weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: float(x)) +/// +/// to cast the edge object as a float as the weight. +/// :param as_undirected: If set to true each directed edge will be treated as +/// bidirectional/undirected. +/// :param int parallel_threshold: The number of nodes to execute +/// the algorithm in parallel at. It defaults to 300, but this can +/// be tuned +/// +/// :returns: A matrix of shortest path distances between nodes. If there is no +/// path between two nodes then the corresponding matrix entry will be +/// ``np.inf``. +/// :rtype: numpy.ndarray +#[pyfunction( + parallel_threshold = "300", + as_undirected = "false", + default_weight = "1.0" +)] +#[pyo3( + text_signature = "(graph, /, weight_fn=None, as_undirected=False, default_weight=1.0, parallel_threshold=300)" +)] +pub fn digraph_floyd_warshall_numpy( + py: Python, + graph: &digraph::PyDiGraph, + weight_fn: Option, + as_undirected: bool, + default_weight: f64, + parallel_threshold: usize, +) -> PyResult { + let n = graph.node_count(); + + // Allocate empty matrix + let mut mat = Array2::::from_elem((n, n), std::f64::INFINITY); + + // Build adjacency matrix + for (i, j, weight) in get_edge_iter_with_weights(graph) { + let edge_weight = + weight_callable(py, &weight_fn, &weight, default_weight)?; + mat[[i, j]] = mat[[i, j]].min(edge_weight); + if as_undirected { + mat[[j, i]] = mat[[j, i]].min(edge_weight); + } + } + // 0 out the diagonal + for x in mat.diag_mut() { + *x = 0.0; + } + // Perform the Floyd-Warshall algorithm. + // In each loop, this finds the shortest path from point i + // to point j using intermediate nodes 0..k + if n < parallel_threshold { + for k in 0..n { + for i in 0..n { + for j in 0..n { + let d_ijk = mat[[i, k]] + mat[[k, j]]; + if d_ijk < mat[[i, j]] { + mat[[i, j]] = d_ijk; + } + } + } + } + } else { + for k in 0..n { + let row_k = mat.slice(s![k, ..]).to_owned(); + mat.axis_iter_mut(Axis(0)) + .into_par_iter() + .for_each(|mut row_i| { + let m_ik = row_i[k]; + row_i.iter_mut().zip(row_k.iter()).for_each( + |(m_ij, m_kj)| { + let d_ijk = m_ik + *m_kj; + if d_ijk < *m_ij { + *m_ij = d_ijk; + } + }, + ) + }) + } + } + Ok(mat.into_pyarray(py).into()) +} + +/// Get the number of unweighted shortest paths from a source node +/// +/// :param PyDiGraph graph: The graph to find the number of shortest paths on +/// :param int source: The source node to find the shortest paths from +/// +/// :returns: A mapping of target node indices to the number of shortest paths +/// from ``source`` to that node. If there is no path from ``source`` to +/// a node in the graph that node will not be preset in the output mapping. +/// :rtype: NodesCountMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, source, /)")] +pub fn digraph_num_shortest_paths_unweighted( + graph: &digraph::PyDiGraph, + source: usize, +) -> PyResult { + Ok(NodesCountMapping { + map: num_shortest_path::num_shortest_paths_unweighted( + &graph.graph, + source, + )?, + }) +} + +/// Get the number of unweighted shortest paths from a source node +/// +/// :param PyGraph graph: The graph to find the number of shortest paths on +/// :param int source: The source node to find the shortest paths from +/// +/// :returns: A mapping of target node indices to the number of shortest paths +/// from ``source`` to that node. If there is no path from ``source`` to +/// a node in the graph that node will not be preset in the output mapping. +/// :rtype: NumPathsMapping +#[pyfunction] +#[pyo3(text_signature = "(graph, source, /)")] +pub fn graph_num_shortest_paths_unweighted( + graph: &graph::PyGraph, + source: usize, +) -> PyResult { + Ok(NodesCountMapping { + map: num_shortest_path::num_shortest_paths_unweighted( + &graph.graph, + source, + )?, + }) +} + +/// Get the distance matrix for a directed graph +/// +/// This differs from functions like digraph_floyd_warshall_numpy in that the +/// edge weight/data payload is not used and each edge is treated as a +/// distance of 1. +/// +/// This function is also multithreaded and will run in parallel if the number +/// of nodes in the graph is above the value of ``parallel_threshold`` (it +/// defaults to 300). If the function will be running in parallel the env var +/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used. +/// +/// :param PyDiGraph graph: The graph to get the distance matrix for +/// :param int parallel_threshold: The number of nodes to calculate the +/// the distance matrix in parallel at. It defaults to 300, but this can +/// be tuned +/// :param bool as_undirected: If set to ``True`` the input directed graph +/// will be treat as if each edge was bidirectional/undirected in the +/// output distance matrix. +/// +/// :returns: The distance matrix +/// :rtype: numpy.ndarray +#[pyfunction(parallel_threshold = "300", as_undirected = "false")] +#[pyo3( + text_signature = "(graph, /, parallel_threshold=300, as_undirected=False)" +)] +pub fn digraph_distance_matrix( + py: Python, + graph: &digraph::PyDiGraph, + parallel_threshold: usize, + as_undirected: bool, +) -> PyResult { + let n = graph.node_count(); + let mut matrix = Array2::::zeros((n, n)); + let bfs_traversal = |index: usize, mut row: ArrayViewMut1| { + let mut seen: HashMap = HashMap::with_capacity(n); + let start_index = NodeIndex::new(index); + let mut level = 0; + let mut next_level: HashSet = HashSet::new(); + next_level.insert(start_index); + while !next_level.is_empty() { + let this_level = next_level; + next_level = HashSet::new(); + let mut found: Vec = Vec::new(); + for v in this_level { + if !seen.contains_key(&v) { + seen.insert(v, level); + found.push(v); + row[[v.index()]] = level as f64; + } + } + if seen.len() == n { + return; + } + for node in found { + for v in graph + .graph + .neighbors_directed(node, petgraph::Direction::Outgoing) + { + next_level.insert(v); + } + if as_undirected { + for v in graph + .graph + .neighbors_directed(node, petgraph::Direction::Incoming) + { + next_level.insert(v); + } + } + } + level += 1 + } + }; + if n < parallel_threshold { + matrix + .axis_iter_mut(Axis(0)) + .enumerate() + .for_each(|(index, row)| bfs_traversal(index, row)); + } else { + // Parallelize by row and iterate from each row index in BFS order + matrix + .axis_iter_mut(Axis(0)) + .into_par_iter() + .enumerate() + .for_each(|(index, row)| bfs_traversal(index, row)); + } + Ok(matrix.into_pyarray(py).into()) +} + +/// Get the distance matrix for an undirected graph +/// +/// This differs from functions like digraph_floyd_warshall_numpy in that the +/// edge weight/data payload is not used and each edge is treated as a +/// distance of 1. +/// +/// This function is also multithreaded and will run in parallel if the number +/// of nodes in the graph is above the value of ``paralllel_threshold`` (it +/// defaults to 300). If the function will be running in parallel the env var +/// ``RAYON_NUM_THREADS`` can be used to adjust how many threads will be used. +/// +/// :param PyGraph graph: The graph to get the distance matrix for +/// :param int parallel_threshold: The number of nodes to calculate the +/// the distance matrix in parallel at. It defaults to 300, but this can +/// be tuned +/// +/// :returns: The distance matrix +/// :rtype: numpy.ndarray +#[pyfunction(parallel_threshold = "300")] +#[pyo3(text_signature = "(graph, /, parallel_threshold=300)")] +pub fn graph_distance_matrix( + py: Python, + graph: &graph::PyGraph, + parallel_threshold: usize, +) -> PyResult { + let n = graph.node_count(); + let mut matrix = Array2::::zeros((n, n)); + let bfs_traversal = |index: usize, mut row: ArrayViewMut1| { + let mut seen: HashMap = HashMap::with_capacity(n); + let start_index = NodeIndex::new(index); + let mut level = 0; + let mut next_level: HashSet = HashSet::new(); + next_level.insert(start_index); + while !next_level.is_empty() { + let this_level = next_level; + next_level = HashSet::new(); + let mut found: Vec = Vec::new(); + for v in this_level { + if !seen.contains_key(&v) { + seen.insert(v, level); + found.push(v); + row[[v.index()]] = level as f64; + } + } + if seen.len() == n { + return; + } + for node in found { + for v in graph.graph.neighbors(node) { + next_level.insert(v); + } + } + level += 1 + } + }; + if n < parallel_threshold { + matrix + .axis_iter_mut(Axis(0)) + .enumerate() + .for_each(|(index, row)| bfs_traversal(index, row)); + } else { + // Parallelize by row and iterate from each row index in BFS order + matrix + .axis_iter_mut(Axis(0)) + .into_par_iter() + .enumerate() + .for_each(|(index, row)| bfs_traversal(index, row)); + } + Ok(matrix.into_pyarray(py).into()) +} diff --git a/src/shortest_path/num_shortest_path.rs b/src/shortest_path/num_shortest_path.rs new file mode 100644 index 0000000000..d00b4e948b --- /dev/null +++ b/src/shortest_path/num_shortest_path.rs @@ -0,0 +1,77 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use hashbrown::HashMap; + +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; + +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::visit::{Bfs, NodeIndexable}; +use petgraph::EdgeType; + +use num_bigint::{BigUint, ToBigUint}; + +pub fn num_shortest_paths_unweighted( + graph: &StableGraph, + source: usize, +) -> PyResult> { + let mut out_map: Vec = + vec![0.to_biguint().unwrap(); graph.node_bound()]; + let node_index = NodeIndex::new(source); + if graph.node_weight(node_index).is_none() { + return Err(PyIndexError::new_err(format!( + "No node found for index {}", + source + ))); + } + let mut bfs = Bfs::new(&graph, node_index); + let mut distance: Vec> = vec![None; graph.node_bound()]; + distance[node_index.index()] = Some(0); + out_map[source] = 1.to_biguint().unwrap(); + while let Some(current) = bfs.next(graph) { + let dist_plus_one = distance[current.index()].unwrap_or_default() + 1; + let count_current = out_map[current.index()].clone(); + for neighbor_index in + graph.neighbors_directed(current, petgraph::Direction::Outgoing) + { + let neighbor: usize = neighbor_index.index(); + if distance[neighbor].is_none() { + distance[neighbor] = Some(dist_plus_one); + out_map[neighbor] = count_current.clone(); + } else if distance[neighbor] == Some(dist_plus_one) { + out_map[neighbor] += &count_current; + } + } + } + + // Do not count paths to source in output + distance[source] = None; + out_map[source] = 0.to_biguint().unwrap(); + + // Return only nodes that are reachable in the graph + Ok(out_map + .into_iter() + .zip(distance.iter()) + .enumerate() + .filter_map(|(index, (count, dist))| { + if dist.is_some() { + Some((index, count)) + } else { + None + } + }) + .collect()) +} diff --git a/src/steiner_tree.rs b/src/steiner_tree.rs index 2ece185607..d47eeb5820 100644 --- a/src/steiner_tree.rs +++ b/src/steiner_tree.rs @@ -18,8 +18,8 @@ use pyo3::Python; use petgraph::graph::NodeIndex; -use crate::_all_pairs_dijkstra_shortest_paths; use crate::graph; +use crate::shortest_path::all_pairs_dijkstra::all_pairs_dijkstra_shortest_paths; struct MetricClosureEdge { source: usize, @@ -71,7 +71,7 @@ fn _metric_closure_edges( } let mut out_vec = Vec::with_capacity(node_count * (node_count - 1) / 2); let mut distances = HashMap::with_capacity(graph.graph.node_count()); - let paths = _all_pairs_dijkstra_shortest_paths( + let paths = all_pairs_dijkstra_shortest_paths( py, &graph.graph, weight_fn, diff --git a/src/transitivity.rs b/src/transitivity.rs new file mode 100644 index 0000000000..c5038c1681 --- /dev/null +++ b/src/transitivity.rs @@ -0,0 +1,194 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use super::{digraph, graph}; +use hashbrown::HashSet; + +use pyo3::prelude::*; + +use petgraph::graph::NodeIndex; +use rayon::prelude::*; + +fn _graph_triangles(graph: &graph::PyGraph, node: usize) -> (usize, usize) { + let mut triangles: usize = 0; + + let index = NodeIndex::new(node); + let mut neighbors: HashSet = + graph.graph.neighbors(index).collect(); + neighbors.remove(&index); + + for nodev in &neighbors { + triangles += graph + .graph + .neighbors(*nodev) + .filter(|&x| (x != *nodev) && neighbors.contains(&x)) + .count(); + } + + let d: usize = neighbors.len(); + let triples: usize = match d { + 0 => 0, + _ => (d * (d - 1)) / 2, + }; + + (triangles / 2, triples) +} + +/// Compute the transitivity of an undirected graph. +/// +/// The transitivity of a graph is defined as: +/// +/// .. math:: +/// `c=3 \times \frac{\text{number of triangles}}{\text{number of connected triples}}` +/// +/// A “connected triple” means a single vertex with +/// edges running to an unordered pair of others. +/// +/// This function is multithreaded and will run +/// launch a thread pool with threads equal to the number of CPUs by default. +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads. +/// +/// .. note:: +/// +/// The function implicitly assumes that there are no parallel edges +/// or self loops. It may produce incorrect/unexpected results if the +/// input graph has self loops or parallel edges. +/// +/// :param PyGraph graph: Graph to be used. +/// +/// :returns: Transitivity. +/// :rtype: float +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +fn graph_transitivity(graph: &graph::PyGraph) -> f64 { + let node_indices: Vec = graph.graph.node_indices().collect(); + let (triangles, triples) = node_indices + .par_iter() + .map(|node| _graph_triangles(graph, node.index())) + .reduce( + || (0, 0), + |(sumx, sumy), (resx, resy)| (sumx + resx, sumy + resy), + ); + + match triangles { + 0 => 0.0, + _ => triangles as f64 / triples as f64, + } +} + +fn _digraph_triangles( + graph: &digraph::PyDiGraph, + node: usize, +) -> (usize, usize) { + let mut triangles: usize = 0; + + let index = NodeIndex::new(node); + let mut out_neighbors: HashSet = graph + .graph + .neighbors_directed(index, petgraph::Direction::Outgoing) + .collect(); + out_neighbors.remove(&index); + + let mut in_neighbors: HashSet = graph + .graph + .neighbors_directed(index, petgraph::Direction::Incoming) + .collect(); + in_neighbors.remove(&index); + + let neighbors = out_neighbors.iter().chain(in_neighbors.iter()); + + for nodev in neighbors { + triangles += graph + .graph + .neighbors_directed(*nodev, petgraph::Direction::Outgoing) + .chain( + graph + .graph + .neighbors_directed(*nodev, petgraph::Direction::Incoming), + ) + .map(|x| { + let mut res: usize = 0; + + if (x != *nodev) && out_neighbors.contains(&x) { + res += 1; + } + if (x != *nodev) && in_neighbors.contains(&x) { + res += 1; + } + res + }) + .sum::(); + } + + let din: usize = in_neighbors.len(); + let dout: usize = out_neighbors.len(); + + let dtot = dout + din; + let dbil: usize = out_neighbors.intersection(&in_neighbors).count(); + let triples: usize = match dtot { + 0 => 0, + _ => dtot * (dtot - 1) - 2 * dbil, + }; + + (triangles / 2, triples) +} + +/// Compute the transitivity of a directed graph. +/// +/// The transitivity of a directed graph is defined in [Fag]_, Eq.8: +/// +/// .. math:: +/// `c=3 \times \frac{\text{number of triangles}}{\text{number of all possible triangles}}` +/// +/// A triangle is a connected triple of nodes. +/// Different edge orientations counts as different triangles. +/// +/// This function is multithreaded and will run +/// launch a thread pool with threads equal to the number of CPUs by default. +/// You can tune the number of threads with the ``RAYON_NUM_THREADS`` +/// environment variable. For example, setting ``RAYON_NUM_THREADS=4`` would +/// limit the thread pool to 4 threads. +/// +/// .. note:: +/// +/// The function implicitly assumes that there are no parallel edges +/// or self loops. It may produce incorrect/unexpected results if the +/// input graph has self loops or parallel edges. +/// +/// :param PyDiGraph graph: Directed graph to be used. +/// +/// :returns: Transitivity. +/// :rtype: float +/// +/// .. [Fag] Clustering in complex directed networks by G. Fagiolo, +/// Physical Review E, 76(2), 026107 (2007) +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +fn digraph_transitivity(graph: &digraph::PyDiGraph) -> f64 { + let node_indices: Vec = graph.graph.node_indices().collect(); + let (triangles, triples) = node_indices + .par_iter() + .map(|node| _digraph_triangles(graph, node.index())) + .reduce( + || (0, 0), + |(sumx, sumy), (resx, resy)| (sumx + resx, sumy + resy), + ); + + match triangles { + 0 => 0.0, + _ => triangles as f64 / triples as f64, + } +} diff --git a/src/traversal/dfs_edges.rs b/src/traversal/dfs_edges.rs new file mode 100644 index 0000000000..dda270a72a --- /dev/null +++ b/src/traversal/dfs_edges.rs @@ -0,0 +1,88 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use hashbrown::{HashMap, HashSet}; + +use petgraph::graph::NodeIndex; +use petgraph::visit::{ + GraphBase, IntoNeighbors, IntoNodeIdentifiers, NodeCount, NodeIndexable, + VisitMap, Visitable, +}; + +pub fn dfs_edges( + graph: G, + source: Option, + edge_count: usize, +) -> Vec<(usize, usize)> +where + G: GraphBase + + IntoNodeIdentifiers + + NodeIndexable + + IntoNeighbors + + NodeCount + + Visitable, + ::Map: VisitMap, +{ + let nodes: Vec = match source { + Some(start) => vec![NodeIndex::new(start)], + None => graph + .node_identifiers() + .map(|ind| NodeIndex::new(graph.to_index(ind))) + .collect(), + }; + let node_count = graph.node_count(); + let mut visited: HashSet = HashSet::with_capacity(node_count); + let mut out_vec: Vec<(usize, usize)> = Vec::with_capacity(edge_count); + for start in nodes { + if visited.contains(&start) { + continue; + } + visited.insert(start); + let mut children: Vec = graph.neighbors(start).collect(); + children.reverse(); + let mut stack: Vec<(NodeIndex, Vec)> = + vec![(start, children)]; + // Used to track the last position in children vec across iterations + let mut index_map: HashMap = + HashMap::with_capacity(node_count); + index_map.insert(start, 0); + while !stack.is_empty() { + let temp_parent = stack.last().unwrap(); + let parent = temp_parent.0; + let children = temp_parent.1.clone(); + let count = *index_map.get(&parent).unwrap(); + let mut found = false; + let mut index = count; + for child in &children[index..] { + index += 1; + if !visited.contains(child) { + out_vec.push((parent.index(), child.index())); + visited.insert(*child); + let mut grandchildren: Vec = + graph.neighbors(*child).collect(); + grandchildren.reverse(); + stack.push((*child, grandchildren)); + index_map.insert(*child, 0); + *index_map.get_mut(&parent).unwrap() = index; + found = true; + break; + } + } + if !found || children.is_empty() { + stack.pop(); + } + } + } + out_vec +} diff --git a/src/traversal/mod.rs b/src/traversal/mod.rs new file mode 100644 index 0000000000..c4b0b0fa21 --- /dev/null +++ b/src/traversal/mod.rs @@ -0,0 +1,169 @@ +// 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. + +#![allow(clippy::float_cmp)] + +mod dfs_edges; + +use super::{digraph, graph, iterators}; + +use hashbrown::HashSet; + +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::visit::{Bfs, NodeCount, Reversed}; + +use crate::iterators::EdgeList; + +/// Get edge list in depth first order +/// +/// :param PyDiGraph graph: The graph to get the DFS edge list from +/// :param int source: An optional node index to use as the starting node +/// for the depth-first search. The edge list will only return edges in +/// the components reachable from this index. If this is not specified +/// then a source will be chosen arbitrarly and repeated until all +/// components of the graph are searched. +/// +/// :returns: A list of edges as a tuple of the form ``(source, target)`` in +/// depth-first order +/// :rtype: EdgeList +#[pyfunction] +#[pyo3(text_signature = "(graph, /, source=None)")] +fn digraph_dfs_edges( + graph: &digraph::PyDiGraph, + source: Option, +) -> EdgeList { + EdgeList { + edges: dfs_edges::dfs_edges(graph, source, graph.graph.edge_count()), + } +} + +/// Get edge list in depth first order +/// +/// :param PyGraph graph: The graph to get the DFS edge list from +/// :param int source: An optional node index to use as the starting node +/// for the depth-first search. The edge list will only return edges in +/// the components reachable from this index. If this is not specified +/// then a source will be chosen arbitrarly and repeated until all +/// components of the graph are searched. +/// +/// :returns: A list of edges as a tuple of the form ``(source, target)`` in +/// depth-first order +/// :rtype: EdgeList +#[pyfunction] +#[pyo3(text_signature = "(graph, /, source=None)")] +fn graph_dfs_edges(graph: &graph::PyGraph, source: Option) -> EdgeList { + EdgeList { + edges: dfs_edges::dfs_edges(graph, source, graph.graph.edge_count()), + } +} + +/// Return successors in a breadth-first-search from a source node. +/// +/// The return format is ``[(Parent Node, [Children Nodes])]`` in a bfs order +/// from the source node provided. +/// +/// :param PyDiGraph graph: The DAG to get the bfs_successors from +/// :param int node: The index of the dag node to get the bfs successors for +/// +/// :returns: A list of nodes's data and their children in bfs order. The +/// BFSSuccessors class that is returned is a custom container class that +/// implements the sequence protocol. This can be used as a python list +/// with index based access. +/// :rtype: BFSSuccessors +#[pyfunction] +#[pyo3(text_signature = "(graph, node, /)")] +fn bfs_successors( + py: Python, + graph: &digraph::PyDiGraph, + node: usize, +) -> iterators::BFSSuccessors { + let index = NodeIndex::new(node); + let mut bfs = Bfs::new(graph, index); + let mut out_list: Vec<(PyObject, Vec)> = + Vec::with_capacity(graph.node_count()); + while let Some(nx) = bfs.next(graph) { + let children = graph + .graph + .neighbors_directed(nx, petgraph::Direction::Outgoing); + let mut succesors: Vec = Vec::new(); + for succ in children { + succesors + .push(graph.graph.node_weight(succ).unwrap().clone_ref(py)); + } + if !succesors.is_empty() { + out_list.push(( + graph.graph.node_weight(nx).unwrap().clone_ref(py), + succesors, + )); + } + } + iterators::BFSSuccessors { + bfs_successors: out_list, + } +} + +/// Return the ancestors of a node in a graph. +/// +/// This differs from :meth:`PyDiGraph.predecessors` method in that +/// ``predecessors`` returns only nodes with a direct edge into the provided +/// node. While this function returns all nodes that have a path into the +/// provided node. +/// +/// :param PyDiGraph graph: The graph to get the descendants from +/// :param int node: The index of the graph node to get the ancestors for +/// +/// :returns: A list of node indexes of ancestors of provided node. +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, node, /)")] +fn ancestors(graph: &digraph::PyDiGraph, node: usize) -> HashSet { + let index = NodeIndex::new(node); + let mut out_set: HashSet = HashSet::new(); + let reverse_graph = Reversed(graph); + let res = algo::dijkstra(reverse_graph, index, None, |_| 1); + for n in res.keys() { + let n_int = n.index(); + out_set.insert(n_int); + } + out_set.remove(&node); + out_set +} + +/// Return the descendants of a node in a graph. +/// +/// This differs from :meth:`PyDiGraph.successors` method in that +/// ``successors``` returns only nodes with a direct edge out of the provided +/// node. While this function returns all nodes that have a path from the +/// provided node. +/// +/// :param PyDiGraph graph: The graph to get the descendants from +/// :param int node: The index of the graph node to get the descendants for +/// +/// :returns: A list of node indexes of descendants of provided node. +/// :rtype: list +#[pyfunction] +#[pyo3(text_signature = "(graph, node, /)")] +fn descendants(graph: &digraph::PyDiGraph, node: usize) -> HashSet { + let index = NodeIndex::new(node); + let mut out_set: HashSet = HashSet::new(); + let res = algo::dijkstra(graph, index, None, |_| 1); + for n in res.keys() { + let n_int = n.index(); + out_set.insert(n_int); + } + out_set.remove(&node); + out_set +} diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000000..5507176e8e --- /dev/null +++ b/src/tree.rs @@ -0,0 +1,140 @@ +// 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. + +#![allow(clippy::float_cmp)] + +use std::cmp::Ordering; + +use super::{graph, weight_callable}; + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::prelude::*; +use petgraph::stable_graph::EdgeReference; +use petgraph::unionfind::UnionFind; +use petgraph::visit::{IntoEdgeReferences, NodeIndexable}; + +use rayon::prelude::*; + +use crate::iterators::WeightedEdgeList; + +/// Find the edges in the minimum spanning tree or forest of a graph +/// using Kruskal's algorithm. +/// +/// :param PyGraph graph: Undirected graph +/// :param weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// minimum_spanning_edges(graph, weight_fn: lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// minimum_spanning_edges(graph, weight_fn: float) +/// +/// to cast the edge object as a float as the weight. +/// :param float default_weight: If ``weight_fn`` isn't specified this optional +/// float value will be used for the weight/cost of each edge. +/// +/// :returns: The :math:`N - |c|` edges of the Minimum Spanning Tree (or Forest, if :math:`|c| > 1`) +/// where :math:`N` is the number of nodes and :math:`|c|` is the number of connected components of the graph +/// :rtype: WeightedEdgeList +#[pyfunction(weight_fn = "None", default_weight = "1.0")] +#[pyo3(text_signature = "(graph, weight_fn=None, default_weight=1.0)")] +pub fn minimum_spanning_edges( + py: Python, + graph: &graph::PyGraph, + weight_fn: Option, + default_weight: f64, +) -> PyResult { + let mut subgraphs = UnionFind::::new(graph.graph.node_bound()); + + let mut edge_list: Vec<(f64, EdgeReference)> = + Vec::with_capacity(graph.graph.edge_count()); + for edge in graph.edge_references() { + let weight = + weight_callable(py, &weight_fn, edge.weight(), default_weight)?; + if weight.is_nan() { + return Err(PyValueError::new_err("NaN found as an edge weight")); + } + edge_list.push((weight, edge)); + } + + edge_list.par_sort_unstable_by(|a, b| { + let weight_a = a.0; + let weight_b = b.0; + weight_a.partial_cmp(&weight_b).unwrap_or(Ordering::Less) + }); + + let mut answer: Vec<(usize, usize, PyObject)> = Vec::new(); + for float_edge_pair in edge_list.iter() { + let edge = float_edge_pair.1; + let u = edge.source().index(); + let v = edge.target().index(); + if subgraphs.union(u, v) { + let w = edge.weight().clone_ref(py); + answer.push((u, v, w)); + } + } + + Ok(WeightedEdgeList { edges: answer }) +} + +/// Find the minimum spanning tree or forest of a graph +/// using Kruskal's algorithm. +/// +/// :param PyGraph graph: Undirected graph +/// :param weight_fn: A callable object (function, lambda, etc) which +/// will be passed the edge object and expected to return a ``float``. This +/// tells retworkx/rust how to extract a numerical weight as a ``float`` +/// for edge object. Some simple examples are:: +/// +/// minimum_spanning_tree(graph, weight_fn: lambda x: 1) +/// +/// to return a weight of 1 for all edges. Also:: +/// +/// minimum_spanning_tree(graph, weight_fn: float) +/// +/// to cast the edge object as a float as the weight. +/// :param float default_weight: If ``weight_fn`` isn't specified this optional +/// float value will be used for the weight/cost of each edge. +/// +/// :returns: A Minimum Spanning Tree (or Forest, if the graph is not connected). +/// +/// :rtype: PyGraph +/// +/// .. note:: +/// +/// The new graph will keep the same node indexes, but edge indexes might differ. +#[pyfunction(weight_fn = "None", default_weight = "1.0")] +#[pyo3(text_signature = "(graph, weight_fn=None, default_weight=1.0)")] +pub fn minimum_spanning_tree( + py: Python, + graph: &graph::PyGraph, + weight_fn: Option, + default_weight: f64, +) -> PyResult { + let mut spanning_tree = (*graph).clone(); + spanning_tree.graph.clear_edges(); + + for edge in minimum_spanning_edges(py, graph, weight_fn, default_weight)? + .edges + .iter() + { + spanning_tree.add_edge(edge.0, edge.1, edge.2.clone_ref(py))?; + } + + Ok(spanning_tree) +} diff --git a/src/union.rs b/src/union.rs index 590dd0b1c1..307bcfccbe 100644 --- a/src/union.rs +++ b/src/union.rs @@ -10,7 +10,7 @@ // License for the specific language governing permissions and limitations // under the License. -use crate::digraph::PyDiGraph; +use crate::{digraph, digraph::PyDiGraph}; use hashbrown::{HashMap, HashSet}; use petgraph::algo; use petgraph::graph::EdgeIndex; @@ -34,7 +34,7 @@ use std::cmp::Ordering; /// The nodes from graph `b` will replace nodes from `a`. /// /// At this point, only `PyDiGraph` is supported. -pub fn digraph_union( +fn _digraph_union( py: Python, a: &PyDiGraph, b: &PyDiGraph, @@ -111,3 +111,45 @@ pub fn digraph_union( Ok(combined) } + +/// Return a new PyDiGraph by forming a union from two input PyDiGraph objects +/// +/// The algorithm in this function operates in three phases: +/// +/// 1. Add all the nodes from ``second`` into ``first``. operates in O(n), +/// with n being number of nodes in `b`. +/// 2. Merge nodes from ``second`` over ``first`` given that: +/// +/// - The ``merge_nodes`` is ``True``. operates in O(n^2), with n being the +/// number of nodes in ``second``. +/// - The respective node in ``second`` and ``first`` share the same +/// weight/data payload. +/// +/// 3. Adds all the edges from ``second`` to ``first``. If the ``merge_edges`` +/// parameter is ``True`` and the respective edge in ``second`` and +/// first`` share the same weight/data payload they will be merged +/// together. +/// +/// :param PyDiGraph first: The first directed graph object +/// :param PyDiGraph second: The second directed graph object +/// :param bool merge_nodes: If set to ``True`` nodes will be merged between +/// ``second`` and ``first`` if the weights are equal. +/// :param bool merge_edges: If set to ``True`` edges will be merged between +/// ``second`` and ``first`` if the weights are equal. +/// +/// :returns: A new PyDiGraph object that is the union of ``second`` and +/// ``first``. It's worth noting the weight/data payload objects are +/// passed by reference from ``first`` and ``second`` to this new object. +/// :rtype: PyDiGraph +#[pyfunction] +#[pyo3(text_signature = "(first, second, merge_nodes, merge_edges, /)")] +fn digraph_union( + py: Python, + first: &digraph::PyDiGraph, + second: &digraph::PyDiGraph, + merge_nodes: bool, + merge_edges: bool, +) -> PyResult { + let res = _digraph_union(py, first, second, merge_nodes, merge_edges)?; + Ok(res) +}