From 45493c93eaf764fbca729952d142346f01f138dc Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Fri, 23 Jul 2021 18:03:13 -0700 Subject: [PATCH 01/38] Reorganize dag algorithms into new file --- src/dag_algorithms.rs | 305 ++++++++++++++++++++++++++++++++++++++++++ src/digraph.rs | 6 +- src/lib.rs | 278 +------------------------------------- 3 files changed, 312 insertions(+), 277 deletions(-) create mode 100644 src/dag_algorithms.rs diff --git a/src/dag_algorithms.rs b/src/dag_algorithms.rs new file mode 100644 index 0000000000..2755bea9b0 --- /dev/null +++ b/src/dag_algorithms.rs @@ -0,0 +1,305 @@ +// 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 crate::{digraph, longest_path}; + +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 super::InvalidNode; + +use super::iterators::NodeIndices; + +/// 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(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(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(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(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()) +} diff --git a/src/digraph.rs b/src/digraph.rs index 5ba1f99fdb..6b3b2f2c27 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_algorithms::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/lib.rs b/src/lib.rs index a819714284..9ea5fe7992 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ #![allow(clippy::float_cmp)] mod astar; +mod dag_algorithms; mod digraph; mod dijkstra; mod dot_utils; @@ -28,6 +29,8 @@ mod union; use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeSet, BinaryHeap}; +use dag_algorithms::*; + use hashbrown::{HashMap, HashSet}; use pyo3::create_exception; @@ -135,174 +138,6 @@ where 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 @@ -378,22 +213,6 @@ pub fn is_weakly_connected(graph: &digraph::PyDiGraph) -> PyResult { 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: @@ -1782,97 +1601,6 @@ fn collect_runs( Ok(out_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 From ccf9f4cfd2f274190c6f3a34abc2ac4133157635 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Fri, 23 Jul 2021 19:27:08 -0700 Subject: [PATCH 02/38] Reorganize shortest path algorithms in new file --- src/lib.rs | 1909 +++++------------------------------------- src/shortest_path.rs | 1500 +++++++++++++++++++++++++++++++++ 2 files changed, 1731 insertions(+), 1678 deletions(-) create mode 100644 src/shortest_path.rs diff --git a/src/lib.rs b/src/lib.rs index 9ea5fe7992..0cc7b68ae0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,17 +24,19 @@ mod iterators; mod k_shortest_path; mod layout; mod max_weight_matching; +mod shortest_path; mod union; use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeSet, BinaryHeap}; use dag_algorithms::*; +use shortest_path::*; use hashbrown::{HashMap, HashSet}; use pyo3::create_exception; -use pyo3::exceptions::{PyException, PyIndexError, PyValueError}; +use pyo3::exceptions::{PyException, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; use pyo3::wrap_pyfunction; @@ -47,14 +49,13 @@ 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, + Bfs, Data, GraphBase, GraphProp, IntoEdgeReferences, IntoNeighbors, + IntoNodeIdentifiers, NodeCount, NodeIndexable, Reversed, VisitMap, + Visitable, }; use petgraph::EdgeType; use ndarray::prelude::*; -use num_bigint::{BigUint, ToBigUint}; use num_traits::{Num, Zero}; use numpy::IntoPyArray; use rand::distributions::{Distribution, Uniform}; @@ -63,13 +64,9 @@ 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 crate::iterators::{EdgeList, NodeIndices, Pos2DMapping, WeightedEdgeList}; -trait NodesRemoved { +pub trait NodesRemoved { fn nodes_removed(&self) -> bool; } @@ -925,367 +922,78 @@ fn graph_greedy_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. +/// Collect runs that match a filter function /// -/// Computes in :math:`O(k * (|E| + |V|*log(|V|)))` time (average). +/// 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 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 +/// :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 dict of lengths where the key is the destination node index and -/// the value is the length of the path. -/// :rtype: PathLengthMapping +/// :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, start, k, edge_cost, /, goal=None)")] -fn digraph_k_shortest_path_lengths( +#[pyo3(text_signature = "(graph, filter)")] +fn collect_runs( 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(), - }) -} + filter_fn: PyObject, +) -> PyResult>> { + let mut out_list: Vec> = Vec::new(); + let mut seen: HashSet = + HashSet::with_capacity(graph.node_count()); -/// 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,))?; + let filter_node = |node: &PyObject| -> PyResult { + let res = filter_fn.call1(py, (node,))?; 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); + let nodes = match algo::toposort(graph, None) { + Ok(nodes) => nodes, + Err(_err) => { + return Err(DAGHasCycle::new_err("Sort encountered a cycle")) } - if as_undirected { - if let Some(row_j) = mat.get_mut(j) { - insert_or_minimize!(row_j, i, edge_weight); - } + }; + 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(); - // 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); - } - } - }) + 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(); } - } 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); - } - } - }) + if !group.is_empty() { + out_list.push(group); } } - - // 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, - ) + Ok(out_list) } -fn get_edge_iter_with_weights( +pub fn get_edge_iter_with_weights( graph: G, ) -> impl Iterator where @@ -1331,458 +1039,29 @@ 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). +/// Return the adjacency matrix for a PyDiGraph object /// -/// 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. +/// 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 to run Floyd's algorithm on -/// :param weight_fn: A callable object (function, lambda, etc) which +/// :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:: /// -/// graph_floyd_warshall_numpy(graph, weight_fn: lambda x: 1) +/// dag_adjacency_matrix(dag, 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:: +/// dag_adjacency_matrix(dag, weight_fn: lambda x: float(x)) /// -/// 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) -} - -/// 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. +/// 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 @@ -1791,744 +1070,184 @@ pub fn graph_distance_matrix( 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, - }) -} - -fn _all_pairs_dijkstra_shortest_paths( - py: Python, - graph: &StableGraph, - edge_cost_fn: PyObject, -) -> 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(); - Ok(AllPairsPathMapping { - paths: node_indices - .into_par_iter() - .map(|x| { - let mut paths: HashMap> = - HashMap::with_capacity(graph.node_count()); - dijkstra::dijkstra( - graph, - x, - None, - |e| edge_cost(e.id()), - Some(&mut paths), - ) - .unwrap(); - 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(), - }) -} - -/// 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) + 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()) } -/// Find the shortest path from all nodes in a :class:`~retworkx.PyDiGraph` -/// object +/// Return the adjacency matrix for a PyGraph class /// -/// 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. +/// 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:: /// -/// :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 +/// graph_adjacency_matrix(graph, weight_fn: lambda x: 1) /// -/// :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:: +/// to return a weight of 1 for all edges. Also:: /// -/// { -/// 0: {1: [0, 1], 2: [0, 1, 2]}, -/// 1: {2: [1, 2]}, -/// 2: {0: [2, 0]}, -/// } +/// graph_adjacency_matrix(graph, weight_fn: lambda x: float(x)) /// -/// :rtype: AllPairsPathMapping -#[pyfunction] -#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] -pub fn digraph_all_pairs_dijkstra_shortest_paths( +/// 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: &digraph::PyDiGraph, - edge_cost_fn: PyObject, -) -> PyResult { - _all_pairs_dijkstra_shortest_paths(py, &graph.graph, edge_cost_fn) + 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()) } -/// 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 all simple paths between 2 nodes in a PyGraph object /// -/// :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:: +/// A simple path is a path with no repeated nodes. /// -/// { -/// 0: {1: 2.0, 2: 2.0}, -/// 1: {2: 1.0}, -/// 2: {0: 1.0}, -/// } +/// :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. /// -/// :rtype: AllPairsPathLengthMapping +/// :returns: A list of lists where each inner list is a path of node indices +/// :rtype: list #[pyfunction] -#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] -pub fn graph_all_pairs_dijkstra_path_lengths( - py: Python, +#[pyo3(text_signature = "(graph, from, to, /, min=None, cutoff=None)")] +fn graph_all_simple_paths( graph: &graph::PyGraph, - edge_cost_fn: PyObject, -) -> PyResult { - _all_pairs_dijkstra_path_lengths(py, &graph.graph, edge_cost_fn) + 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) } -/// 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 all simple paths between 2 nodes in a PyDiGraph object /// -/// :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:: +/// A simple path is a path with no repeated nodes. /// -/// { -/// 0: {1: [0, 1], 2: [0, 1, 2]}, -/// 1: {2: [1, 2]}, -/// 2: {0: [2, 0]}, -/// } +/// :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. /// -/// :rtype: AllPairsPathMapping +/// :returns: A list of lists where each inner list is a path +/// :rtype: list #[pyfunction] -#[pyo3(text_signature = "(graph, edge_cost_fn, /)")] -pub fn graph_all_pairs_dijkstra_shortest_paths( +#[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, - graph: &graph::PyGraph, - edge_cost_fn: PyObject, -) -> PyResult { - _all_pairs_dijkstra_shortest_paths(py, &graph.graph, edge_cost_fn) + 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), + } } /// Compute the A* shortest path for a PyGraph @@ -2605,80 +1324,6 @@ fn graph_astar_shortest_path( }) } -/// 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. /// @@ -4516,98 +3161,6 @@ pub fn digraph_spiral_layout( 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. create_exception!(retworkx, InvalidNode, PyException); // Performing this operation would result in trying to add a cycle to a DAG. diff --git a/src/shortest_path.rs b/src/shortest_path.rs new file mode 100644 index 0000000000..9c22b0cab2 --- /dev/null +++ b/src/shortest_path.rs @@ -0,0 +1,1500 @@ +// 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 crate::{ + astar, digraph, dijkstra, get_edge_iter_with_weights, graph, + k_shortest_path, NoPathFound, +}; + +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::visit::{ + Bfs, EdgeIndexable, IntoEdgeReferences, NodeCount, NodeIndexable, +}; +use petgraph::EdgeType; + +use ndarray::prelude::*; +use num_bigint::{BigUint, ToBigUint}; +use numpy::IntoPyArray; +use rayon::prelude::*; + +use crate::iterators::{ + AllPairsPathLengthMapping, AllPairsPathMapping, NodeIndices, + NodesCountMapping, PathLengthMapping, PathMapping, +}; + +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)")] +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(), + }) +} + +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, + }) +} + +fn _all_pairs_dijkstra_shortest_paths( + py: Python, + graph: &StableGraph, + edge_cost_fn: PyObject, +) -> 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(); + Ok(AllPairsPathMapping { + paths: node_indices + .into_par_iter() + .map(|x| { + let mut paths: HashMap> = + HashMap::with_capacity(graph.node_count()); + dijkstra::dijkstra( + graph, + x, + None, + |e| edge_cost(e.id()), + Some(&mut paths), + ) + .unwrap(); + 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(), + }) +} + +/// 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) +} + +/// 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) +} + +/// 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 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(), + }) +} + +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, + }) +} + +/// 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)" +)] +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( + 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()) +} + +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)?, + }) +} + +/// 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()) +} From f218504b4f691279cdff5f1c8dea8e8950aa8958 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Fri, 23 Jul 2021 19:29:56 -0700 Subject: [PATCH 03/38] Move astar to short path file --- src/shortest_path.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/shortest_path.rs b/src/shortest_path.rs index 9c22b0cab2..9e7f1ad974 100644 --- a/src/shortest_path.rs +++ b/src/shortest_path.rs @@ -689,6 +689,80 @@ fn digraph_astar_shortest_path( }) } +/// 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 From db353d2c5042771249f475bbf8e5cd66c7115f98 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 14:21:17 -0700 Subject: [PATCH 04/38] Reorganize matching in new file --- src/lib.rs | 238 +----------------- src/matching.rs | 187 ++++++++++++++ ...atching.rs => max_weight_matching_algo.rs} | 0 3 files changed, 190 insertions(+), 235 deletions(-) create mode 100644 src/matching.rs rename src/{max_weight_matching.rs => max_weight_matching_algo.rs} (100%) diff --git a/src/lib.rs b/src/lib.rs index 0cc7b68ae0..372b0ba75b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,8 @@ mod isomorphism; mod iterators; mod k_shortest_path; mod layout; -mod max_weight_matching; +mod matching; +mod max_weight_matching_algo; mod shortest_path; mod union; @@ -32,6 +33,7 @@ use std::collections::{BTreeSet, BinaryHeap}; use dag_algorithms::*; use shortest_path::*; +use matching::*; use hashbrown::{HashMap, HashSet}; @@ -1250,80 +1252,6 @@ fn weight_callable( } } -/// 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(), - }) -} - /// Return a :math:`G_{np}` directed random graph, also known as an /// Erdős-Rényi graph or a binomial graph. /// @@ -1880,72 +1808,6 @@ pub fn cycle_basis( 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 @@ -2047,100 +1909,6 @@ pub fn digraph_find_cycle( 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; diff --git a/src/matching.rs b/src/matching.rs new file mode 100644 index 0000000000..dc48ef8236 --- /dev/null +++ b/src/matching.rs @@ -0,0 +1,187 @@ +// 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::max_weight_matching_algo; +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_algo::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) + }) +} \ No newline at end of file diff --git a/src/max_weight_matching.rs b/src/max_weight_matching_algo.rs similarity index 100% rename from src/max_weight_matching.rs rename to src/max_weight_matching_algo.rs From 794e97d7ff45037b21bd4741c28d7e1a96b0a12a Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 15:37:02 -0700 Subject: [PATCH 05/38] Reorganize random_circuit in new file --- src/lib.rs | 468 +-------------------------------------- src/random_circuit.rs | 494 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 496 insertions(+), 466 deletions(-) create mode 100644 src/random_circuit.rs diff --git a/src/lib.rs b/src/lib.rs index 372b0ba75b..1725bafb54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod k_shortest_path; mod layout; mod matching; mod max_weight_matching_algo; +mod random_circuit; mod shortest_path; mod union; @@ -34,6 +35,7 @@ use std::collections::{BTreeSet, BinaryHeap}; use dag_algorithms::*; use shortest_path::*; use matching::*; +use random_circuit::*; use hashbrown::{HashMap, HashSet}; @@ -1252,472 +1254,6 @@ fn weight_callable( } } -/// 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 diff --git a/src/random_circuit.rs b/src/random_circuit.rs new file mode 100644 index 0000000000..672121ebb6 --- /dev/null +++ b/src/random_circuit.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::{graph, digraph}; + +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) +} \ No newline at end of file From 9f0d235423acb8248f5bdd94746b85c359f799f1 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 15:37:35 -0700 Subject: [PATCH 06/38] Run cargo fmt --- src/lib.rs | 2 +- src/matching.rs | 10 ++++------ src/random_circuit.rs | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1725bafb54..53016a042d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,9 @@ use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeSet, BinaryHeap}; use dag_algorithms::*; -use shortest_path::*; use matching::*; use random_circuit::*; +use shortest_path::*; use hashbrown::{HashMap, HashSet}; diff --git a/src/matching.rs b/src/matching.rs index dc48ef8236..f4e4537fa9 100644 --- a/src/matching.rs +++ b/src/matching.rs @@ -13,18 +13,16 @@ #![allow(clippy::float_cmp)] use super::max_weight_matching_algo; -use crate::{graph}; +use crate::graph; -use hashbrown::{HashSet}; +use hashbrown::HashSet; use pyo3::prelude::*; use pyo3::Python; use petgraph::graph::NodeIndex; use petgraph::prelude::*; -use petgraph::visit::{ - IntoEdgeReferences -}; +use petgraph::visit::IntoEdgeReferences; /// Compute a maximum-weighted matching for a :class:`~retworkx.PyGraph` /// @@ -184,4 +182,4 @@ pub fn is_maximal_matching( tmp_set.insert((e[0], e[1])); !_inner_is_matching(graph, &tmp_set) }) -} \ No newline at end of file +} diff --git a/src/random_circuit.rs b/src/random_circuit.rs index 672121ebb6..c04a576f33 100644 --- a/src/random_circuit.rs +++ b/src/random_circuit.rs @@ -12,11 +12,11 @@ #![allow(clippy::float_cmp)] -use crate::{graph, digraph}; +use crate::{digraph, graph}; -use pyo3::exceptions::{ PyValueError}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyDict}; +use pyo3::types::PyDict; use pyo3::Python; use petgraph::algo; @@ -491,4 +491,4 @@ pub fn random_geometric_graph( multigraph: true, }; Ok(graph) -} \ No newline at end of file +} From 229e7818e7fc4f7c1307f02f4c9c464e8310fcd8 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 16:00:54 -0700 Subject: [PATCH 07/38] Move layout to its own module --- src/{ => layout}/layout.rs | 0 src/layout/mod.rs | 570 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 542 +---------------------------------- 3 files changed, 572 insertions(+), 540 deletions(-) rename src/{ => layout}/layout.rs (100%) create mode 100644 src/layout/mod.rs diff --git a/src/layout.rs b/src/layout/layout.rs similarity index 100% rename from src/layout.rs rename to src/layout/layout.rs diff --git a/src/layout/mod.rs b/src/layout/mod.rs new file mode 100644 index 0000000000..2ce616f273 --- /dev/null +++ b/src/layout/mod.rs @@ -0,0 +1,570 @@ +// 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 layout; + +use crate::{digraph, graph, weight_callable}; + +use hashbrown::{HashMap, HashSet}; + +use pyo3::exceptions::{ PyValueError}; +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::prelude::*; +use petgraph::visit::{ + IntoEdgeReferences, + NodeIndexable +}; +use petgraph::EdgeType; + +use rand::distributions::{Distribution, Uniform}; +use rand::prelude::*; +use rand_pcg::Pcg64; + +use crate::iterators::{Pos2DMapping}; + +#[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) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 53016a042d..f54c61771c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,7 @@ use dag_algorithms::*; use matching::*; use random_circuit::*; use shortest_path::*; +use layout::*; use hashbrown::{HashMap, HashSet}; @@ -62,13 +63,10 @@ use petgraph::EdgeType; use ndarray::prelude::*; 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::{EdgeList, NodeIndices, Pos2DMapping, WeightedEdgeList}; +use crate::iterators::{EdgeList, NodeIndices, WeightedEdgeList}; pub trait NodesRemoved { fn nodes_removed(&self) -> bool; @@ -1929,542 +1927,6 @@ fn digraph_complement( 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) -} - // The provided node is invalid. create_exception!(retworkx, InvalidNode, PyException); // Performing this operation would result in trying to add a cycle to a DAG. From 630765820b9bf08351998695bd83bb44dc84d96d Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 16:01:07 -0700 Subject: [PATCH 08/38] Run cargo fmt --- src/layout/mod.rs | 11 ++++------- src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 2ce616f273..30458c4c51 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -16,22 +16,19 @@ use crate::{digraph, graph, weight_callable}; use hashbrown::{HashMap, HashSet}; -use pyo3::exceptions::{ PyValueError}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::Python; use petgraph::prelude::*; -use petgraph::visit::{ - IntoEdgeReferences, - NodeIndexable -}; +use petgraph::visit::{IntoEdgeReferences, NodeIndexable}; use petgraph::EdgeType; use rand::distributions::{Distribution, Uniform}; use rand::prelude::*; use rand_pcg::Pcg64; -use crate::iterators::{Pos2DMapping}; +use crate::iterators::Pos2DMapping; #[allow(clippy::too_many_arguments)] fn _spring_layout( @@ -567,4 +564,4 @@ pub fn digraph_spiral_layout( equidistant: Option, ) -> Pos2DMapping { layout::spiral_layout(&graph.graph, scale, center, resolution, equidistant) -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index f54c61771c..59187acf01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,10 +33,10 @@ use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeSet, BinaryHeap}; use dag_algorithms::*; +use layout::*; use matching::*; use random_circuit::*; use shortest_path::*; -use layout::*; use hashbrown::{HashMap, HashSet}; From e0b9b94be3d739bd813fe51d0960adf599440c6d Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 16:11:35 -0700 Subject: [PATCH 09/38] Move isomorphism to its own module --- src/{ => isomorphism}/isomorphism.rs | 2 +- src/isomorphism/mod.rs | 326 +++++++++++++++++++++++++++ src/lib.rs | 305 +------------------------ 3 files changed, 328 insertions(+), 305 deletions(-) rename src/{ => isomorphism}/isomorphism.rs (99%) create mode 100644 src/isomorphism/mod.rs diff --git a/src/isomorphism.rs b/src/isomorphism/isomorphism.rs similarity index 99% rename from src/isomorphism.rs rename to src/isomorphism/isomorphism.rs index c4c9edb484..3915dd894f 100644 --- a/src/isomorphism.rs +++ b/src/isomorphism/isomorphism.rs @@ -19,7 +19,7 @@ use std::marker; use hashbrown::HashMap; -use super::NodesRemoved; +use crate::NodesRemoved; use pyo3::prelude::*; diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs new file mode 100644 index 0000000000..334dbfce8b --- /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 isomorphism; + +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 = 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) +} diff --git a/src/lib.rs b/src/lib.rs index 59187acf01..6d1b5c1123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeSet, BinaryHeap}; use dag_algorithms::*; +use isomorphism::*; use layout::*; use matching::*; use random_circuit::*; @@ -255,310 +256,6 @@ fn digraph_union( 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 From 8d5ef8ef038f9bcd37cb4ee41150b9683b0f02cb Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 16:18:28 -0700 Subject: [PATCH 10/38] Move matching to its own module --- src/lib.rs | 1 - .../max_weight_matching.rs} | 0 src/{matching.rs => matching/mod.rs} | 4 ++-- 3 files changed, 2 insertions(+), 3 deletions(-) rename src/{max_weight_matching_algo.rs => matching/max_weight_matching.rs} (100%) rename src/{matching.rs => matching/mod.rs} (98%) diff --git a/src/lib.rs b/src/lib.rs index 6d1b5c1123..39f51f0fa2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,6 @@ mod iterators; mod k_shortest_path; mod layout; mod matching; -mod max_weight_matching_algo; mod random_circuit; mod shortest_path; mod union; diff --git a/src/max_weight_matching_algo.rs b/src/matching/max_weight_matching.rs similarity index 100% rename from src/max_weight_matching_algo.rs rename to src/matching/max_weight_matching.rs diff --git a/src/matching.rs b/src/matching/mod.rs similarity index 98% rename from src/matching.rs rename to src/matching/mod.rs index f4e4537fa9..edb29ebceb 100644 --- a/src/matching.rs +++ b/src/matching/mod.rs @@ -12,7 +12,7 @@ #![allow(clippy::float_cmp)] -use super::max_weight_matching_algo; +mod max_weight_matching; use crate::graph; use hashbrown::HashSet; @@ -80,7 +80,7 @@ pub fn max_weight_matching( default_weight: i128, verify_optimum: bool, ) -> PyResult> { - max_weight_matching_algo::max_weight_matching( + max_weight_matching::max_weight_matching( py, graph, max_cardinality, From bad296f83568fcfef1671a8511e2a69e9b945b0e Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 16:25:06 -0700 Subject: [PATCH 11/38] Move shortest_path to its own module --- src/lib.rs | 3 --- src/{ => shortest_path}/astar.rs | 0 src/{ => shortest_path}/dijkstra.rs | 2 +- src/{ => shortest_path}/k_shortest_path.rs | 2 +- src/{shortest_path.rs => shortest_path/mod.rs} | 9 +++++---- 5 files changed, 7 insertions(+), 9 deletions(-) rename src/{ => shortest_path}/astar.rs (100%) rename src/{ => shortest_path}/dijkstra.rs (99%) rename src/{ => shortest_path}/k_shortest_path.rs (99%) rename src/{shortest_path.rs => shortest_path/mod.rs} (99%) diff --git a/src/lib.rs b/src/lib.rs index 39f51f0fa2..7df2bb4b7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,16 +12,13 @@ #![allow(clippy::float_cmp)] -mod astar; mod dag_algorithms; mod digraph; -mod dijkstra; mod dot_utils; mod generators; mod graph; mod isomorphism; mod iterators; -mod k_shortest_path; mod layout; mod matching; mod random_circuit; 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/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.rs b/src/shortest_path/mod.rs similarity index 99% rename from src/shortest_path.rs rename to src/shortest_path/mod.rs index 9e7f1ad974..a9227b7082 100644 --- a/src/shortest_path.rs +++ b/src/shortest_path/mod.rs @@ -12,12 +12,13 @@ #![allow(clippy::float_cmp)] +mod astar; +mod dijkstra; +mod k_shortest_path; + use hashbrown::{HashMap, HashSet}; -use crate::{ - astar, digraph, dijkstra, get_edge_iter_with_weights, graph, - k_shortest_path, NoPathFound, -}; +use crate::{digraph, get_edge_iter_with_weights, graph, NoPathFound}; use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; From f6350760ed28a152793777a68cc8d0ecda2b1cbd Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 16:45:48 -0700 Subject: [PATCH 12/38] Remove weight_callable duplication --- src/shortest_path/mod.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/shortest_path/mod.rs b/src/shortest_path/mod.rs index a9227b7082..eb1b8a3fb6 100644 --- a/src/shortest_path/mod.rs +++ b/src/shortest_path/mod.rs @@ -18,6 +18,7 @@ mod k_shortest_path; use hashbrown::{HashMap, HashSet}; +use super::weight_callable; use crate::{digraph, get_edge_iter_with_weights, graph, NoPathFound}; use pyo3::exceptions::PyIndexError; @@ -41,21 +42,6 @@ use crate::iterators::{ NodesCountMapping, PathLengthMapping, PathMapping, }; -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 From 2ceb35df2b54f28790177f1f91ea437214bc9c41 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 16:53:17 -0700 Subject: [PATCH 13/38] Move floyd_warshall to its own file --- src/shortest_path/floyd_warshall.rs | 146 ++++++++++++++++++++++++++++ src/shortest_path/mod.rs | 127 +----------------------- 2 files changed, 150 insertions(+), 123 deletions(-) create mode 100644 src/shortest_path/floyd_warshall.rs 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/shortest_path/mod.rs b/src/shortest_path/mod.rs index eb1b8a3fb6..08da506c18 100644 --- a/src/shortest_path/mod.rs +++ b/src/shortest_path/mod.rs @@ -14,6 +14,7 @@ mod astar; mod dijkstra; +mod floyd_warshall; mod k_shortest_path; use hashbrown::{HashMap, HashSet}; @@ -27,9 +28,7 @@ use pyo3::Python; use petgraph::graph::NodeIndex; use petgraph::prelude::*; -use petgraph::visit::{ - Bfs, EdgeIndexable, IntoEdgeReferences, NodeCount, NodeIndexable, -}; +use petgraph::visit::{Bfs, EdgeIndexable, NodeCount, NodeIndexable}; use petgraph::EdgeType; use ndarray::prelude::*; @@ -862,124 +861,6 @@ pub fn graph_k_shortest_path_lengths( }) } -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, - }) -} - /// Find all-pairs shortest path lengths using Floyd's algorithm /// /// Floyd's algorithm is used for finding shortest paths in dense graphs @@ -1038,7 +919,7 @@ fn digraph_floyd_warshall( default_weight: f64, parallel_threshold: usize, ) -> PyResult { - _floyd_warshall( + floyd_warshall::floyd_warshall( py, &graph.graph, weight_fn, @@ -1100,7 +981,7 @@ pub fn graph_floyd_warshall( parallel_threshold: usize, ) -> PyResult { let as_undirected = true; - _floyd_warshall( + floyd_warshall::floyd_warshall( py, &graph.graph, weight_fn, From 0d8ec61ab825947463178b169717246f05d60e8a Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 17:12:01 -0700 Subject: [PATCH 14/38] Move num_shortest_path_unweighted to its own file --- src/shortest_path/mod.rs | 66 ++++------------------ src/shortest_path/num_shortest_path.rs | 77 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 56 deletions(-) create mode 100644 src/shortest_path/num_shortest_path.rs diff --git a/src/shortest_path/mod.rs b/src/shortest_path/mod.rs index 08da506c18..d93c7a29c9 100644 --- a/src/shortest_path/mod.rs +++ b/src/shortest_path/mod.rs @@ -16,6 +16,7 @@ mod astar; mod dijkstra; mod floyd_warshall; mod k_shortest_path; +mod num_shortest_path; use hashbrown::{HashMap, HashSet}; @@ -28,11 +29,10 @@ use pyo3::Python; use petgraph::graph::NodeIndex; use petgraph::prelude::*; -use petgraph::visit::{Bfs, EdgeIndexable, NodeCount, NodeIndexable}; +use petgraph::visit::{EdgeIndexable, NodeCount}; use petgraph::EdgeType; use ndarray::prelude::*; -use num_bigint::{BigUint, ToBigUint}; use numpy::IntoPyArray; use rayon::prelude::*; @@ -1190,58 +1190,6 @@ pub fn digraph_floyd_warshall_numpy( Ok(mat.into_pyarray(py).into()) } -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 @@ -1258,7 +1206,10 @@ pub fn digraph_num_shortest_paths_unweighted( source: usize, ) -> PyResult { Ok(NodesCountMapping { - map: _num_shortest_paths_unweighted(&graph.graph, source)?, + map: num_shortest_path::num_shortest_paths_unweighted( + &graph.graph, + source, + )?, }) } @@ -1278,7 +1229,10 @@ pub fn graph_num_shortest_paths_unweighted( source: usize, ) -> PyResult { Ok(NodesCountMapping { - map: _num_shortest_paths_unweighted(&graph.graph, source)?, + map: num_shortest_path::num_shortest_paths_unweighted( + &graph.graph, + source, + )?, }) } 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()) +} From 13db6c66c555aede86f793c3a53a6ffdc53a6c9f Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 24 Jul 2021 17:19:56 -0700 Subject: [PATCH 15/38] Move all_pairs_dijkstra to its own file --- src/shortest_path/all_pairs_dijkstra.rs | 198 ++++++++++++++++++++++++ src/shortest_path/mod.rs | 195 +++-------------------- 2 files changed, 220 insertions(+), 173 deletions(-) create mode 100644 src/shortest_path/all_pairs_dijkstra.rs diff --git a/src/shortest_path/all_pairs_dijkstra.rs b/src/shortest_path/all_pairs_dijkstra.rs new file mode 100644 index 0000000000..fffc5e8e56 --- /dev/null +++ b/src/shortest_path/all_pairs_dijkstra.rs @@ -0,0 +1,198 @@ +// 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 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, +) -> 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(); + Ok(AllPairsPathMapping { + paths: node_indices + .into_par_iter() + .map(|x| { + let mut paths: HashMap> = + HashMap::with_capacity(graph.node_count()); + dijkstra::dijkstra( + graph, + x, + None, + |e| edge_cost(e.id()), + Some(&mut paths), + ) + .unwrap(); + 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(), + }) +} diff --git a/src/shortest_path/mod.rs b/src/shortest_path/mod.rs index d93c7a29c9..7c64c569dc 100644 --- a/src/shortest_path/mod.rs +++ b/src/shortest_path/mod.rs @@ -12,6 +12,7 @@ #![allow(clippy::float_cmp)] +mod all_pairs_dijkstra; mod astar; mod dijkstra; mod floyd_warshall; @@ -23,14 +24,11 @@ use hashbrown::{HashMap, HashSet}; use super::weight_callable; use crate::{digraph, get_edge_iter_with_weights, graph, NoPathFound}; -use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; use pyo3::Python; use petgraph::graph::NodeIndex; -use petgraph::prelude::*; -use petgraph::visit::{EdgeIndexable, NodeCount}; -use petgraph::EdgeType; +use petgraph::visit::NodeCount; use ndarray::prelude::*; use numpy::IntoPyArray; @@ -296,171 +294,6 @@ pub fn digraph_dijkstra_shortest_path_lengths( }) } -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, - }) -} - -fn _all_pairs_dijkstra_shortest_paths( - py: Python, - graph: &StableGraph, - edge_cost_fn: PyObject, -) -> 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(); - Ok(AllPairsPathMapping { - paths: node_indices - .into_par_iter() - .map(|x| { - let mut paths: HashMap> = - HashMap::with_capacity(graph.node_count()); - dijkstra::dijkstra( - graph, - x, - None, - |e| edge_cost(e.id()), - Some(&mut paths), - ) - .unwrap(); - 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(), - }) -} - /// Calculate the the shortest length from all nodes in a /// :class:`~retworkx.PyDiGraph` object /// @@ -495,7 +328,11 @@ pub fn digraph_all_pairs_dijkstra_path_lengths( graph: &digraph::PyDiGraph, edge_cost_fn: PyObject, ) -> PyResult { - _all_pairs_dijkstra_path_lengths(py, &graph.graph, edge_cost_fn) + 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` @@ -532,7 +369,11 @@ pub fn digraph_all_pairs_dijkstra_shortest_paths( graph: &digraph::PyDiGraph, edge_cost_fn: PyObject, ) -> PyResult { - _all_pairs_dijkstra_shortest_paths(py, &graph.graph, edge_cost_fn) + all_pairs_dijkstra::all_pairs_dijkstra_shortest_paths( + py, + &graph.graph, + edge_cost_fn, + ) } /// Calculate the the shortest length from all nodes in a @@ -565,7 +406,11 @@ pub fn graph_all_pairs_dijkstra_path_lengths( graph: &graph::PyGraph, edge_cost_fn: PyObject, ) -> PyResult { - _all_pairs_dijkstra_path_lengths(py, &graph.graph, edge_cost_fn) + 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` @@ -598,7 +443,11 @@ pub fn graph_all_pairs_dijkstra_shortest_paths( graph: &graph::PyGraph, edge_cost_fn: PyObject, ) -> PyResult { - _all_pairs_dijkstra_shortest_paths(py, &graph.graph, edge_cost_fn) + all_pairs_dijkstra::all_pairs_dijkstra_shortest_paths( + py, + &graph.graph, + edge_cost_fn, + ) } /// Compute the A* shortest path for a PyDiGraph From 170f2f581536a8cb6df4bc5b368f29e1d77b608d Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 11:10:14 -0700 Subject: [PATCH 16/38] Rename to dag_algo --- src/{dag_algorithms.rs => dag_algo.rs} | 0 src/digraph.rs | 2 +- src/lib.rs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{dag_algorithms.rs => dag_algo.rs} (100%) diff --git a/src/dag_algorithms.rs b/src/dag_algo.rs similarity index 100% rename from src/dag_algorithms.rs rename to src/dag_algo.rs diff --git a/src/digraph.rs b/src/digraph.rs index 6b3b2f2c27..8a934135e8 100644 --- a/src/digraph.rs +++ b/src/digraph.rs @@ -54,7 +54,7 @@ use super::{ NodesRemoved, }; -use super::dag_algorithms::is_directed_acyclic_graph; +use super::dag_algo::is_directed_acyclic_graph; /// A class for creating directed graphs /// diff --git a/src/lib.rs b/src/lib.rs index 7df2bb4b7a..c2ebc293b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ #![allow(clippy::float_cmp)] -mod dag_algorithms; +mod dag_algo; mod digraph; mod dot_utils; mod generators; @@ -28,7 +28,7 @@ mod union; use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeSet, BinaryHeap}; -use dag_algorithms::*; +use dag_algo::*; use isomorphism::*; use layout::*; use matching::*; From e12b8f2b8c364e2ca1b86f49acb6d7b19dda9b27 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 11:20:17 -0700 Subject: [PATCH 17/38] Move tree algorithms to its own file --- src/lib.rs | 118 ++----------------------------------------- src/tree.rs | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 114 deletions(-) create mode 100644 src/tree.rs diff --git a/src/lib.rs b/src/lib.rs index c2ebc293b5..51da2c2e9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ mod layout; mod matching; mod random_circuit; mod shortest_path; +mod tree; mod union; use std::cmp::{Ordering, Reverse}; @@ -34,11 +35,12 @@ use layout::*; use matching::*; use random_circuit::*; use shortest_path::*; +use tree::*; use hashbrown::{HashMap, HashSet}; use pyo3::create_exception; -use pyo3::exceptions::{PyException, PyValueError}; +use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; use pyo3::wrap_pyfunction; @@ -48,8 +50,6 @@ 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, GraphBase, GraphProp, IntoEdgeReferences, IntoNeighbors, IntoNodeIdentifiers, NodeCount, NodeIndexable, Reversed, VisitMap, @@ -63,7 +63,7 @@ use numpy::IntoPyArray; use rayon::prelude::*; use crate::generators::PyInit_generators; -use crate::iterators::{EdgeList, NodeIndices, WeightedEdgeList}; +use crate::iterators::{EdgeList, NodeIndices}; pub trait NodesRemoved { fn nodes_removed(&self) -> bool; @@ -1427,116 +1427,6 @@ pub fn digraph_core_number( _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. 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) +} From 159bc7a369f09ba07a1c65c52aa1d0a7908cc4af Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 11:34:01 -0700 Subject: [PATCH 18/38] Move connectivity to its own file --- src/connectivity.rs | 292 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 270 +--------------------------------------- 2 files changed, 295 insertions(+), 267 deletions(-) create mode 100644 src/connectivity.rs diff --git a/src/connectivity.rs b/src/connectivity.rs new file mode 100644 index 0000000000..bf20036852 --- /dev/null +++ b/src/connectivity.rs @@ -0,0 +1,292 @@ +// 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, NullGraph}; + +use hashbrown::{HashMap, HashSet}; +use std::collections::BTreeSet; + +use pyo3::prelude::*; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::visit::NodeCount; + +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()) +} diff --git a/src/lib.rs b/src/lib.rs index 51da2c2e9e..1b13420512 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ #![allow(clippy::float_cmp)] +mod connectivity; mod dag_algo; mod digraph; mod dot_utils; @@ -27,8 +28,9 @@ mod tree; mod union; use std::cmp::{Ordering, Reverse}; -use std::collections::{BTreeSet, BinaryHeap}; +use std::collections::BinaryHeap; +use connectivity::*; use dag_algo::*; use isomorphism::*; use layout::*; @@ -134,81 +136,6 @@ where Ok((path, 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()) -} - /// Return a new PyDiGraph by forming a union from two input PyDiGraph objects /// /// The algorithm in this function operates in three phases: @@ -945,197 +872,6 @@ fn weight_callable( } } -/// 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 } -} - fn _graph_triangles(graph: &graph::PyGraph, node: usize) -> (usize, usize) { let mut triangles: usize = 0; From c288ab358dacecf1f7d5fb32425a41e993b92ce1 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 11:50:01 -0700 Subject: [PATCH 19/38] Move dag_algo to its own module --- src/dag_algo/longest_path.rs | 90 ++++++++++++++++++++++++++++ src/{dag_algo.rs => dag_algo/mod.rs} | 14 +++-- src/lib.rs | 66 -------------------- 3 files changed, 99 insertions(+), 71 deletions(-) create mode 100644 src/dag_algo/longest_path.rs rename src/{dag_algo.rs => dag_algo/mod.rs} (96%) 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.rs b/src/dag_algo/mod.rs similarity index 96% rename from src/dag_algo.rs rename to src/dag_algo/mod.rs index 2755bea9b0..ccab01a63d 100644 --- a/src/dag_algo.rs +++ b/src/dag_algo/mod.rs @@ -12,9 +12,11 @@ #![allow(clippy::float_cmp)] +mod longest_path; + use hashbrown::{HashMap, HashSet}; -use crate::{digraph, longest_path}; +use crate::digraph; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -65,7 +67,7 @@ pub fn dag_longest_path( } }; Ok(NodeIndices { - nodes: longest_path(graph, edge_weight_callable)?.0, + nodes: longest_path::longest_path(graph, edge_weight_callable)?.0, }) } @@ -104,7 +106,8 @@ pub fn dag_longest_path_length( None => Ok(1), } }; - let (_, path_weight) = longest_path(graph, edge_weight_callable)?; + let (_, path_weight) = + longest_path::longest_path(graph, edge_weight_callable)?; Ok(path_weight) } @@ -149,7 +152,7 @@ pub fn dag_weighted_longest_path( Ok(float_res) }; Ok(NodeIndices { - nodes: longest_path(graph, edge_weight_callable)?.0, + nodes: longest_path::longest_path(graph, edge_weight_callable)?.0, }) } @@ -193,7 +196,8 @@ pub fn dag_weighted_longest_path_length( } Ok(float_res) }; - let (_, path_weight) = longest_path(graph, edge_weight_callable)?; + let (_, path_weight) = + longest_path::longest_path(graph, edge_weight_callable)?; Ok(path_weight) } diff --git a/src/lib.rs b/src/lib.rs index 1b13420512..258273b3e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,6 @@ use petgraph::visit::{ use petgraph::EdgeType; use ndarray::prelude::*; -use num_traits::{Num, Zero}; use numpy::IntoPyArray; use rayon::prelude::*; @@ -71,71 +70,6 @@ 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)) -} - /// Return a new PyDiGraph by forming a union from two input PyDiGraph objects /// /// The algorithm in this function operates in three phases: From b80f7efdcc5117366d81e9126f688fb5dd1ab25a Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 12:15:27 -0700 Subject: [PATCH 20/38] Move traversal to its own module --- src/lib.rs | 402 +------------------------------------ src/traversal/dfs_edges.rs | 88 ++++++++ src/traversal/mod.rs | 354 ++++++++++++++++++++++++++++++++ 3 files changed, 448 insertions(+), 396 deletions(-) create mode 100644 src/traversal/dfs_edges.rs create mode 100644 src/traversal/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 258273b3e2..998ed8b563 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,11 +24,11 @@ mod layout; mod matching; mod random_circuit; mod shortest_path; +mod traversal; mod tree; mod union; -use std::cmp::{Ordering, Reverse}; -use std::collections::BinaryHeap; +use std::cmp::Reverse; use connectivity::*; use dag_algo::*; @@ -37,6 +37,7 @@ use layout::*; use matching::*; use random_circuit::*; use shortest_path::*; +use traversal::*; use tree::*; use hashbrown::{HashMap, HashSet}; @@ -44,7 +45,7 @@ use hashbrown::{HashMap, HashSet}; use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList}; +use pyo3::types::PyDict; use pyo3::wrap_pyfunction; use pyo3::wrap_pymodule; use pyo3::Python; @@ -53,9 +54,8 @@ use petgraph::algo; use petgraph::graph::NodeIndex; use petgraph::prelude::*; use petgraph::visit::{ - Bfs, Data, GraphBase, GraphProp, IntoEdgeReferences, IntoNeighbors, - IntoNodeIdentifiers, NodeCount, NodeIndexable, Reversed, VisitMap, - Visitable, + Data, GraphBase, GraphProp, IntoEdgeReferences, IntoNodeIdentifiers, + NodeCount, NodeIndexable, }; use petgraph::EdgeType; @@ -64,7 +64,6 @@ use numpy::IntoPyArray; use rayon::prelude::*; use crate::generators::PyInit_generators; -use crate::iterators::{EdgeList, NodeIndices}; pub trait NodesRemoved { fn nodes_removed(&self) -> bool; @@ -113,324 +112,6 @@ fn digraph_union( 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 @@ -478,77 +159,6 @@ fn graph_greedy_color( Ok(out_dict.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) -} - pub fn get_edge_iter_with_weights( graph: G, ) -> impl Iterator 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..70bed95e61 --- /dev/null +++ b/src/traversal/mod.rs @@ -0,0 +1,354 @@ +// 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, DAGHasCycle}; + +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use hashbrown::{HashMap, HashSet}; + +use pyo3::prelude::*; +use pyo3::types::PyList; +use pyo3::Python; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::visit::{Bfs, NodeCount, Reversed}; + +use crate::iterators::{EdgeList, NodeIndices}; + +/// 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(), + }) +} + +/// 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 +} + +/// 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()) +} + +/// 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) +} From b8b5eb15ae8f933e76e29a95e6cec2fdac39b626 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 12:17:26 -0700 Subject: [PATCH 21/38] Temporarily allow module inception --- src/isomorphism/mod.rs | 1 + src/layout/mod.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index 334dbfce8b..b625602cda 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -11,6 +11,7 @@ // under the License. #![allow(clippy::float_cmp)] +#![allow(clippy::module_inception)] mod isomorphism; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 30458c4c51..6fe289f6c6 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -10,6 +10,8 @@ // License for the specific language governing permissions and limitations // under the License. +#![allow(clippy::module_inception)] + mod layout; use crate::{digraph, graph, weight_callable}; From 22abf8505a740bb8c5f069e261cee8cc0ed8ab87 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 17:22:43 -0700 Subject: [PATCH 22/38] Rename isomorphism and layouts --- src/{isomorphism => isomorphism_algo}/isomorphism.rs | 0 src/{isomorphism => isomorphism_algo}/mod.rs | 1 - src/{layout => layout_algo}/layout.rs | 0 src/{layout => layout_algo}/mod.rs | 2 -- src/lib.rs | 8 ++++---- 5 files changed, 4 insertions(+), 7 deletions(-) rename src/{isomorphism => isomorphism_algo}/isomorphism.rs (100%) rename src/{isomorphism => isomorphism_algo}/mod.rs (99%) rename src/{layout => layout_algo}/layout.rs (100%) rename src/{layout => layout_algo}/mod.rs (99%) diff --git a/src/isomorphism/isomorphism.rs b/src/isomorphism_algo/isomorphism.rs similarity index 100% rename from src/isomorphism/isomorphism.rs rename to src/isomorphism_algo/isomorphism.rs diff --git a/src/isomorphism/mod.rs b/src/isomorphism_algo/mod.rs similarity index 99% rename from src/isomorphism/mod.rs rename to src/isomorphism_algo/mod.rs index b625602cda..334dbfce8b 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism_algo/mod.rs @@ -11,7 +11,6 @@ // under the License. #![allow(clippy::float_cmp)] -#![allow(clippy::module_inception)] mod isomorphism; diff --git a/src/layout/layout.rs b/src/layout_algo/layout.rs similarity index 100% rename from src/layout/layout.rs rename to src/layout_algo/layout.rs diff --git a/src/layout/mod.rs b/src/layout_algo/mod.rs similarity index 99% rename from src/layout/mod.rs rename to src/layout_algo/mod.rs index 6fe289f6c6..30458c4c51 100644 --- a/src/layout/mod.rs +++ b/src/layout_algo/mod.rs @@ -10,8 +10,6 @@ // License for the specific language governing permissions and limitations // under the License. -#![allow(clippy::module_inception)] - mod layout; use crate::{digraph, graph, weight_callable}; diff --git a/src/lib.rs b/src/lib.rs index 998ed8b563..4601d22c15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,9 +18,9 @@ mod digraph; mod dot_utils; mod generators; mod graph; -mod isomorphism; +mod isomorphism_algo; mod iterators; -mod layout; +mod layout_algo; mod matching; mod random_circuit; mod shortest_path; @@ -32,8 +32,8 @@ use std::cmp::Reverse; use connectivity::*; use dag_algo::*; -use isomorphism::*; -use layout::*; +use isomorphism_algo::*; +use layout_algo::*; use matching::*; use random_circuit::*; use shortest_path::*; From 420d47dbf1e083ac5b1f22db579899a620c5a5ee Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 17:31:05 -0700 Subject: [PATCH 23/38] Move simple_path to its own file --- src/lib.rs | 111 +-------------------------------------- src/simple_path.rs | 128 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 109 deletions(-) create mode 100644 src/simple_path.rs diff --git a/src/lib.rs b/src/lib.rs index 4601d22c15..53a0a74b22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ mod layout_algo; mod matching; mod random_circuit; mod shortest_path; +mod simple_path; mod traversal; mod tree; mod union; @@ -37,6 +38,7 @@ use layout_algo::*; use matching::*; use random_circuit::*; use shortest_path::*; +use simple_path::*; use traversal::*; use tree::*; @@ -50,7 +52,6 @@ use pyo3::wrap_pyfunction; use pyo3::wrap_pymodule; use pyo3::Python; -use petgraph::algo; use petgraph::graph::NodeIndex; use petgraph::prelude::*; use petgraph::visit::{ @@ -293,114 +294,6 @@ fn graph_adjacency_matrix( 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, diff --git a/src/simple_path.rs b/src/simple_path.rs new file mode 100644 index 0000000000..79366d9421 --- /dev/null +++ b/src/simple_path.rs @@ -0,0 +1,128 @@ +// 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, InvalidNode}; + +use pyo3::prelude::*; + +use petgraph::algo; +use petgraph::graph::NodeIndex; + +/// 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) +} From 8eb199ac0858dbdf51821cff14bae9621f9aecf0 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 17:34:35 -0700 Subject: [PATCH 24/38] Move transitivity to its own file --- src/lib.rs | 175 +-------------------------------------- src/transitivity.rs | 194 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 173 deletions(-) create mode 100644 src/transitivity.rs diff --git a/src/lib.rs b/src/lib.rs index 53a0a74b22..9e16c5116c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod matching; mod random_circuit; mod shortest_path; mod simple_path; +mod transitivity; mod traversal; mod tree; mod union; @@ -39,6 +40,7 @@ use matching::*; use random_circuit::*; use shortest_path::*; use simple_path::*; +use transitivity::*; use traversal::*; use tree::*; @@ -309,179 +311,6 @@ fn weight_callable( } } -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, 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, + } +} From e875d0d030628129f37dc3aed6ec361f0d3e77b4 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 17:39:52 -0700 Subject: [PATCH 25/38] Move core_number to its own file --- src/core_number.rs | 145 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 121 +------------------------------------ 2 files changed, 147 insertions(+), 119 deletions(-) create mode 100644 src/core_number.rs diff --git a/src/core_number.rs b/src/core_number.rs new file mode 100644 index 0000000000..1c16b4dbc8 --- /dev/null +++ b/src/core_number.rs @@ -0,0 +1,145 @@ +// 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::{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()) +} + +/// 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) +} diff --git a/src/lib.rs b/src/lib.rs index 9e16c5116c..57f063c97e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ #![allow(clippy::float_cmp)] mod connectivity; +mod core_number; mod dag_algo; mod digraph; mod dot_utils; @@ -33,6 +34,7 @@ mod union; use std::cmp::Reverse; use connectivity::*; +use core_number::*; use dag_algo::*; use isomorphism_algo::*; use layout_algo::*; @@ -60,7 +62,6 @@ use petgraph::visit::{ Data, GraphBase, GraphProp, IntoEdgeReferences, IntoNodeIdentifiers, NodeCount, NodeIndexable, }; -use petgraph::EdgeType; use ndarray::prelude::*; use numpy::IntoPyArray; @@ -311,124 +312,6 @@ fn weight_callable( } } -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) -} - /// Compute the complement of a graph. /// /// :param PyGraph graph: The graph to be used. From 7d81bdad31fdb892d5ce8753b81f6d6027ae74f6 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 17:47:47 -0700 Subject: [PATCH 26/38] Create representation_algo --- src/lib.rs | 175 +------------------------------- src/representation_algo.rs | 197 +++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 173 deletions(-) create mode 100644 src/representation_algo.rs diff --git a/src/lib.rs b/src/lib.rs index 57f063c97e..096f4e5d08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ mod iterators; mod layout_algo; mod matching; mod random_circuit; +mod representation_algo; mod shortest_path; mod simple_path; mod transitivity; @@ -40,6 +41,7 @@ use isomorphism_algo::*; use layout_algo::*; use matching::*; use random_circuit::*; +use representation_algo::*; use shortest_path::*; use simple_path::*; use transitivity::*; @@ -63,8 +65,6 @@ use petgraph::visit::{ NodeCount, NodeIndexable, }; -use ndarray::prelude::*; -use numpy::IntoPyArray; use rayon::prelude::*; use crate::generators::PyInit_generators; @@ -209,94 +209,6 @@ where }) } -/// 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()) -} - fn weight_callable( py: Python, weight_fn: &Option, @@ -312,89 +224,6 @@ fn weight_callable( } } -/// 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) -} - // The provided node is invalid. create_exception!(retworkx, InvalidNode, PyException); // Performing this operation would result in trying to add a cycle to a DAG. diff --git a/src/representation_algo.rs b/src/representation_algo.rs new file mode 100644 index 0000000000..ba201b23ec --- /dev/null +++ b/src/representation_algo.rs @@ -0,0 +1,197 @@ +// 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, get_edge_iter_with_weights, graph, weight_callable}; + +use hashbrown::HashSet; + +use pyo3::prelude::*; +use pyo3::Python; + +use petgraph::graph::NodeIndex; +use petgraph::visit::NodeCount; + +use ndarray::prelude::*; +use numpy::IntoPyArray; + +/// 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()) +} + +/// 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) +} From 326b62ebb7ae076a1e0f29b011e374a297d6e0b3 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 17:52:39 -0700 Subject: [PATCH 27/38] Move digraph_union to union --- src/lib.rs | 44 +------------------------------------------- src/union.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 096f4e5d08..483cd2dbb4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,7 @@ use simple_path::*; use transitivity::*; use traversal::*; use tree::*; +use union::*; use hashbrown::{HashMap, HashSet}; @@ -73,49 +74,6 @@ pub trait NodesRemoved { fn nodes_removed(&self) -> bool; } -/// 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) -} - /// Color a PyGraph using a largest_first strategy greedy graph coloring. /// /// :param PyGraph: The input PyGraph object to color 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) +} From 24bfce3518ba9cf10b4611963922e4414b625fa1 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 25 Jul 2021 17:57:37 -0700 Subject: [PATCH 28/38] Move graph_greedy_coloring to its own file --- src/coloring.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 56 ++---------------------------------- 2 files changed, 78 insertions(+), 53 deletions(-) create mode 100644 src/coloring.rs diff --git a/src/coloring.rs b/src/coloring.rs new file mode 100644 index 0000000000..a1caada39f --- /dev/null +++ b/src/coloring.rs @@ -0,0 +1,75 @@ +// 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 hashbrown::{HashMap, HashSet}; +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: HashMap = HashMap::new(); + 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/lib.rs b/src/lib.rs index 483cd2dbb4..4cbc484d4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ #![allow(clippy::float_cmp)] +mod coloring; mod connectivity; mod core_number; mod dag_algo; @@ -32,8 +33,7 @@ mod traversal; mod tree; mod union; -use std::cmp::Reverse; - +use coloring::*; use connectivity::*; use core_number::*; use dag_algo::*; @@ -49,12 +49,11 @@ use traversal::*; use tree::*; use union::*; -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashMap; use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; -use pyo3::types::PyDict; use pyo3::wrap_pyfunction; use pyo3::wrap_pymodule; use pyo3::Python; @@ -66,61 +65,12 @@ use petgraph::visit::{ NodeCount, NodeIndexable, }; -use rayon::prelude::*; - use crate::generators::PyInit_generators; pub trait NodesRemoved { fn nodes_removed(&self) -> bool; } -/// 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: HashMap = HashMap::new(); - 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()) -} - pub fn get_edge_iter_with_weights( graph: G, ) -> impl Iterator From c23040eb4117760090bab8744bec08013418dc3c Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Mon, 26 Jul 2021 18:07:43 -0700 Subject: [PATCH 29/38] Move code from traversal to dag_algo --- src/dag_algo/mod.rs | 191 +++++++++++++++++++++++++++++++++++++++++-- src/traversal/mod.rs | 191 +------------------------------------------ 2 files changed, 189 insertions(+), 193 deletions(-) diff --git a/src/dag_algo/mod.rs b/src/dag_algo/mod.rs index ccab01a63d..9125634698 100644 --- a/src/dag_algo/mod.rs +++ b/src/dag_algo/mod.rs @@ -15,8 +15,11 @@ mod longest_path; use hashbrown::{HashMap, HashSet}; +use std::cmp::Ordering; +use std::collections::BinaryHeap; -use crate::digraph; +use super::iterators::NodeIndices; +use crate::{digraph, DAGHasCycle, InvalidNode}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -26,10 +29,7 @@ use pyo3::Python; use petgraph::algo; use petgraph::graph::NodeIndex; use petgraph::prelude::*; - -use super::InvalidNode; - -use super::iterators::NodeIndices; +use petgraph::visit::NodeCount; /// Find the longest path in a DAG /// @@ -307,3 +307,184 @@ pub fn layers( } 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) +} diff --git a/src/traversal/mod.rs b/src/traversal/mod.rs index 70bed95e61..c4b0b0fa21 100644 --- a/src/traversal/mod.rs +++ b/src/traversal/mod.rs @@ -14,44 +14,18 @@ mod dfs_edges; -use super::{digraph, graph, iterators, DAGHasCycle}; +use super::{digraph, graph, iterators}; -use std::cmp::Ordering; -use std::collections::BinaryHeap; - -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashSet; use pyo3::prelude::*; -use pyo3::types::PyList; use pyo3::Python; use petgraph::algo; use petgraph::graph::NodeIndex; use petgraph::visit::{Bfs, NodeCount, Reversed}; -use crate::iterators::{EdgeList, NodeIndices}; - -/// 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(), - }) -} +use crate::iterators::EdgeList; /// Get edge list in depth first order /// @@ -193,162 +167,3 @@ fn descendants(graph: &digraph::PyDiGraph, node: usize) -> HashSet { 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()) -} - -/// 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) -} From 5c37a5e9f9a51fd2879a60ce49dc82212b27b399 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Mon, 26 Jul 2021 18:08:08 -0700 Subject: [PATCH 30/38] Revert "Rename isomorphism and layouts" This reverts commit 22abf8505a740bb8c5f069e261cee8cc0ed8ab87. --- src/{isomorphism_algo => isomorphism}/isomorphism.rs | 0 src/{isomorphism_algo => isomorphism}/mod.rs | 1 + src/{layout_algo => layout}/layout.rs | 0 src/{layout_algo => layout}/mod.rs | 2 ++ src/lib.rs | 8 ++++---- 5 files changed, 7 insertions(+), 4 deletions(-) rename src/{isomorphism_algo => isomorphism}/isomorphism.rs (100%) rename src/{isomorphism_algo => isomorphism}/mod.rs (99%) rename src/{layout_algo => layout}/layout.rs (100%) rename src/{layout_algo => layout}/mod.rs (99%) diff --git a/src/isomorphism_algo/isomorphism.rs b/src/isomorphism/isomorphism.rs similarity index 100% rename from src/isomorphism_algo/isomorphism.rs rename to src/isomorphism/isomorphism.rs diff --git a/src/isomorphism_algo/mod.rs b/src/isomorphism/mod.rs similarity index 99% rename from src/isomorphism_algo/mod.rs rename to src/isomorphism/mod.rs index 334dbfce8b..b625602cda 100644 --- a/src/isomorphism_algo/mod.rs +++ b/src/isomorphism/mod.rs @@ -11,6 +11,7 @@ // under the License. #![allow(clippy::float_cmp)] +#![allow(clippy::module_inception)] mod isomorphism; diff --git a/src/layout_algo/layout.rs b/src/layout/layout.rs similarity index 100% rename from src/layout_algo/layout.rs rename to src/layout/layout.rs diff --git a/src/layout_algo/mod.rs b/src/layout/mod.rs similarity index 99% rename from src/layout_algo/mod.rs rename to src/layout/mod.rs index 30458c4c51..6fe289f6c6 100644 --- a/src/layout_algo/mod.rs +++ b/src/layout/mod.rs @@ -10,6 +10,8 @@ // License for the specific language governing permissions and limitations // under the License. +#![allow(clippy::module_inception)] + mod layout; use crate::{digraph, graph, weight_callable}; diff --git a/src/lib.rs b/src/lib.rs index 4cbc484d4e..13198e4058 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,9 +20,9 @@ mod digraph; mod dot_utils; mod generators; mod graph; -mod isomorphism_algo; +mod isomorphism; mod iterators; -mod layout_algo; +mod layout; mod matching; mod random_circuit; mod representation_algo; @@ -37,8 +37,8 @@ use coloring::*; use connectivity::*; use core_number::*; use dag_algo::*; -use isomorphism_algo::*; -use layout_algo::*; +use isomorphism::*; +use layout::*; use matching::*; use random_circuit::*; use representation_algo::*; From c521fdc12a6fd4e3c2a56066d7be093e5be0d759 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Mon, 26 Jul 2021 18:29:49 -0700 Subject: [PATCH 31/38] Move more items into connectivity module --- src/connectivity.rs | 292 ------------ src/{ => connectivity}/core_number.rs | 51 +-- src/connectivity/mod.rs | 627 ++++++++++++++++++++++++++ src/lib.rs | 6 - src/representation_algo.rs | 197 -------- src/simple_path.rs | 128 ------ 6 files changed, 628 insertions(+), 673 deletions(-) delete mode 100644 src/connectivity.rs rename src/{ => connectivity}/core_number.rs (66%) create mode 100644 src/connectivity/mod.rs delete mode 100644 src/representation_algo.rs delete mode 100644 src/simple_path.rs diff --git a/src/connectivity.rs b/src/connectivity.rs deleted file mode 100644 index bf20036852..0000000000 --- a/src/connectivity.rs +++ /dev/null @@ -1,292 +0,0 @@ -// 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, NullGraph}; - -use hashbrown::{HashMap, HashSet}; -use std::collections::BTreeSet; - -use pyo3::prelude::*; - -use petgraph::algo; -use petgraph::graph::NodeIndex; -use petgraph::visit::NodeCount; - -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()) -} diff --git a/src/core_number.rs b/src/connectivity/core_number.rs similarity index 66% rename from src/core_number.rs rename to src/connectivity/core_number.rs index 1c16b4dbc8..d089d05b26 100644 --- a/src/core_number.rs +++ b/src/connectivity/core_number.rs @@ -12,8 +12,6 @@ #![allow(clippy::float_cmp)] -use super::{digraph, graph}; - use hashbrown::{HashMap, HashSet}; use pyo3::prelude::*; @@ -26,7 +24,7 @@ use petgraph::EdgeType; use rayon::prelude::*; -pub fn _core_number( +pub fn core_number( py: Python, graph: &StableGraph, ) -> PyResult @@ -96,50 +94,3 @@ where } 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) -} 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/lib.rs b/src/lib.rs index 13198e4058..780c6aed01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,6 @@ mod coloring; mod connectivity; -mod core_number; mod dag_algo; mod digraph; mod dot_utils; @@ -25,9 +24,7 @@ mod iterators; mod layout; mod matching; mod random_circuit; -mod representation_algo; mod shortest_path; -mod simple_path; mod transitivity; mod traversal; mod tree; @@ -35,15 +32,12 @@ mod union; use coloring::*; use connectivity::*; -use core_number::*; use dag_algo::*; use isomorphism::*; use layout::*; use matching::*; use random_circuit::*; -use representation_algo::*; use shortest_path::*; -use simple_path::*; use transitivity::*; use traversal::*; use tree::*; diff --git a/src/representation_algo.rs b/src/representation_algo.rs deleted file mode 100644 index ba201b23ec..0000000000 --- a/src/representation_algo.rs +++ /dev/null @@ -1,197 +0,0 @@ -// 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, get_edge_iter_with_weights, graph, weight_callable}; - -use hashbrown::HashSet; - -use pyo3::prelude::*; -use pyo3::Python; - -use petgraph::graph::NodeIndex; -use petgraph::visit::NodeCount; - -use ndarray::prelude::*; -use numpy::IntoPyArray; - -/// 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()) -} - -/// 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) -} diff --git a/src/simple_path.rs b/src/simple_path.rs deleted file mode 100644 index 79366d9421..0000000000 --- a/src/simple_path.rs +++ /dev/null @@ -1,128 +0,0 @@ -// 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, InvalidNode}; - -use pyo3::prelude::*; - -use petgraph::algo; -use petgraph::graph::NodeIndex; - -/// 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) -} From ed7e6ac7d96b10618b3af6dc7ede9b75b31b0186 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Mon, 26 Jul 2021 18:30:55 -0700 Subject: [PATCH 32/38] Rename random_circuit to random_graph --- src/lib.rs | 4 ++-- src/{random_circuit.rs => random_graph.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{random_circuit.rs => random_graph.rs} (100%) diff --git a/src/lib.rs b/src/lib.rs index 780c6aed01..4e6d004813 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ mod isomorphism; mod iterators; mod layout; mod matching; -mod random_circuit; +mod random_graph; mod shortest_path; mod transitivity; mod traversal; @@ -36,7 +36,7 @@ use dag_algo::*; use isomorphism::*; use layout::*; use matching::*; -use random_circuit::*; +use random_graph::*; use shortest_path::*; use transitivity::*; use traversal::*; diff --git a/src/random_circuit.rs b/src/random_graph.rs similarity index 100% rename from src/random_circuit.rs rename to src/random_graph.rs From ea4bd1813499f7f77054f06a79597cc39cf87b27 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 5 Aug 2021 19:37:11 -0700 Subject: [PATCH 33/38] Avoid module inception in isomorphism.rs --- src/isomorphism/mod.rs | 11 +++++------ src/isomorphism/{isomorphism.rs => vf2.rs} | 0 2 files changed, 5 insertions(+), 6 deletions(-) rename src/isomorphism/{isomorphism.rs => vf2.rs} (100%) diff --git a/src/isomorphism/mod.rs b/src/isomorphism/mod.rs index b625602cda..125ff2c623 100644 --- a/src/isomorphism/mod.rs +++ b/src/isomorphism/mod.rs @@ -11,9 +11,8 @@ // under the License. #![allow(clippy::float_cmp)] -#![allow(clippy::module_inception)] -mod isomorphism; +mod vf2; use crate::{digraph, graph}; @@ -81,7 +80,7 @@ fn digraph_is_isomorphic( } }); - let res = isomorphism::is_isomorphic( + let res = vf2::is_isomorphic( py, &first.graph, &second.graph, @@ -153,7 +152,7 @@ fn graph_is_isomorphic( } }); - let res = isomorphism::is_isomorphic( + let res = vf2::is_isomorphic( py, &first.graph, &second.graph, @@ -233,7 +232,7 @@ fn digraph_is_subgraph_isomorphic( } }); - let res = isomorphism::is_isomorphic( + let res = vf2::is_isomorphic( py, &first.graph, &second.graph, @@ -313,7 +312,7 @@ fn graph_is_subgraph_isomorphic( } }); - let res = isomorphism::is_isomorphic( + let res = vf2::is_isomorphic( py, &first.graph, &second.graph, diff --git a/src/isomorphism/isomorphism.rs b/src/isomorphism/vf2.rs similarity index 100% rename from src/isomorphism/isomorphism.rs rename to src/isomorphism/vf2.rs From 955a6bd68a8659d996acf80e4db3bfce87fabe20 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 5 Aug 2021 21:14:26 -0700 Subject: [PATCH 34/38] Avoid module inception in layout.rs --- src/layout/bipartite_layout.rs | 105 ++++++++ src/layout/circular_layout.rs | 52 ++++ src/layout/mod.rs | 198 ++++----------- src/layout/random_layout.rs | 51 ++++ src/layout/shell_layout.rs | 80 ++++++ src/layout/spiral_layout.rs | 68 ++++++ src/layout/{layout.rs => spring_layout.rs} | 267 ++++++--------------- 7 files changed, 471 insertions(+), 350 deletions(-) create mode 100644 src/layout/bipartite_layout.rs create mode 100644 src/layout/circular_layout.rs create mode 100644 src/layout/random_layout.rs create mode 100644 src/layout/shell_layout.rs create mode 100644 src/layout/spiral_layout.rs rename src/layout/{layout.rs => spring_layout.rs} (55%) diff --git a/src/layout/bipartite_layout.rs b/src/layout/bipartite_layout.rs new file mode 100644 index 0000000000..b799a1e205 --- /dev/null +++ b/src/layout/bipartite_layout.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_layout::{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_layout.rs b/src/layout/circular_layout.rs new file mode 100644 index 0000000000..2045a6c591 --- /dev/null +++ b/src/layout/circular_layout.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_layout::{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 index 6fe289f6c6..db5f92fca3 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -10,115 +10,23 @@ // License for the specific language governing permissions and limitations // under the License. -#![allow(clippy::module_inception)] +mod bipartite_layout; +mod circular_layout; +mod random_layout; +mod shell_layout; +mod spiral_layout; +mod spring_layout; -mod layout; - -use crate::{digraph, graph, weight_callable}; +use crate::{digraph, graph}; +use spring_layout::Point; use hashbrown::{HashMap, HashSet}; -use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::Python; -use petgraph::prelude::*; -use petgraph::visit::{IntoEdgeReferences, NodeIndexable}; -use petgraph::EdgeType; - -use rand::distributions::{Distribution, Uniform}; -use rand::prelude::*; -use rand_pcg::Pcg64; - use crate::iterators::Pos2DMapping; -#[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 @@ -171,7 +79,7 @@ where pub fn graph_spring_layout( py: Python, graph: &graph::PyGraph, - pos: Option>, + pos: Option>, fixed: Option>, k: Option, repulsive_exponent: Option, @@ -181,10 +89,10 @@ pub fn graph_spring_layout( weight_fn: Option, default_weight: f64, scale: Option, - center: Option, + center: Option, seed: Option, ) -> PyResult { - _spring_layout( + spring_layout::spring_layout( py, &graph.graph, pos, @@ -254,7 +162,7 @@ pub fn graph_spring_layout( pub fn digraph_spring_layout( py: Python, graph: &digraph::PyDiGraph, - pos: Option>, + pos: Option>, fixed: Option>, k: Option, repulsive_exponent: Option, @@ -264,10 +172,10 @@ pub fn digraph_spring_layout( weight_fn: Option, default_weight: f64, scale: Option, - center: Option, + center: Option, seed: Option, ) -> PyResult { - _spring_layout( + spring_layout::spring_layout( py, &graph.graph, pos, @@ -285,36 +193,6 @@ pub fn digraph_spring_layout( ) } -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 @@ -331,7 +209,7 @@ pub fn graph_random_layout( center: Option<[f64; 2]>, seed: Option, ) -> Pos2DMapping { - _random_layout(&graph.graph, center, seed) + random_layout::random_layout(&graph.graph, center, seed) } /// Generate a random layout @@ -350,7 +228,7 @@ pub fn digraph_random_layout( center: Option<[f64; 2]>, seed: Option, ) -> Pos2DMapping { - _random_layout(&graph.graph, center, seed) + random_layout::random_layout(&graph.graph, center, seed) } /// Generate a bipartite layout of the graph @@ -376,10 +254,10 @@ pub fn graph_bipartite_layout( first_nodes: HashSet, horizontal: Option, scale: Option, - center: Option, + center: Option, aspect_ratio: Option, ) -> Pos2DMapping { - layout::bipartite_layout( + bipartite_layout::bipartite_layout( &graph.graph, first_nodes, horizontal, @@ -412,10 +290,10 @@ pub fn digraph_bipartite_layout( first_nodes: HashSet, horizontal: Option, scale: Option, - center: Option, + center: Option, aspect_ratio: Option, ) -> Pos2DMapping { - layout::bipartite_layout( + bipartite_layout::bipartite_layout( &graph.graph, first_nodes, horizontal, @@ -439,9 +317,9 @@ pub fn digraph_bipartite_layout( pub fn graph_circular_layout( graph: &graph::PyGraph, scale: Option, - center: Option, + center: Option, ) -> Pos2DMapping { - layout::circular_layout(&graph.graph, scale, center) + circular_layout::circular_layout(&graph.graph, scale, center) } /// Generate a circular layout of the graph @@ -458,9 +336,9 @@ pub fn graph_circular_layout( pub fn digraph_circular_layout( graph: &digraph::PyDiGraph, scale: Option, - center: Option, + center: Option, ) -> Pos2DMapping { - layout::circular_layout(&graph.graph, scale, center) + circular_layout::circular_layout(&graph.graph, scale, center) } /// Generate a shell layout of the graph @@ -485,9 +363,9 @@ pub fn graph_shell_layout( nlist: Option>>, rotate: Option, scale: Option, - center: Option, + center: Option, ) -> Pos2DMapping { - layout::shell_layout(&graph.graph, nlist, rotate, scale, center) + shell_layout::shell_layout(&graph.graph, nlist, rotate, scale, center) } /// Generate a shell layout of the graph @@ -511,9 +389,9 @@ pub fn digraph_shell_layout( nlist: Option>>, rotate: Option, scale: Option, - center: Option, + center: Option, ) -> Pos2DMapping { - layout::shell_layout(&graph.graph, nlist, rotate, scale, center) + shell_layout::shell_layout(&graph.graph, nlist, rotate, scale, center) } /// Generate a spiral layout of the graph @@ -535,11 +413,17 @@ pub fn digraph_shell_layout( pub fn graph_spiral_layout( graph: &graph::PyGraph, scale: Option, - center: Option, + center: Option, resolution: Option, equidistant: Option, ) -> Pos2DMapping { - layout::spiral_layout(&graph.graph, scale, center, resolution, equidistant) + spiral_layout::spiral_layout( + &graph.graph, + scale, + center, + resolution, + equidistant, + ) } /// Generate a spiral layout of the graph @@ -561,9 +445,15 @@ pub fn graph_spiral_layout( pub fn digraph_spiral_layout( graph: &digraph::PyDiGraph, scale: Option, - center: Option, + center: Option, resolution: Option, equidistant: Option, ) -> Pos2DMapping { - layout::spiral_layout(&graph.graph, scale, center, resolution, equidistant) + spiral_layout::spiral_layout( + &graph.graph, + scale, + center, + resolution, + equidistant, + ) } diff --git a/src/layout/random_layout.rs b/src/layout/random_layout.rs new file mode 100644 index 0000000000..e58387f5c3 --- /dev/null +++ b/src/layout/random_layout.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_layout.rs b/src/layout/shell_layout.rs new file mode 100644 index 0000000000..561ae5f4ba --- /dev/null +++ b/src/layout/shell_layout.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_layout::{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_layout.rs b/src/layout/spiral_layout.rs new file mode 100644 index 0000000000..df7d4927b7 --- /dev/null +++ b/src/layout/spiral_layout.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_layout::{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/layout.rs b/src/layout/spring_layout.rs similarity index 55% rename from src/layout/layout.rs rename to src/layout/spring_layout.rs index 4e0a26ff3a..62a6aa27c6 100644 --- a/src/layout/layout.rs +++ b/src/layout/spring_layout.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(), - } + }) } From cc7581757765ad8bc26bf96c4ca0effaa19f32e8 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 5 Aug 2021 21:33:01 -0700 Subject: [PATCH 35/38] Simplify names in layout module --- .../{bipartite_layout.rs => bipartite.rs} | 2 +- .../{circular_layout.rs => circular.rs} | 2 +- src/layout/mod.rs | 50 +++++++------------ src/layout/{random_layout.rs => random.rs} | 0 src/layout/{shell_layout.rs => shell.rs} | 2 +- src/layout/{spiral_layout.rs => spiral.rs} | 2 +- src/layout/{spring_layout.rs => spring.rs} | 0 7 files changed, 23 insertions(+), 35 deletions(-) rename src/layout/{bipartite_layout.rs => bipartite.rs} (98%) rename src/layout/{circular_layout.rs => circular.rs} (96%) rename src/layout/{random_layout.rs => random.rs} (100%) rename src/layout/{shell_layout.rs => shell.rs} (98%) rename src/layout/{spiral_layout.rs => spiral.rs} (97%) rename src/layout/{spring_layout.rs => spring.rs} (100%) diff --git a/src/layout/bipartite_layout.rs b/src/layout/bipartite.rs similarity index 98% rename from src/layout/bipartite_layout.rs rename to src/layout/bipartite.rs index b799a1e205..25287ea996 100644 --- a/src/layout/bipartite_layout.rs +++ b/src/layout/bipartite.rs @@ -19,7 +19,7 @@ use pyo3::prelude::*; use petgraph::prelude::*; use petgraph::EdgeType; -use super::spring_layout::{recenter, rescale, Point}; +use super::spring::{recenter, rescale, Point}; use crate::iterators::Pos2DMapping; pub fn bipartite_layout( diff --git a/src/layout/circular_layout.rs b/src/layout/circular.rs similarity index 96% rename from src/layout/circular_layout.rs rename to src/layout/circular.rs index 2045a6c591..b770ad9a75 100644 --- a/src/layout/circular_layout.rs +++ b/src/layout/circular.rs @@ -17,7 +17,7 @@ use pyo3::prelude::*; use petgraph::prelude::*; use petgraph::EdgeType; -use super::spring_layout::{recenter, rescale, Point}; +use super::spring::{recenter, rescale, Point}; use crate::iterators::Pos2DMapping; pub fn circular_layout( diff --git a/src/layout/mod.rs b/src/layout/mod.rs index db5f92fca3..4f9ce3bb43 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -10,15 +10,15 @@ // License for the specific language governing permissions and limitations // under the License. -mod bipartite_layout; -mod circular_layout; -mod random_layout; -mod shell_layout; -mod spiral_layout; -mod spring_layout; +mod bipartite; +mod circular; +mod random; +mod shell; +mod spiral; +mod spring; use crate::{digraph, graph}; -use spring_layout::Point; +use spring::Point; use hashbrown::{HashMap, HashSet}; @@ -92,7 +92,7 @@ pub fn graph_spring_layout( center: Option, seed: Option, ) -> PyResult { - spring_layout::spring_layout( + spring::spring_layout( py, &graph.graph, pos, @@ -175,7 +175,7 @@ pub fn digraph_spring_layout( center: Option, seed: Option, ) -> PyResult { - spring_layout::spring_layout( + spring::spring_layout( py, &graph.graph, pos, @@ -209,7 +209,7 @@ pub fn graph_random_layout( center: Option<[f64; 2]>, seed: Option, ) -> Pos2DMapping { - random_layout::random_layout(&graph.graph, center, seed) + random::random_layout(&graph.graph, center, seed) } /// Generate a random layout @@ -228,7 +228,7 @@ pub fn digraph_random_layout( center: Option<[f64; 2]>, seed: Option, ) -> Pos2DMapping { - random_layout::random_layout(&graph.graph, center, seed) + random::random_layout(&graph.graph, center, seed) } /// Generate a bipartite layout of the graph @@ -257,7 +257,7 @@ pub fn graph_bipartite_layout( center: Option, aspect_ratio: Option, ) -> Pos2DMapping { - bipartite_layout::bipartite_layout( + bipartite::bipartite_layout( &graph.graph, first_nodes, horizontal, @@ -293,7 +293,7 @@ pub fn digraph_bipartite_layout( center: Option, aspect_ratio: Option, ) -> Pos2DMapping { - bipartite_layout::bipartite_layout( + bipartite::bipartite_layout( &graph.graph, first_nodes, horizontal, @@ -319,7 +319,7 @@ pub fn graph_circular_layout( scale: Option, center: Option, ) -> Pos2DMapping { - circular_layout::circular_layout(&graph.graph, scale, center) + circular::circular_layout(&graph.graph, scale, center) } /// Generate a circular layout of the graph @@ -338,7 +338,7 @@ pub fn digraph_circular_layout( scale: Option, center: Option, ) -> Pos2DMapping { - circular_layout::circular_layout(&graph.graph, scale, center) + circular::circular_layout(&graph.graph, scale, center) } /// Generate a shell layout of the graph @@ -365,7 +365,7 @@ pub fn graph_shell_layout( scale: Option, center: Option, ) -> Pos2DMapping { - shell_layout::shell_layout(&graph.graph, nlist, rotate, scale, center) + shell::shell_layout(&graph.graph, nlist, rotate, scale, center) } /// Generate a shell layout of the graph @@ -391,7 +391,7 @@ pub fn digraph_shell_layout( scale: Option, center: Option, ) -> Pos2DMapping { - shell_layout::shell_layout(&graph.graph, nlist, rotate, scale, center) + shell::shell_layout(&graph.graph, nlist, rotate, scale, center) } /// Generate a spiral layout of the graph @@ -417,13 +417,7 @@ pub fn graph_spiral_layout( resolution: Option, equidistant: Option, ) -> Pos2DMapping { - spiral_layout::spiral_layout( - &graph.graph, - scale, - center, - resolution, - equidistant, - ) + spiral::spiral_layout(&graph.graph, scale, center, resolution, equidistant) } /// Generate a spiral layout of the graph @@ -449,11 +443,5 @@ pub fn digraph_spiral_layout( resolution: Option, equidistant: Option, ) -> Pos2DMapping { - spiral_layout::spiral_layout( - &graph.graph, - scale, - center, - resolution, - equidistant, - ) + spiral::spiral_layout(&graph.graph, scale, center, resolution, equidistant) } diff --git a/src/layout/random_layout.rs b/src/layout/random.rs similarity index 100% rename from src/layout/random_layout.rs rename to src/layout/random.rs diff --git a/src/layout/shell_layout.rs b/src/layout/shell.rs similarity index 98% rename from src/layout/shell_layout.rs rename to src/layout/shell.rs index 561ae5f4ba..8bb2134053 100644 --- a/src/layout/shell_layout.rs +++ b/src/layout/shell.rs @@ -18,7 +18,7 @@ use petgraph::prelude::*; use petgraph::visit::NodeIndexable; use petgraph::EdgeType; -use super::spring_layout::{recenter, Point}; +use super::spring::{recenter, Point}; use crate::iterators::Pos2DMapping; pub fn shell_layout( diff --git a/src/layout/spiral_layout.rs b/src/layout/spiral.rs similarity index 97% rename from src/layout/spiral_layout.rs rename to src/layout/spiral.rs index df7d4927b7..bad44af0e7 100644 --- a/src/layout/spiral_layout.rs +++ b/src/layout/spiral.rs @@ -17,7 +17,7 @@ use pyo3::prelude::*; use petgraph::prelude::*; use petgraph::EdgeType; -use super::spring_layout::{recenter, rescale, Point}; +use super::spring::{recenter, rescale, Point}; use crate::iterators::Pos2DMapping; pub fn spiral_layout( diff --git a/src/layout/spring_layout.rs b/src/layout/spring.rs similarity index 100% rename from src/layout/spring_layout.rs rename to src/layout/spring.rs From e55d07e4d7216fcd383434d02e710ed01b38b502 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 5 Aug 2021 23:19:30 -0700 Subject: [PATCH 36/38] Add details to CONTRIBUTING.md --- CONTRIBUTING.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9594184e37..0799b6965c 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, /)")] +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 first contribution? +> Contact us at the [\#retworkx channel in Qiskit's Slack](https://qiskit.slack.com/messages/retworkx/) + ### Tests Once you've made a code change, it is important to verify that your change From bb63b2ea8a92e9d9d4c74903b4c0277a8119d47a Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Fri, 6 Aug 2021 00:49:00 -0700 Subject: [PATCH 37/38] Fix tox -edocs issues --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0799b6965c..4c07fd9e33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ or create a new one like `your_module.rs`. Then, start with the boilerplate bell /// Docstring containing description of the function #[pyfunction] #[pyo3(text_signature = "(graph, /)")] -fn your_new_function( +pub fn your_new_function( py: Python, graph: &graph::PyGraph, ) -> PyResult<()> { @@ -63,10 +63,10 @@ fn your_new_function( } ``` -> **_NOTE:_** If you create a new `your_module.rs`, remember to declare and import it in `lib.rs`: +> __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::*; +> use your_module::your_new_function; > ``` #### Module directories: when a single file is not enough @@ -79,8 +79,8 @@ Module directories have a `mod.rs` file containing the pyfunctions. The pyfuncti 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 first contribution? -> Contact us at the [\#retworkx channel in Qiskit's Slack](https://qiskit.slack.com/messages/retworkx/) +> __NOTE:__ Do you still have questions about making your first contribution? +> Contact us at the [\#retworkx channel in Qiskit Slack](https://qiskit.slack.com/messages/retworkx/) ### Tests From bf7e3febf9589bc8b62f61cc50bd6ab4fec4181d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 6 Aug 2021 09:32:41 -0400 Subject: [PATCH 38/38] Minor CONTRIBUTING.md fixes --- CONTRIBUTING.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c07fd9e33..b006237e30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,8 +16,8 @@ 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 +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: @@ -39,8 +39,8 @@ 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 +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 @@ -48,7 +48,7 @@ annotation gives them power to interact both with the Python interpreter and pur 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 +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 @@ -66,7 +66,7 @@ pub fn your_new_function( > __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::your_new_function; +> use your_module::*; > ``` #### Module directories: when a single file is not enough @@ -76,10 +76,10 @@ 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 +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 first contribution? +> __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