From 1822fc3cdfcc0f30ff45c1a9e5bca0232e0dcfde Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 27 Mar 2022 13:43:34 -0400 Subject: [PATCH 01/11] Add function to find densest subgraph This commit adds a new function find_densest_subgraph_of_size() to retworkx. This function is used to find a subgraph in a given graph of a specified size that has the highest degree of connectivity of nodes. Fixes #570 --- docs/source/api.rst | 1 + .../densest_subgraph-1b068f69f80facd4.yaml | 31 +++ retworkx/__init__.py | 34 +++ src/dense_subgraph.rs | 213 ++++++++++++++++++ src/lib.rs | 4 + tests/graph/test_densest_subgraph.py | 31 +++ 6 files changed, 314 insertions(+) create mode 100644 releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml create mode 100644 src/dense_subgraph.rs create mode 100644 tests/graph/test_densest_subgraph.py diff --git a/docs/source/api.rst b/docs/source/api.rst index 6dc8117e8..339424a33 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -155,6 +155,7 @@ Connectivity and Cycles retworkx.chain_decomposition retworkx.all_simple_paths retworkx.all_pairs_all_simple_paths + retworkx.densest_subgraph_of_size .. _graph-ops: diff --git a/releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml b/releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml new file mode 100644 index 000000000..cfa209d12 --- /dev/null +++ b/releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + Added a new function, :func:`~.densest_subgraph_of_size`, which is used to return a + subgraph of given size that has the highest degree of connecitivity between the nodes. + For example, if you wanted to find the subgraph of 5 nodes in a 19 node heavy hexagon + graph: + + .. jupyter-execute:: + + import retworkx + from retworkx.visualization import mpl_draw + + graph = retworkx.generators.hexagonal_lattice_graph(4, 5) + + subgraph, node_map = retworkx.densest_subgraph_of_size(graph, 5) + subgraph_edge_set = set(subgraph.edge_list()) + node_colors = [] + for node in graph.node_indices(): + if node in node_map: + node_colors.append('red') + else: + node_colors.append('blue') + graph[node] = node + edge_colors = [] + for edge in graph.edge_list(): + if edge[0] in node_map and edge[1] in node_map: + edge_colors.append('red') + else: + edge_colors.append('blue') + mpl_draw(graph, with_labels=True, node_color=node_colors, edge_color=edge_colors, labels=str) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 1be867d87..92036828f 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -2334,3 +2334,37 @@ def _digraph_all_pairs_bellman_ford_shortest_path(graph, edge_cost_fn): @all_pairs_bellman_ford_shortest_paths.register(PyGraph) def _graph_all_pairs_bellman_ford_shortest_path(graph, edge_cost_fn): return graph_all_pairs_bellman_ford_shortest_paths(graph, edge_cost_fn) + + +@functools.singledispatch +def densest_subgraph_of_size(graph, num_nodes, weight_callback=None): + """Find densest subgraph in a :class:`~.PyGraph` + + This method does not provide any guarantees on the approximation as it + does a naive search using BFS traversal. + + :param graph: The graph to find the densest subgraph in. This can be a + :class:`~retworkx.PyGraph` or a :class:`~retworkx.PyDiGraph`. + :param int num_nodes: The number of nodes in the subgraph to find + :param func weight_callback: An optional callable that if specified will be + passed the node indices of each edge in the graph and it is expected to + return a float value. If specified the lowest avg weight for edges in + a found subgraph will be a criteria for selection in addition to the + connectivity of the subgraph. + :returns: A tuple of the subgraph found and a :class:`~.NodeMap` of the + mapping of node indices in the input ``graph`` to the index in the + output subgraph. + + :rtype: (subgraph, node_map) + """ + raise TypeError("Invalid Input Type %s for graph" % type(graph)) + + +@densest_subgraph_of_size.register(PyDiGraph) +def _digraph_densest_subgraph_of_size(graph, num_nodes, weight_callback=None): + return digraph_densest_subgraph_of_size(graph, num_nodes, weight_callback=weight_callback) + + +@densest_subgraph_of_size.register(PyGraph) +def _graph_densest_subgraph_of_size(graph, num_nodes, weight_callback=None): + return graph_densest_subgraph_of_size(graph, num_nodes, weight_callback=weight_callback) diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs new file mode 100644 index 000000000..b33eb9ff8 --- /dev/null +++ b/src/dense_subgraph.rs @@ -0,0 +1,213 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +use hashbrown::{HashMap, HashSet}; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::prelude::*; +use petgraph::visit::{IntoEdgeReferences, NodeFiltered}; +use petgraph::EdgeType; + +use rayon::prelude::*; + +use pyo3::prelude::*; +use pyo3::Python; + +use retworkx_core::dictmap::*; + +use crate::digraph; +use crate::graph; +use crate::iterators::NodeMap; +use crate::StablePyGraph; + +struct SubsetResult { + pub count: usize, + pub error: f64, + pub map: Vec, + pub subgraph: Vec<[NodeIndex; 2]>, +} + +pub fn densest_subgraph( + py: Python, + graph: &StablePyGraph, + num_nodes: usize, + weight_callback: Option, +) -> PyResult<(StablePyGraph, NodeMap)> +where + Ty: EdgeType + Sync, +{ + let node_indices: Vec = graph.node_indices().collect(); + let float_callback = + |callback: PyObject, source_node: usize, target_node: usize| -> PyResult { + let res = callback.as_ref(py).call1((source_node, target_node))?; + res.extract() + }; + let mut weight_map: Option> = None; + + if weight_callback.is_some() { + let mut inner_weight_map: HashMap<[NodeIndex; 2], f64> = + HashMap::with_capacity(graph.edge_count()); + let callback = weight_callback.as_ref().unwrap(); + for edge in graph.edge_references() { + let source: NodeIndex = edge.source(); + let target: NodeIndex = edge.target(); + let weight = float_callback(callback.clone_ref(py), source.index(), target.index())?; + inner_weight_map.insert([source, target], weight); + } + weight_map = Some(inner_weight_map); + } + let reduce_identity_fn = || -> SubsetResult { + SubsetResult { + count: 0, + map: Vec::new(), + error: std::f64::INFINITY, + subgraph: Vec::new(), + } + }; + + let reduce_fn = |best: SubsetResult, curr: SubsetResult| -> SubsetResult { + if weight_callback.is_some() { + if curr.count >= best.count && curr.error <= best.error { + curr + } else { + best + } + } else if curr.count > best.count { + curr + } else { + best + } + }; + + let best_result = node_indices + .into_par_iter() + .map(|index| { + let mut subgraph: Vec<[NodeIndex; 2]> = Vec::with_capacity(num_nodes); + let mut bfs = Bfs::new(&graph, index); + let mut bfs_vec: Vec = Vec::with_capacity(num_nodes); + let mut bfs_set: HashSet = HashSet::with_capacity(num_nodes); + + let mut count = 0; + while let Some(node) = bfs.next(&graph) { + bfs_vec.push(node); + bfs_set.insert(node); + count += 1; + if count >= num_nodes { + break; + } + } + let mut connection_count = 0; + for node in &bfs_vec { + for j in graph.node_indices().filter(|j| bfs_set.contains(j)) { + if graph.contains_edge(*node, j) { + connection_count += 1; + subgraph.push([*node, j]); + } + } + } + let error = match &weight_map { + Some(map) => subgraph.iter().map(|edge| map[edge]).sum::() / num_nodes as f64, + None => 0., + }; + SubsetResult { + count: connection_count, + error, + map: bfs_vec, + subgraph, + } + }) + .reduce(reduce_identity_fn, reduce_fn); + + let mut subgraph = StablePyGraph::::with_capacity(num_nodes, best_result.subgraph.len()); + let mut node_map: DictMap = DictMap::with_capacity(num_nodes); + for node in best_result.map { + let new_index = subgraph.add_node(graph[node].clone_ref(py)); + node_map.insert(node.index(), new_index.index()); + } + let node_filter = |node: NodeIndex| -> bool { node_map.contains_key(&node.index()) }; + let filtered = NodeFiltered(graph, node_filter); + for edge in filtered.edge_references() { + let new_source = NodeIndex::new(*node_map.get(&edge.source().index()).unwrap()); + let new_target = NodeIndex::new(*node_map.get(&edge.target().index()).unwrap()); + subgraph.add_edge(new_source, new_target, edge.weight().clone_ref(py)); + } + Ok((subgraph, NodeMap { node_map })) +} + +/// Find densest subgraph in a :class:`~.PyGraph` +/// +/// This method does not provide any guarantees on the approximation as it +/// does a naive search using BFS traversal. +/// +/// :param PyGraph graph: The graph to find densest subgraph in. +/// :param int num_nodes: The number of nodes in the subgraph to find +/// :param func weight_callback: An optional callable that if specified will be +/// passed the node indices of each edge in the graph and it is expected to +/// return a float value. If specified the lowest avg weight for edges in +/// a found subgraph will be a criteria for selection in addition to the +/// connectivity of the subgraph. +/// :returns: A tuple of the subgraph found and a :class:`~.NodeMap` of the +/// mapping of node indices in the input ``graph`` to the index in the +/// output subgraph. +/// :rtype: (PyGraph, NodeMap) +#[pyfunction] +#[pyo3(text_signature = "(graph. num_nodes, /, weight_callback=None)")] +pub fn graph_densest_subgraph_of_size( + py: Python, + graph: &graph::PyGraph, + num_nodes: usize, + weight_callback: Option, +) -> PyResult<(graph::PyGraph, NodeMap)> { + let (inner_graph, node_map) = densest_subgraph(py, &graph.graph, num_nodes, weight_callback)?; + let out_graph = graph::PyGraph { + graph: inner_graph, + node_removed: false, + multigraph: graph.multigraph, + }; + Ok((out_graph, node_map)) +} + +/// Find densest subgraph in a :class:`~.PyDiGraph` +/// +/// This method does not provide any guarantees on the approximation as it +/// does a naive search using BFS traversal. +/// +/// :param PyDiGraph graph: The graph to find the densest subgraph in. +/// :param int num_nodes: The number of nodes in the subgraph to find +/// :param func weight_callback: An optional callable that if specified will be +/// passed the node indices of each edge in the graph and it is expected to +/// return a float value. If specified the lowest avg weight for edges in +/// a found subgraph will be a criteria for selection in addition to the +/// connectivity of the subgraph. +/// :returns: A tuple of the subgraph found and a :class:`~.NodeMap` of the +/// mapping of node indices in the input ``graph`` to the index in the +/// output subgraph. +/// :rtype: (PyDiGraph, NodeMap) +#[pyfunction] +#[pyo3(text_signature = "(graph. num_nodes, /, weight_callback=None)")] +pub fn digraph_densest_subgraph_of_size( + py: Python, + graph: &digraph::PyDiGraph, + num_nodes: usize, + weight_callback: Option, +) -> PyResult<(digraph::PyDiGraph, NodeMap)> { + let (inner_graph, node_map) = densest_subgraph(py, &graph.graph, num_nodes, weight_callback)?; + let out_graph = digraph::PyDiGraph { + graph: inner_graph, + node_removed: false, + cycle_state: algo::DfsSpace::default(), + check_cycle: graph.check_cycle, + multigraph: graph.multigraph, + }; + Ok((out_graph, node_map)) +} diff --git a/src/lib.rs b/src/lib.rs index de051ed7d..0b74ab6aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ mod centrality; mod coloring; mod connectivity; mod dag_algo; +mod dense_subgraph; mod digraph; mod dot_utils; mod generators; @@ -39,6 +40,7 @@ use centrality::*; use coloring::*; use connectivity::*; use dag_algo::*; +use dense_subgraph::*; use graphml::*; use isomorphism::*; use layout::*; @@ -461,6 +463,8 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(biconnected_components))?; m.add_wrapped(wrap_pyfunction!(chain_decomposition))?; m.add_wrapped(wrap_pyfunction!(read_graphml))?; + m.add_wrapped(wrap_pyfunction!(digraph_densest_subgraph_of_size))?; + m.add_wrapped(wrap_pyfunction!(graph_densest_subgraph_of_size))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/tests/graph/test_densest_subgraph.py b/tests/graph/test_densest_subgraph.py new file mode 100644 index 000000000..b8041aa31 --- /dev/null +++ b/tests/graph/test_densest_subgraph.py @@ -0,0 +1,31 @@ +# 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. + +import unittest + +import retworkx + + +class TestDensestSubgraph(unittest.TestCase): + def test_simple_grid_three_nodes(self): + graph = retworkx.generators.grid_graph(3, 3) + subgraph, node_map = retworkx.densest_subgraph_of_size(graph, 3) + expected_subgraph_edge_list = [(0, 2), (0, 1)] + self.assertEqual(expected_subgraph_edge_list, subgraph.edge_list()) + self.assertEqual(node_map, {0: 0, 1: 1, 3: 2}) + + def test_simple_grid_six_nodes(self): + graph = retworkx.generators.grid_graph(3, 3) + subgraph, node_map = retworkx.densest_subgraph_of_size(graph, 6) + expected_subgraph_edge_list = [(5, 2), (5, 3), (3, 0), (3, 4), (4, 1), (2, 0), (0, 1)] + self.assertEqual(expected_subgraph_edge_list, subgraph.edge_list()) + self.assertEqual(node_map, {7: 0, 8: 1, 6: 2, 4: 3, 5: 4, 3: 5}) From 2354801e4daf869ac00ea7fe659bfc8dba9a1bb7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 28 Jun 2022 16:24:59 -0400 Subject: [PATCH 02/11] Update for rebase errors --- src/dense_subgraph.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs index b33eb9ff8..0765070fa 100644 --- a/src/dense_subgraph.rs +++ b/src/dense_subgraph.rs @@ -173,6 +173,7 @@ pub fn graph_densest_subgraph_of_size( graph: inner_graph, node_removed: false, multigraph: graph.multigraph, + attrs: py.None(), }; Ok((out_graph, node_map)) } @@ -208,6 +209,7 @@ pub fn digraph_densest_subgraph_of_size( cycle_state: algo::DfsSpace::default(), check_cycle: graph.check_cycle, multigraph: graph.multigraph, + attrs: py.None(), }; Ok((out_graph, node_map)) } From 755cec8973f31864946a373d5f79116af8b8b205 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 19 Sep 2022 15:58:23 -0400 Subject: [PATCH 03/11] Apply suggestions from code review Co-authored-by: georgios-ts <45130028+georgios-ts@users.noreply.github.com> --- retworkx/__init__.py | 4 ++-- src/dense_subgraph.rs | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 92036828f..7ff9cbf7d 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -2338,9 +2338,9 @@ def _graph_all_pairs_bellman_ford_shortest_path(graph, edge_cost_fn): @functools.singledispatch def densest_subgraph_of_size(graph, num_nodes, weight_callback=None): - """Find densest subgraph in a :class:`~.PyGraph` + """Find a connected and dense subgraph of a given size in a graph. - This method does not provide any guarantees on the approximation as it + This method does not provide any guarantees on the approximation of the optimal solution as it does a naive search using BFS traversal. :param graph: The graph to find the densest subgraph in. This can be a diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs index 0765070fa..5952a5e3f 100644 --- a/src/dense_subgraph.rs +++ b/src/dense_subgraph.rs @@ -48,7 +48,7 @@ where { let node_indices: Vec = graph.node_indices().collect(); let float_callback = - |callback: PyObject, source_node: usize, target_node: usize| -> PyResult { + |callback: &PyObject, source_node: usize, target_node: usize| -> PyResult { let res = callback.as_ref(py).call1((source_node, target_node))?; res.extract() }; @@ -61,7 +61,7 @@ where for edge in graph.edge_references() { let source: NodeIndex = edge.source(); let target: NodeIndex = edge.target(); - let weight = float_callback(callback.clone_ref(py), source.index(), target.index())?; + let weight = float_callback(callback, source.index(), target.index())?; inner_weight_map.insert([source, target], weight); } weight_map = Some(inner_weight_map); @@ -108,15 +108,13 @@ where } let mut connection_count = 0; for node in &bfs_vec { - for j in graph.node_indices().filter(|j| bfs_set.contains(j)) { - if graph.contains_edge(*node, j) { - connection_count += 1; - subgraph.push([*node, j]); - } + for nbr in graph.neighbors(*node).filter(|j| bfs_set.contains(j)) { + connection_count += 1; + subgraph.push([*node, nbr]); } } let error = match &weight_map { - Some(map) => subgraph.iter().map(|edge| map[edge]).sum::() / num_nodes as f64, + Some(map) => subgraph.iter().map(|edge| map[edge]).sum::() / subgraph.len() as f64, None => 0., }; SubsetResult { @@ -161,7 +159,7 @@ where /// output subgraph. /// :rtype: (PyGraph, NodeMap) #[pyfunction] -#[pyo3(text_signature = "(graph. num_nodes, /, weight_callback=None)")] +#[pyo3(text_signature = "(graph, num_nodes, /, weight_callback=None)")] pub fn graph_densest_subgraph_of_size( py: Python, graph: &graph::PyGraph, @@ -195,7 +193,7 @@ pub fn graph_densest_subgraph_of_size( /// output subgraph. /// :rtype: (PyDiGraph, NodeMap) #[pyfunction] -#[pyo3(text_signature = "(graph. num_nodes, /, weight_callback=None)")] +#[pyo3(text_signature = "(graph, num_nodes, /, weight_callback=None)")] pub fn digraph_densest_subgraph_of_size( py: Python, graph: &digraph::PyDiGraph, From 1dc2aec0ef31ea12f6fff4334a71e3bcdf5d2e0b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 19 Sep 2022 17:17:14 -0400 Subject: [PATCH 04/11] Update imports in release notes --- releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml b/releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml index cfa209d12..a83383f55 100644 --- a/releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml +++ b/releasenotes/notes/densest_subgraph-1b068f69f80facd4.yaml @@ -8,12 +8,12 @@ features: .. jupyter-execute:: - import retworkx - from retworkx.visualization import mpl_draw + import rustworkx as rx + from rustworkx.visualization import mpl_draw - graph = retworkx.generators.hexagonal_lattice_graph(4, 5) + graph = rx.generators.hexagonal_lattice_graph(4, 5) - subgraph, node_map = retworkx.densest_subgraph_of_size(graph, 5) + subgraph, node_map = rx.densest_subgraph_of_size(graph, 5) subgraph_edge_set = set(subgraph.edge_list()) node_colors = [] for node in graph.node_indices(): From 3371bf1e3ec751d3170d97b4a7e36d78e8214d5d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 20 Sep 2022 09:52:37 -0400 Subject: [PATCH 05/11] Don't include bfs paths with insufficient nodes --- src/dense_subgraph.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs index 801d191d8..342db2705 100644 --- a/src/dense_subgraph.rs +++ b/src/dense_subgraph.rs @@ -91,7 +91,7 @@ where let best_result = node_indices .into_par_iter() - .map(|index| { + .filter_map(|index| { let mut subgraph: Vec<[NodeIndex; 2]> = Vec::with_capacity(num_nodes); let mut bfs = Bfs::new(&graph, index); let mut bfs_vec: Vec = Vec::with_capacity(num_nodes); @@ -106,6 +106,9 @@ where break; } } + if bfs_vec.len() < num_nodes { + return None; + } let mut connection_count = 0; for node in &bfs_vec { for nbr in graph.neighbors(*node).filter(|j| bfs_set.contains(j)) { @@ -119,12 +122,12 @@ where } None => 0., }; - SubsetResult { + Some(SubsetResult { count: connection_count, error, map: bfs_vec, subgraph, - } + }) }) .reduce(reduce_identity_fn, reduce_fn); From a95143515342d7e75cd09ad927b34656e2c4c840 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 20 Sep 2022 14:14:05 -0400 Subject: [PATCH 06/11] Handle weighting of undirected graphs --- src/dense_subgraph.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs index 342db2705..cdcbcf962 100644 --- a/src/dense_subgraph.rs +++ b/src/dense_subgraph.rs @@ -63,6 +63,9 @@ where let target: NodeIndex = edge.target(); let weight = float_callback(callback, source.index(), target.index())?; inner_weight_map.insert([source, target], weight); + if !graph.is_directed() { + inner_weight_map.insert([target, source], weight); + } } weight_map = Some(inner_weight_map); } From 40aac33693308115a4a88ee714dffd8830398880 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 29 Sep 2022 15:36:31 -0400 Subject: [PATCH 07/11] Add node weight callback --- src/dense_subgraph.rs | 92 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs index cdcbcf962..e0882bfab 100644 --- a/src/dense_subgraph.rs +++ b/src/dense_subgraph.rs @@ -41,7 +41,8 @@ pub fn densest_subgraph( py: Python, graph: &StablePyGraph, num_nodes: usize, - weight_callback: Option, + edge_weight_callback: Option, + node_weight_callback: Option, ) -> PyResult<(StablePyGraph, NodeMap)> where Ty: EdgeType + Sync, @@ -52,12 +53,17 @@ where let res = callback.as_ref(py).call1((source_node, target_node))?; res.extract() }; - let mut weight_map: Option> = None; + let node_callback = |callback: &PyObject, node_index: usize| -> PyResult { + let res = callback.as_ref(py).call1((node_index,))?; + res.extract() + }; + let mut edge_weight_map: Option> = None; + let mut node_weight_map: Option> = None; - if weight_callback.is_some() { + if edge_weight_callback.is_some() { let mut inner_weight_map: HashMap<[NodeIndex; 2], f64> = HashMap::with_capacity(graph.edge_count()); - let callback = weight_callback.as_ref().unwrap(); + let callback = edge_weight_callback.as_ref().unwrap(); for edge in graph.edge_references() { let source: NodeIndex = edge.source(); let target: NodeIndex = edge.target(); @@ -67,7 +73,21 @@ where inner_weight_map.insert([target, source], weight); } } - weight_map = Some(inner_weight_map); + edge_weight_map = Some(inner_weight_map); + } + let mut avg_node_error: f64 = 0.; + if node_weight_callback.is_some() { + let callback = node_weight_callback.as_ref().unwrap(); + let mut inner_weight_map: HashMap = + HashMap::with_capacity(graph.node_count()); + for node in graph.node_indices() { + let node_index = node.index(); + let weight = node_callback(callback, node_index)?; + avg_node_error += weight; + inner_weight_map.insert(node, weight); + } + avg_node_error /= graph.node_count() as f64; + node_weight_map = Some(inner_weight_map); } let reduce_identity_fn = || -> SubsetResult { SubsetResult { @@ -79,7 +99,7 @@ where }; let reduce_fn = |best: SubsetResult, curr: SubsetResult| -> SubsetResult { - if weight_callback.is_some() { + if edge_weight_callback.is_some() || node_weight_callback.is_some() { if curr.count >= best.count && curr.error <= best.error { curr } else { @@ -119,12 +139,26 @@ where subgraph.push([*node, nbr]); } } - let error = match &weight_map { + let mut error = match &edge_weight_map { Some(map) => { subgraph.iter().map(|edge| map[edge]).sum::() / subgraph.len() as f64 } None => 0., }; + error *= match &node_weight_map { + Some(map) => { + let subgraph_node_error_avg = + bfs_vec.iter().map(|node| map[node]).sum::() / num_nodes as f64; + let node_error_diff = subgraph_node_error_avg - avg_node_error; + if node_error_diff > 0. { + num_nodes as f64 * node_error_diff + } else { + 1. + } + } + None => 1., + }; + Some(SubsetResult { count: connection_count, error, @@ -157,24 +191,38 @@ where /// /// :param PyGraph graph: The graph to find densest subgraph in. /// :param int num_nodes: The number of nodes in the subgraph to find -/// :param func weight_callback: An optional callable that if specified will be +/// :param func edge_weight_callback: An optional callable that if specified will be /// passed the node indices of each edge in the graph and it is expected to /// return a float value. If specified the lowest avg weight for edges in /// a found subgraph will be a criteria for selection in addition to the /// connectivity of the subgraph. +/// :param func node_weight_callback: An optional callable that if specified will be +/// passed the node indices of each node in the graph and it is expected to +/// return a float value. If specified the lowest avg weight for node of +/// a found subgraph will be a criteria for selection in addition to the +/// connectivity of the subgraph.// /// :returns: A tuple of the subgraph found and a :class:`~.NodeMap` of the /// mapping of node indices in the input ``graph`` to the index in the /// output subgraph. /// :rtype: (PyGraph, NodeMap) #[pyfunction] -#[pyo3(text_signature = "(graph, num_nodes, /, weight_callback=None)")] +#[pyo3( + text_signature = "(graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None)" +)] pub fn graph_densest_subgraph_of_size( py: Python, graph: &graph::PyGraph, num_nodes: usize, - weight_callback: Option, + edge_weight_callback: Option, + node_weight_callback: Option, ) -> PyResult<(graph::PyGraph, NodeMap)> { - let (inner_graph, node_map) = densest_subgraph(py, &graph.graph, num_nodes, weight_callback)?; + let (inner_graph, node_map) = densest_subgraph( + py, + &graph.graph, + num_nodes, + edge_weight_callback, + node_weight_callback, + )?; let out_graph = graph::PyGraph { graph: inner_graph, node_removed: false, @@ -191,24 +239,38 @@ pub fn graph_densest_subgraph_of_size( /// /// :param PyDiGraph graph: The graph to find the densest subgraph in. /// :param int num_nodes: The number of nodes in the subgraph to find -/// :param func weight_callback: An optional callable that if specified will be +/// :param func edge_weight_callback: An optional callable that if specified will be /// passed the node indices of each edge in the graph and it is expected to /// return a float value. If specified the lowest avg weight for edges in /// a found subgraph will be a criteria for selection in addition to the /// connectivity of the subgraph. +/// :param func node_weight_callback: An optional callable that if specified will be +/// passed the node indices of each node in the graph and it is expected to +/// return a float value. If specified the lowest avg weight for node of +/// a found subgraph will be a criteria for selection in addition to the +/// connectivity of the subgraph. /// :returns: A tuple of the subgraph found and a :class:`~.NodeMap` of the /// mapping of node indices in the input ``graph`` to the index in the /// output subgraph. /// :rtype: (PyDiGraph, NodeMap) #[pyfunction] -#[pyo3(text_signature = "(graph, num_nodes, /, weight_callback=None)")] +#[pyo3( + text_signature = "(graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None)" +)] pub fn digraph_densest_subgraph_of_size( py: Python, graph: &digraph::PyDiGraph, num_nodes: usize, - weight_callback: Option, + edge_weight_callback: Option, + node_weight_callback: Option, ) -> PyResult<(digraph::PyDiGraph, NodeMap)> { - let (inner_graph, node_map) = densest_subgraph(py, &graph.graph, num_nodes, weight_callback)?; + let (inner_graph, node_map) = densest_subgraph( + py, + &graph.graph, + num_nodes, + edge_weight_callback, + node_weight_callback, + )?; let out_graph = digraph::PyDiGraph { graph: inner_graph, node_removed: false, From f528e2fc5d8cceeee0b7146ec0b7b26cb3aa8cb4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 18 Jun 2024 19:02:51 -0400 Subject: [PATCH 08/11] Fix lint --- rustworkx/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index 819c5a0f2..80c1378ff 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -1875,6 +1875,7 @@ def longest_simple_path(graph): """ raise TypeError("Invalid Input Type %s for graph" % type(graph)) + @_rustworkx_dispatch def densest_subgraph_of_size(graph, num_nodes, weight_callback=None): """Find a connected and dense subgraph of a given size in a graph. @@ -1898,6 +1899,7 @@ def densest_subgraph_of_size(graph, num_nodes, weight_callback=None): """ raise TypeError("Invalid Input Type %s for graph" % type(graph)) + @_rustworkx_dispatch def isolates(graph): """Return a list of isolates in a graph object From bd3b81719771ccfbdd2cd769d7e001c03e812cb4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 19 Jun 2024 06:25:49 -0400 Subject: [PATCH 09/11] Pivot to rustworkx-core This commit migrates the implementation of the core algorithm to rustworkx-core. --- rustworkx-core/src/dense_subgraph.rs | 212 ++++++++++++++++++++++ rustworkx-core/src/lib.rs | 1 + src/dense_subgraph.rs | 252 +++++++++------------------ 3 files changed, 291 insertions(+), 174 deletions(-) create mode 100644 rustworkx-core/src/dense_subgraph.rs diff --git a/rustworkx-core/src/dense_subgraph.rs b/rustworkx-core/src/dense_subgraph.rs new file mode 100644 index 000000000..714fd7a9c --- /dev/null +++ b/rustworkx-core/src/dense_subgraph.rs @@ -0,0 +1,212 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +use hashbrown::{HashMap, HashSet}; +use std::hash::Hash; + +use petgraph::prelude::*; +use petgraph::visit::{ + EdgeCount, GraphProp, IntoEdgeReferences, IntoNeighbors, IntoNodeIdentifiers, NodeCount, + Visitable, +}; + +use rayon::prelude::*; + +struct SubsetResult { + pub count: usize, + pub error: f64, + pub map: Vec, +} + +/// Find the most densely connected k-subgraph +/// +/// This function will return the node indices of the subgraph of `num_nodes` that is the +/// most densely connected. +/// +/// This method does not provide any guarantees on the approximation as it +/// does a naive search using BFS traversal. +/// +/// # Arguments +/// +/// * `graph` - The graph to find densest subgraph in. +/// * `num_nodes` - The number of nodes in the subgraph to find +/// * `edge_weight_callback` - An optional callable that if specified will be +/// passed the node indices of each edge in the graph and it is expected to +/// return a float value. If specified the lowest avg weight for edges in +/// a found subgraph will be a criteria for selection in addition to the +/// connectivity of the subgraph. +/// * `node_weight_callback` - An optional callable that if specified will be +/// passed the node indices of each node in the graph and it is expected to +/// return a float value. If specified the lowest avg weight for node of +/// a found subgraph will be a criteria for selection in addition to the +/// connectivity of the subgraph. +/// +/// # Example: +/// +/// ```rust +/// use std::convert::Infallible; +/// use rustworkx_core::petgraph::stable_graph::{StableDiGraph, NodeIndex}; +/// use rustworkx_core::petgraph::visit::IntoEdgeReferences; +/// use rustworkx_core::generators::grid_graph; +/// use rustworkx_core::dense_subgraph::densest_subgraph; +/// +/// type EdgeWeightType = Box as IntoEdgeReferences>::EdgeRef) -> Result>; +/// type NodeWeightType = Box Result>; +/// +/// let graph: StableDiGraph<(), ()> = grid_graph( +/// Some(10), +/// Some(10), +/// None, +/// || {()}, +/// || {()}, +/// false +/// ).unwrap(); +/// let subgraph_nodes = densest_subgraph(&graph, 10, None::, None::).unwrap(); +/// +/// let expected = vec![ +/// NodeIndex::new(7), NodeIndex::new(8), NodeIndex::new(17), NodeIndex::new(9), +/// NodeIndex::new(18), NodeIndex::new(27), NodeIndex::new(19), NodeIndex::new(28), +/// NodeIndex::new(37), NodeIndex::new(29) +/// ]; +/// +/// assert_eq!(subgraph_nodes, expected); +/// ``` +pub fn densest_subgraph( + graph: G, + num_nodes: usize, + edge_weight_callback: Option, + node_weight_callback: Option, +) -> Result, E> +where + G: IntoNodeIdentifiers + + IntoEdgeReferences + + EdgeCount + + GraphProp + + NodeCount + + IntoNeighbors + + Visitable + + Sync, + G::NodeId: Eq + Hash + Send + Sync, + F: FnMut(G::NodeId) -> Result, + H: FnMut(G::EdgeRef) -> Result, +{ + let node_indices: Vec = graph.node_identifiers().collect(); + let mut edge_weight_map: Option> = None; + let mut node_weight_map: Option> = None; + + if edge_weight_callback.is_some() { + let mut inner_weight_map: HashMap<[G::NodeId; 2], f64> = + HashMap::with_capacity(graph.edge_count()); + let mut callback = edge_weight_callback.unwrap(); + for edge in graph.edge_references() { + let source = edge.source(); + let target = edge.target(); + let weight = callback(edge)?; + inner_weight_map.insert([source, target], weight); + if !graph.is_directed() { + inner_weight_map.insert([target, source], weight); + } + } + edge_weight_map = Some(inner_weight_map); + } + let mut avg_node_error: f64 = 0.; + if node_weight_callback.is_some() { + let mut callback = node_weight_callback.unwrap(); + let mut inner_weight_map: HashMap = + HashMap::with_capacity(graph.node_count()); + for node in graph.node_identifiers() { + let weight = callback(node)?; + avg_node_error += weight; + inner_weight_map.insert(node, weight); + } + avg_node_error /= graph.node_count() as f64; + node_weight_map = Some(inner_weight_map); + } + let reduce_identity_fn = || -> SubsetResult { + SubsetResult { + count: 0, + map: Vec::new(), + error: f64::INFINITY, + } + }; + + let reduce_fn = + |best: SubsetResult, curr: SubsetResult| -> SubsetResult { + if edge_weight_map.is_some() || node_weight_map.is_some() { + if curr.count >= best.count && curr.error <= best.error { + curr + } else { + best + } + } else if curr.count > best.count { + curr + } else { + best + } + }; + + let best_result = node_indices + .into_par_iter() + .filter_map(|index| { + let mut subgraph: Vec<[G::NodeId; 2]> = Vec::with_capacity(num_nodes); + let mut bfs = Bfs::new(&graph, index); + let mut bfs_vec: Vec = Vec::with_capacity(num_nodes); + let mut bfs_set: HashSet = HashSet::with_capacity(num_nodes); + + let mut count = 0; + while let Some(node) = bfs.next(&graph) { + bfs_vec.push(node); + bfs_set.insert(node); + count += 1; + if count >= num_nodes { + break; + } + } + if bfs_vec.len() < num_nodes { + return None; + } + let mut connection_count = 0; + for node in &bfs_vec { + for nbr in graph.neighbors(*node).filter(|j| bfs_set.contains(j)) { + connection_count += 1; + subgraph.push([*node, nbr]); + } + } + let mut error = match &edge_weight_map { + Some(map) => { + subgraph.iter().map(|edge| map[edge]).sum::() / subgraph.len() as f64 + } + None => 0., + }; + error *= match &node_weight_map { + Some(map) => { + let subgraph_node_error_avg = + bfs_vec.iter().map(|node| map[node]).sum::() / num_nodes as f64; + let node_error_diff = subgraph_node_error_avg - avg_node_error; + if node_error_diff > 0. { + num_nodes as f64 * node_error_diff + } else { + 1. + } + } + None => 1., + }; + + Some(SubsetResult { + count: connection_count, + error, + map: bfs_vec, + }) + }) + .reduce(reduce_identity_fn, reduce_fn); + Ok(best_result.map) +} diff --git a/rustworkx-core/src/lib.rs b/rustworkx-core/src/lib.rs index fc5d6f5df..e19895212 100644 --- a/rustworkx-core/src/lib.rs +++ b/rustworkx-core/src/lib.rs @@ -109,6 +109,7 @@ pub mod planar; pub mod shortest_path; pub mod traversal; // These modules define additional data structures +pub mod dense_subgraph; pub mod dictmap; pub mod distancemap; mod min_scored; diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs index 86e637856..37afcadaa 100644 --- a/src/dense_subgraph.rs +++ b/src/dense_subgraph.rs @@ -10,19 +10,17 @@ // License for the specific language governing permissions and limitations // under the License. -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashSet; use petgraph::algo; use petgraph::graph::NodeIndex; use petgraph::prelude::*; use petgraph::visit::{IntoEdgeReferences, NodeFiltered}; -use petgraph::EdgeType; - -use rayon::prelude::*; use pyo3::prelude::*; use pyo3::Python; +use rustworkx_core::dense_subgraph::densest_subgraph; use rustworkx_core::dictmap::*; use crate::digraph; @@ -30,160 +28,6 @@ use crate::graph; use crate::iterators::NodeMap; use crate::StablePyGraph; -struct SubsetResult { - pub count: usize, - pub error: f64, - pub map: Vec, - pub subgraph: Vec<[NodeIndex; 2]>, -} - -pub fn densest_subgraph( - py: Python, - graph: &StablePyGraph, - num_nodes: usize, - edge_weight_callback: Option, - node_weight_callback: Option, -) -> PyResult<(StablePyGraph, NodeMap)> -where - Ty: EdgeType + Sync, -{ - let node_indices: Vec = graph.node_indices().collect(); - let float_callback = - |callback: &PyObject, source_node: usize, target_node: usize| -> PyResult { - let res = callback.bind(py).call1((source_node, target_node))?; - res.extract() - }; - let node_callback = |callback: &PyObject, node_index: usize| -> PyResult { - let res = callback.bind(py).call1((node_index,))?; - res.extract() - }; - let mut edge_weight_map: Option> = None; - let mut node_weight_map: Option> = None; - - if edge_weight_callback.is_some() { - let mut inner_weight_map: HashMap<[NodeIndex; 2], f64> = - HashMap::with_capacity(graph.edge_count()); - let callback = edge_weight_callback.as_ref().unwrap(); - for edge in graph.edge_references() { - let source: NodeIndex = edge.source(); - let target: NodeIndex = edge.target(); - let weight = float_callback(callback, source.index(), target.index())?; - inner_weight_map.insert([source, target], weight); - if !graph.is_directed() { - inner_weight_map.insert([target, source], weight); - } - } - edge_weight_map = Some(inner_weight_map); - } - let mut avg_node_error: f64 = 0.; - if node_weight_callback.is_some() { - let callback = node_weight_callback.as_ref().unwrap(); - let mut inner_weight_map: HashMap = - HashMap::with_capacity(graph.node_count()); - for node in graph.node_indices() { - let node_index = node.index(); - let weight = node_callback(callback, node_index)?; - avg_node_error += weight; - inner_weight_map.insert(node, weight); - } - avg_node_error /= graph.node_count() as f64; - node_weight_map = Some(inner_weight_map); - } - let reduce_identity_fn = || -> SubsetResult { - SubsetResult { - count: 0, - map: Vec::new(), - error: f64::INFINITY, - subgraph: Vec::new(), - } - }; - - let reduce_fn = |best: SubsetResult, curr: SubsetResult| -> SubsetResult { - if edge_weight_callback.is_some() || node_weight_callback.is_some() { - if curr.count >= best.count && curr.error <= best.error { - curr - } else { - best - } - } else if curr.count > best.count { - curr - } else { - best - } - }; - - let best_result = node_indices - .into_par_iter() - .filter_map(|index| { - let mut subgraph: Vec<[NodeIndex; 2]> = Vec::with_capacity(num_nodes); - let mut bfs = Bfs::new(&graph, index); - let mut bfs_vec: Vec = Vec::with_capacity(num_nodes); - let mut bfs_set: HashSet = HashSet::with_capacity(num_nodes); - - let mut count = 0; - while let Some(node) = bfs.next(&graph) { - bfs_vec.push(node); - bfs_set.insert(node); - count += 1; - if count >= num_nodes { - break; - } - } - if bfs_vec.len() < num_nodes { - return None; - } - let mut connection_count = 0; - for node in &bfs_vec { - for nbr in graph.neighbors(*node).filter(|j| bfs_set.contains(j)) { - connection_count += 1; - subgraph.push([*node, nbr]); - } - } - let mut error = match &edge_weight_map { - Some(map) => { - subgraph.iter().map(|edge| map[edge]).sum::() / subgraph.len() as f64 - } - None => 0., - }; - error *= match &node_weight_map { - Some(map) => { - let subgraph_node_error_avg = - bfs_vec.iter().map(|node| map[node]).sum::() / num_nodes as f64; - let node_error_diff = subgraph_node_error_avg - avg_node_error; - if node_error_diff > 0. { - num_nodes as f64 * node_error_diff - } else { - 1. - } - } - None => 1., - }; - - Some(SubsetResult { - count: connection_count, - error, - map: bfs_vec, - subgraph, - }) - }) - .reduce(reduce_identity_fn, reduce_fn); - - let mut subgraph = StablePyGraph::::with_capacity(num_nodes, best_result.subgraph.len()); - let mut node_map: DictMap = DictMap::with_capacity(num_nodes); - for node in best_result.map { - let new_index = subgraph.add_node(graph[node].clone_ref(py)); - node_map.insert(node.index(), new_index.index()); - } - let node_filter = |node: NodeIndex| -> bool { node_map.contains_key(&node.index()) }; - let filtered = NodeFiltered(graph, node_filter); - for edge in filtered.edge_references() { - let new_source = NodeIndex::new(*node_map.get(&edge.source().index()).unwrap()); - let new_target = NodeIndex::new(*node_map.get(&edge.target().index()).unwrap()); - subgraph.add_edge(new_source, new_target, edge.weight().clone_ref(py)); - } - Ok((subgraph, NodeMap { node_map })) -} - /// Find densest subgraph in a :class:`~.PyGraph` /// /// This method does not provide any guarantees on the approximation as it @@ -216,20 +60,50 @@ pub fn graph_densest_subgraph_of_size( edge_weight_callback: Option, node_weight_callback: Option, ) -> PyResult<(graph::PyGraph, NodeMap)> { - let (inner_graph, node_map) = densest_subgraph( - py, - &graph.graph, - num_nodes, - edge_weight_callback, - node_weight_callback, - )?; + let edge_callback = edge_weight_callback.map(|callback| { + move |edge: <&StablePyGraph as IntoEdgeReferences>::EdgeRef| -> PyResult { + let res = callback + .bind(py) + .call1((edge.source().index(), edge.target().index()))?; + res.extract() + } + }); + let node_callback = node_weight_callback.map(|callback| { + move |node_index: NodeIndex| -> PyResult { + let res = callback.bind(py).call1((node_index.index(),))?; + res.extract() + } + }); + + let subgraph_nodes = densest_subgraph(&graph.graph, num_nodes, edge_callback, node_callback)?; + let node_subset: HashSet = subgraph_nodes.iter().copied().collect(); + let node_filter = |node: NodeIndex| -> bool { node_subset.contains(&node) }; + let filtered = NodeFiltered(&graph.graph, node_filter); + let mut inner_graph: StablePyGraph = + StablePyGraph::with_capacity(subgraph_nodes.len(), 0); + let node_map: DictMap = subgraph_nodes + .into_iter() + .map(|node| { + ( + node.index(), + inner_graph + .add_node(graph.graph.node_weight(node).unwrap().clone_ref(py)) + .index(), + ) + }) + .collect(); + for edge in filtered.edge_references() { + let new_source = NodeIndex::new(*node_map.get(&edge.source().index()).unwrap()); + let new_target = NodeIndex::new(*node_map.get(&edge.target().index()).unwrap()); + inner_graph.add_edge(new_source, new_target, edge.weight().clone_ref(py)); + } let out_graph = graph::PyGraph { graph: inner_graph, node_removed: false, multigraph: graph.multigraph, attrs: py.None(), }; - Ok((out_graph, node_map)) + Ok((out_graph, NodeMap { node_map })) } /// Find densest subgraph in a :class:`~.PyDiGraph` @@ -264,13 +138,43 @@ pub fn digraph_densest_subgraph_of_size( edge_weight_callback: Option, node_weight_callback: Option, ) -> PyResult<(digraph::PyDiGraph, NodeMap)> { - let (inner_graph, node_map) = densest_subgraph( - py, - &graph.graph, - num_nodes, - edge_weight_callback, - node_weight_callback, - )?; + let edge_callback = edge_weight_callback.map(|callback| { + move |edge: <&StablePyGraph as IntoEdgeReferences>::EdgeRef| -> PyResult { + let res = callback + .bind(py) + .call1((edge.source().index(), edge.target().index()))?; + res.extract() + } + }); + let node_callback = node_weight_callback.map(|callback| { + move |node_index: NodeIndex| -> PyResult { + let res = callback.bind(py).call1((node_index.index(),))?; + res.extract() + } + }); + + let subgraph_nodes = densest_subgraph(&graph.graph, num_nodes, edge_callback, node_callback)?; + let node_subset: HashSet = subgraph_nodes.iter().copied().collect(); + let node_filter = |node: NodeIndex| -> bool { node_subset.contains(&node) }; + let filtered = NodeFiltered(&graph.graph, node_filter); + let mut inner_graph: StablePyGraph = + StablePyGraph::with_capacity(subgraph_nodes.len(), 0); + let node_map: DictMap = subgraph_nodes + .into_iter() + .map(|node| { + ( + node.index(), + inner_graph + .add_node(graph.graph.node_weight(node).unwrap().clone_ref(py)) + .index(), + ) + }) + .collect(); + for edge in filtered.edge_references() { + let new_source = NodeIndex::new(*node_map.get(&edge.source().index()).unwrap()); + let new_target = NodeIndex::new(*node_map.get(&edge.target().index()).unwrap()); + inner_graph.add_edge(new_source, new_target, edge.weight().clone_ref(py)); + } let out_graph = digraph::PyDiGraph { graph: inner_graph, node_removed: false, @@ -279,5 +183,5 @@ pub fn digraph_densest_subgraph_of_size( multigraph: graph.multigraph, attrs: py.None(), }; - Ok((out_graph, node_map)) + Ok((out_graph, NodeMap { node_map })) } From ff29cd4b0b64e335762d9c5da8a3ac1d1bbd1bb9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 26 Jun 2024 12:49:23 -0400 Subject: [PATCH 10/11] Fix python type hint stubs --- rustworkx/__init__.py | 2 +- rustworkx/__init__.pyi | 9 +++++++++ rustworkx/rustworkx.pyi | 17 +++++++++++++++++ src/dense_subgraph.rs | 4 ++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index 80c1378ff..4fbf3b69b 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -1877,7 +1877,7 @@ def longest_simple_path(graph): @_rustworkx_dispatch -def densest_subgraph_of_size(graph, num_nodes, weight_callback=None): +def densest_subgraph_of_size(graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None): """Find a connected and dense subgraph of a given size in a graph. This method does not provide any guarantees on the approximation of the optimal solution as it diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index 11edc5922..43210318f 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -232,6 +232,8 @@ from .rustworkx import steiner_tree as steiner_tree from .rustworkx import metric_closure as metric_closure from .rustworkx import digraph_union as digraph_union from .rustworkx import graph_union as graph_union +from .rustworkx import digraph_densest_subgraph_of_size as digraph_densest_subgraph_of_size +from .rustworkx import graph_densest_subgraph_of_size as graph_densest_subgraph_of_size from .rustworkx import NodeIndices as NodeIndices from .rustworkx import PathLengthMapping as PathLengthMapping from .rustworkx import PathMapping as PathMapping @@ -602,3 +604,10 @@ def longest_simple_path(graph: PyGraph[_S, _T] | PyDiGraph[_S, _T]) -> NodeIndic def isolates(graph: PyGraph[_S, _T] | PyDiGraph[_S, _T]) -> NodeIndices: ... def two_color(graph: PyGraph[_S, _T] | PyDiGraph[_S, _T]) -> dict[int, int]: ... def is_bipartite(graph: PyGraph[_S, _T] | PyDiGraph[_S, _T]) -> bool: ... +def densest_subgraph_of_size( + graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], + num_nodes: int, + /, + edge_weight_callback: Callable[[_T], float] | None = ..., + node_weight_callback: Callable[[_S], float] | None = ..., +) -> tuple[PyGraph[_S, _T], NodeMap]: ... diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi index 47c8e3673..6999eabb1 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -1012,6 +1012,23 @@ def graph_union( merge_edges: bool = ..., ) -> PyGraph[_S, _T]: ... +# Densest Subgraph + +def graph_densest_subgraph_of_size( + graph: PyGraph[_S, _T], + num_nodes: int, + /, + edge_weight_callback: Callable[[_T], float] | None = ..., + node_weight_callback: Callable[[_T], float] | None = ..., +) -> tuple[PyGraph[_S, _T], NodeMap]: ... +def digraph_densest_subgraph_of_size( + graph: PyDiGraph[_S, _T], + num_nodes: int, + /, + edge_weight_callback: Callable[[_T], float] | None = ..., + node_weight_callback: Callable[[_T], float] | None = ..., +) -> tuple[PyGraph[_S, _T], NodeMap]: ... + # Iterators _T_co = TypeVar("_T_co", covariant=True) diff --git a/src/dense_subgraph.rs b/src/dense_subgraph.rs index 37afcadaa..73dc6e19a 100644 --- a/src/dense_subgraph.rs +++ b/src/dense_subgraph.rs @@ -51,7 +51,7 @@ use crate::StablePyGraph; /// :rtype: (PyGraph, NodeMap) #[pyfunction] #[pyo3( - text_signature = "(graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None)" + signature=(graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None) )] pub fn graph_densest_subgraph_of_size( py: Python, @@ -129,7 +129,7 @@ pub fn graph_densest_subgraph_of_size( /// :rtype: (PyDiGraph, NodeMap) #[pyfunction] #[pyo3( - text_signature = "(graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None)" + signature = (graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None) )] pub fn digraph_densest_subgraph_of_size( py: Python, From cece039b417e670e9e830478556f63496b165488 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 26 Jun 2024 15:35:39 -0400 Subject: [PATCH 11/11] Fix lint --- rustworkx/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index 4fbf3b69b..2be66a7c1 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -1877,7 +1877,9 @@ def longest_simple_path(graph): @_rustworkx_dispatch -def densest_subgraph_of_size(graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None): +def densest_subgraph_of_size( + graph, num_nodes, /, edge_weight_callback=None, node_weight_callback=None +): """Find a connected and dense subgraph of a given size in a graph. This method does not provide any guarantees on the approximation of the optimal solution as it