Skip to content

Commit

Permalink
Merge branch 'main' into planar_layout
Browse files Browse the repository at this point in the history
  • Loading branch information
enavarro51 authored Oct 4, 2022
2 parents 3a080e7 + 63de4cb commit d14ad6e
Show file tree
Hide file tree
Showing 16 changed files with 551 additions and 30 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
run: black --check --diff retworkx rustworkx retworkx tests
- name: Python Lint
run: flake8 --per-file-ignores='retworkx/__init__.py:F405,F403' setup.py retworkx tests rustworkx
- name: Check stray release notes
run: python tools/find_stray_release_notes.py
- name: rustworkx-core Rust Tests
run: pushd rustworkx-core && cargo test && popd
- name: rustworkx-core Docs
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ Connectivity and Cycles
rustworkx.chain_decomposition
rustworkx.all_simple_paths
rustworkx.all_pairs_all_simple_paths
rustworkx.stoer_wagner_min_cut

.. _graph-ops:

Expand Down
11 changes: 11 additions & 0 deletions releasenotes/notes/0.11/prepare-0.11-af688e532712c830.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,14 @@ features:
[{'color': 'turquoise', 'size': 'extra large', '__networkx_node__': 'A'}, {'color': 'fuschia', 'size': 'tiny', '__networkx_node__': 'B'}]
WeightedEdgeList[(0, 1, {})]
fixes:
- |
Fixed an issue with the :func:`~retworkx.generators.heavy_hex_graph`,
:func:`~retworkx.generators.directed_heavy_hex_graph`,
:func:`~retworkx.generators.heavy_square_graph`,
and :func:`~retworkx.generators.directed_heavy_square_graph` generator
functions. When the input parameter ``d`` was set to 1 these functions
would previously raise a ``pyo3_runtime.PanicException`` instead of
returning the expected graph (a single node).
Fixed `#452 <https://github.com/Qiskit/retworkx/issues/452>`__
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
features:
- |
Added a new algorithm function,
:func:`rustworkx.stoer_wagner_min_cut` that uses the
Stoer Wagner algorithm for computing a weighted minimum cut
in an undirected :class:`~rustworkx.PyGraph`.
For example:
.. jupyter-execute::
import rustworkx
from rustworkx.visualization import mpl_draw
graph = rustworkx.generators.grid_graph(2, 2)
cut_val, partition = rustworkx.stoer_wagner_min_cut(graph)
colors = [
'orange' if node in partition else 'blue' for node in graph.node_indexes()
]
mpl_draw(graph, node_color=colors)
5 changes: 5 additions & 0 deletions rustworkx-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ ahash = { version = "0.7.6", default-features = false }
fixedbitset = "0.4.2"
petgraph = "0.6.2"
rayon = "1.5"
num-traits = "0.2"
priority-queue = "1.2.0"

[dependencies.hashbrown]
version = "0.11"
Expand All @@ -23,3 +25,6 @@ features = ["rayon"]
[dependencies.indexmap]
version = "1.7"
features = ["rayon"]

[dev-dependencies]
rand = "0.8"
198 changes: 198 additions & 0 deletions rustworkx-core/src/connectivity/min_cut.rs
Original file line number Diff line number Diff line change
@@ -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.

use hashbrown::HashMap;
use num_traits::Zero;
use std::{hash::Hash, ops::AddAssign};

use priority_queue::PriorityQueue;

use petgraph::{
stable_graph::StableUnGraph,
visit::{Bfs, EdgeCount, EdgeRef, GraphProp, IntoEdges, IntoNodeIdentifiers, NodeCount},
Undirected,
};

type StCut<K, T> = Option<((T, T), K)>;
type MinCut<K, T, E> = Result<Option<(K, Vec<T>)>, E>;

fn zip<T, U>(a: Option<T>, b: Option<U>) -> Option<(T, U)> {
match (a, b) {
(Some(a), Some(b)) => Some((a, b)),
_ => None,
}
}

fn stoer_wagner_phase<G, F, K>(graph: G, mut edge_cost: F) -> StCut<K, G::NodeId>
where
G: GraphProp<EdgeType = Undirected> + IntoEdges + IntoNodeIdentifiers,
G::NodeId: Hash + Eq,
F: FnMut(G::EdgeRef) -> K,
K: Copy + Ord + Zero + AddAssign,
{
let mut pq = PriorityQueue::<G::NodeId, K, ahash::RandomState>::from(
graph
.node_identifiers()
.map(|nx| (nx, K::zero()))
.collect::<Vec<(G::NodeId, K)>>(),
);

let mut cut_w = None;
let (mut s, mut t) = (None, None);
while let Some((nx, nx_val)) = pq.pop() {
s = t;
t = Some(nx);
cut_w = Some(nx_val);
for edge in graph.edges(nx) {
pq.change_priority_by(&edge.target(), |x| {
*x += edge_cost(edge);
})
}
}

zip(zip(s, t), cut_w)
}

