Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move core_number to rustworkx-core #795

Merged
merged 13 commits into from
Feb 2, 2023
6 changes: 6 additions & 0 deletions releasenotes/notes/move-core-number-48efa7ef968b6736.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
The function ``core_number``has been added to the ``rustworkx-core``
crate in the ``connectivity`` module. It computes the k-core number
for the nodes in a graph.
209 changes: 209 additions & 0 deletions rustworkx-core/src/connectivity/core_number.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// 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::hash::Hash;

use hashbrown::{HashMap, HashSet};
use petgraph::visit::{GraphBase, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount};
use petgraph::Direction::{Incoming, Outgoing};
use rayon::prelude::*;

use crate::dictmap::*;

/// 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.
///
/// 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.
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved
///
/// Arguments:
///
/// * `graph` - The graph in which to find the core numbers.
///
/// # Example
/// ```rust
/// use petgraph::prelude::*;
/// use rustworkx_core::connectivity::core_number;
///
/// let edge_list = vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
/// let graph = DiGraph::<i32, i32>::from_edges(&edge_list);
/// let res: Vec<(usize, usize)> = core_number(graph)
/// .iter()
/// .map(|(k, v)| (k.index(), *v))
/// .collect();
/// assert_eq!(res, vec![(0, 3), (1, 3), (2, 3), (3, 3)]);
/// ```
pub fn core_number<G>(graph: G) -> DictMap<G::NodeId, usize>
where
G: GraphBase + NodeCount,
for<'b> &'b G: GraphBase<NodeId = G::NodeId> + IntoNodeIdentifiers + IntoNeighborsDirected,
G::NodeId: Eq + Hash + Send + Sync,
{
let node_num = graph.node_count();
if node_num == 0 {
return DictMap::new();
}

let mut cores: DictMap<G::NodeId, usize> = DictMap::with_capacity(node_num);
let mut node_vec: Vec<G::NodeId> = graph.node_identifiers().collect();
let mut degree_map: HashMap<G::NodeId, usize> = HashMap::with_capacity(node_num);
let mut nbrs: HashMap<G::NodeId, HashSet<G::NodeId>> = HashMap::with_capacity(node_num);
let mut node_pos: HashMap<G::NodeId, usize> = HashMap::with_capacity(node_num);

for k in node_vec.iter() {
let k_nbrs: HashSet<G::NodeId> = graph
.neighbors_directed(*k, Incoming)
.chain(graph.neighbors_directed(*k, Outgoing))
.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<usize> =
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;
}
}
}
cores
}

#[cfg(test)]
mod tests {
use crate::connectivity::core_number;
use petgraph::prelude::*;

#[test]
fn test_directed_empty() {
let graph = DiGraph::<i32, i32>::new();
let res: Vec<(usize, usize)> = core_number(graph)
.iter()
.map(|(k, v)| (k.index(), *v))
.collect();
assert_eq!(res, vec![]);
}

#[test]
fn test_directed_all_0() {
let mut graph = DiGraph::<i32, i32>::new();
for _ in 0..4 {
graph.add_node(0);
}
let res: Vec<(usize, usize)> = core_number(graph)
.iter()
.map(|(k, v)| (k.index(), *v))
.collect();
assert_eq!(res, vec![(0, 0), (1, 0), (2, 0), (3, 0)]);
}

#[test]
fn test_directed_all_3() {
let edge_list = vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
let graph = DiGraph::<i32, i32>::from_edges(&edge_list);
let res: Vec<(usize, usize)> = core_number(graph)
.iter()
.map(|(k, v)| (k.index(), *v))
.collect();
assert_eq!(res, vec![(0, 3), (1, 3), (2, 3), (3, 3)]);
}

#[test]
fn test_directed_paper_example() {
// This is the example graph in Figure 1 from Batagelj and
// Zaversnik's paper titled An O(m) Algorithm for Cores
// Decomposition of Networks, 2003,
// http://arXiv.org/abs/cs/0310049. With nodes labeled as
// shown, the 3-core is given by nodes 0-7, the 2-core by nodes
// 8-15, the 1-core by nodes 16-19 and node 20 is in the
// 0-core.
let edge_list = [
(0, 2),
(0, 3),
(0, 5),
(1, 4),
(1, 6),
(1, 7),
(2, 3),
(3, 5),
(2, 5),
(5, 6),
(4, 6),
(4, 7),
(6, 7),
(5, 8),
(6, 8),
(6, 9),
(8, 9),
(0, 10),
(1, 10),
(1, 11),
(10, 11),
(12, 13),
(13, 15),
(14, 15),
(12, 14),
(8, 19),
(11, 16),
(11, 17),
(12, 18),
];
let mut example_core = vec![];
for i in 0..8 {
example_core.push((i, 3));
}
for i in 8..16 {
example_core.push((i, 2));
}
for i in 16..20 {
example_core.push((i, 1));
}
example_core.push((20, 0));
let mut graph = DiGraph::<i32, i32>::from_edges(&edge_list);
graph.add_node(0);
let res: Vec<(usize, usize)> = core_number(graph)
.iter()
.map(|(k, v)| (k.index(), *v))
.collect();
assert_eq!(res, example_core);
}
}
7 changes: 0 additions & 7 deletions rustworkx-core/src/connectivity/find_cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@
// License for the specific language governing permissions and limitations
// under the License.