/// Stoer-Wagner's min cut algorithm.
///
/// Compute a weighted minimum cut using the Stoer-Wagner algorithm [`stoer_simple_1997`](https://dl.acm.org/doi/10.1145/263867.263872).
///
/// The graph should be undirected. If the input graph is disconnected,
/// a cut with zero value will be returned. For graphs with less than
/// two nodes, this function returns [`None`]. The function `edge_cost`
/// should return the cost for a particular edge. Edge costs must be non-negative.
///
/// Returns a tuple containing the value of a minimum cut and a vector
/// of all `NodeId`s contained in one part of the partition that defines a minimum cut.
///
/// # Example
/// ```rust
/// use std::collections::HashSet;
/// use std::iter::FromIterator;
///
/// use rustworkx_core::connectivity::stoer_wagner_min_cut;
/// use rustworkx_core::petgraph::graph::{NodeIndex, UnGraph};
/// use rustworkx_core::Result;
///
/// let mut graph : UnGraph<(), ()> = UnGraph::new_undirected();
/// let a = graph.add_node(()); // node with no weight
/// let b = graph.add_node(());
/// let c = graph.add_node(());
/// let d = graph.add_node(());
/// let e = graph.add_node(());
/// let f = graph.add_node(());
/// let g = graph.add_node(());
/// let h = graph.add_node(());
///
/// graph.extend_with_edges(&[
/// (a, b),
/// (b, c),
/// (c, d),
/// (d, a),
/// (e, f),
/// (b, e),
/// (f, g),
/// (g, h),
/// (h, e)
/// ]);
/// // a ---- b ---- e ---- f
/// // | | | |
/// // d ---- c h ---- g
///
/// let min_cut_res: Result<Option<(usize, Vec<_>)>> =
/// stoer_wagner_min_cut(&graph, |_| Ok(1));
///
/// let (min_cut, partition) = min_cut_res.unwrap().unwrap();
/// assert_eq!(min_cut, 1);
/// assert_eq!(
/// HashSet::<NodeIndex>::from_iter(partition),
/// HashSet::from_iter([e, f, g, h])
/// );
/// ```
pub fn stoer_wagner_min_cut<G, F, K, E>(graph: G, mut edge_cost: F) -> MinCut<K, G::NodeId, E>
where
G: GraphProp<EdgeType = Undirected> + IntoEdges + IntoNodeIdentifiers + NodeCount + EdgeCount,
G::NodeId: Hash + Eq,
F: FnMut(G::EdgeRef) -> Result<K, E>,
K: Copy + Ord + Zero + AddAssign,
{
let mut graph_with_super_nodes =
StableUnGraph::with_capacity(graph.node_count(), graph.edge_count());

let mut node_map = HashMap::with_capacity(graph.node_count());
let mut rev_node_map = HashMap::with_capacity(graph.node_count());

for node in graph.node_identifiers() {
let index = graph_with_super_nodes.add_node(());
node_map.insert(node, index);
rev_node_map.insert(index, node);
}

for edge in graph.edge_references() {
let cost = edge_cost(edge)?;
let source = node_map[&edge.source()];
let target = node_map[&edge.target()];
graph_with_super_nodes.add_edge(source, target, cost);
}

if graph_with_super_nodes.node_count() == 0 {
return Ok(None);
}

let (mut best_phase, mut min_cut_val) = (None, None);

let mut contractions = Vec::new();
for phase in 0..(graph_with_super_nodes.node_count() - 1) {
if let Some(((s, t), cut_w)) =
stoer_wagner_phase(&graph_with_super_nodes, |edge| *edge.weight())
{
if min_cut_val.is_none() || Some(cut_w) < min_cut_val {
best_phase = Some(phase);
min_cut_val = Some(cut_w);
}
// now merge nodes ``s`` and ``t``.
contractions.push((s, t));
let edges = graph_with_super_nodes
.edges(t)
.map(|edge| (s, edge.target(), *edge.weight()))
.collect::<Vec<_>>();
for (source, target, cost) in edges {
if let Some(edge_index) = graph_with_super_nodes.find_edge(source, target) {
graph_with_super_nodes[edge_index] += cost;
} else {
graph_with_super_nodes.add_edge(source, target, cost);
}
}
graph_with_super_nodes.remove_node(t);
}
}

// Recover the optimal partitioning from the contractions
let min_cut = best_phase.map(|phase| {
let mut clustered_graph = StableUnGraph::<(), ()>::default();
clustered_graph.extend_with_edges(&contractions[..phase]);

let node = contractions[phase].1;
if clustered_graph.contains_node(node) {
let mut cluster = Vec::new();
let mut bfs = Bfs::new(&clustered_graph, node);
while let Some(nx) = bfs.next(&clustered_graph) {
cluster.push(rev_node_map[&nx])
}
cluster
} else {
vec![rev_node_map[&node]]
}
});

Ok(zip(min_cut_val, min_cut))
}
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,10 +16,12 @@ mod all_simple_paths;
mod biconnected;
mod chain;
mod conn_components;
mod min_cut;

pub use all_simple_paths::all_simple_paths_multiple_targets;
pub use biconnected::articulation_points;
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 min_cut::stoer_wagner_min_cut;
28 changes: 12 additions & 16 deletions rustworkx/visualization/graphviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

import os
import subprocess
import tempfile
import uuid
import io

try:
from PIL import Image
Expand Down Expand Up @@ -185,20 +184,17 @@ def node_attr(node):
prog = method

if not filename:
with tempfile.TemporaryDirectory() as tmpdirname:
filename = f"graphviz_draw_{str(uuid.uuid4())}.{output_format}"
tmp_path = os.path.join(tmpdirname, filename)
subprocess.run(
[prog, "-T", output_format, "-o", tmp_path],
input=dot_str,
check=True,
encoding="utf8",
text=True,
)
with Image.open(tmp_path) as temp_image:
image = temp_image.copy()
os.remove(tmp_path)
return image
dot_result = subprocess.run(
[prog, "-T", output_format],
input=dot_str.encode("utf-8"),
capture_output=True,
encoding=None,
check=True,
text=False,
)
dot_bytes_image = io.BytesIO(dot_result.stdout)
image = Image.open(dot_bytes_image)
return image
else:
subprocess.run(
[prog, "-T", output_format, "-o", filename],
Expand Down
Loading

0 comments on commit d14ad6e

Please sign in to comment.