// This module was forked from petgraph:
//
// https://github.com/petgraph/petgraph/blob/9ff688872b467d3e1b5adef19f5c52f519d3279c/src/algo/simple_paths.rs
//
// to add support for returning all simple paths to a list of targets instead
// of just between a single node pair.
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved

use hashbrown::{HashMap, HashSet};
use petgraph::visit::{
EdgeCount, GraphBase, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount,
Expand Down
2 changes: 2 additions & 0 deletions rustworkx-core/src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod all_simple_paths;
mod biconnected;
mod chain;
mod conn_components;
mod core_number;
mod cycle_basis;
mod find_cycle;
mod min_cut;
Expand All @@ -26,6 +27,7 @@ pub use chain::chain_decomposition;
pub use conn_components::bfs_undirected;
pub use conn_components::connected_components;
pub use conn_components::number_connected_components;
pub use core_number::core_number;
pub use cycle_basis::cycle_basis;
pub use find_cycle::find_cycle;
pub use min_cut::stoer_wagner_min_cut;
89 changes: 0 additions & 89 deletions src/connectivity/core_number.rs

This file was deleted.

16 changes: 13 additions & 3 deletions src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
#![allow(clippy::float_cmp)]

mod all_pairs_all_simple_paths;
mod core_number;
mod johnson_simple_cycles;

use super::{
Expand All @@ -25,6 +24,7 @@ use hashbrown::{HashMap, HashSet};

use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::Python;

use petgraph::algo;
Expand Down Expand Up @@ -645,7 +645,12 @@ pub fn graph_all_pairs_all_simple_paths(
#[pyfunction]
#[pyo3(text_signature = "(graph, /)")]
pub fn graph_core_number(py: Python, graph: &graph::PyGraph) -> PyResult<PyObject> {
core_number::core_number(py, &graph.graph)
let cores = connectivity::core_number(&graph.graph);
let out_dict = PyDict::new(py);
for (k, v) in cores {
out_dict.set_item(k.index(), v)?;
}
Ok(out_dict.into())
}

/// Return the core number for each node in the directed graph.
Expand All @@ -666,7 +671,12 @@ pub fn graph_core_number(py: Python, graph: &graph::PyGraph) -> PyResult<PyObjec
#[pyfunction]
#[pyo3(text_signature = "(graph, /)")]
pub fn digraph_core_number(py: Python, graph: &digraph::PyDiGraph) -> PyResult<PyObject> {
core_number::core_number(py, &graph.graph)
let cores = connectivity::core_number(&graph.graph);
let out_dict = PyDict::new(py);
for (k, v) in cores {
out_dict.set_item(k.index(), v)?;
}
Ok(out_dict.into())
}

/// Compute a weighted minimum cut using the Stoer-Wagner algorithm.
Expand Down