Skip to content

Commit

Permalink
Add complement function (#289)
Browse files Browse the repository at this point in the history
Closes #285

Adds a new retworkx.complement() function for calculating the
complement of PyGraph and PyDiGraph

* Add graph complement function

* Add graph complement to api doc

* Version bump

* Avoid parallel edges in multigraph

* Add tests for complement function

* Adequate to CONTRIBUTING guidelines

* Fix lint issues after merge

* Use new format for tests

* Remove blank line for lint

* Fix docstring lint

* Address PR comments for docstrings

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>

* Add type specific complement functions to doc
  • Loading branch information
IvanIsCoding committed Apr 4, 2021
1 parent aa5a25c commit 963704b
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/source/api.rst
Expand Up @@ -97,6 +97,8 @@ Specific Graph Type Methods
retworkx.digraph_transitivity
retworkx.graph_core_number
retworkx.digraph_core_number
retworkx.graph_complement
retworkx.digraph_complement

.. _universal-functions:

Expand All @@ -111,6 +113,7 @@ type functions in the algorithms API but can be run with a
.. autosummary::
:toctree: stubs

retworkx.complement
retworkx.distance_matrix
retworkx.floyd_warshall_numpy
retworkx.adjacency_matrix
Expand Down
27 changes: 27 additions & 0 deletions retworkx/__init__.py
Expand Up @@ -593,3 +593,30 @@ def _digraph_core_number(graph):
@core_number.register(PyGraph)
def _graph_core_number(graph):
return graph_core_number(graph)


@functools.singledispatch
def complement(graph):
"""Compute the complement of a graph.
:param graph: The graph to be used, can be either a
:class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`.
:returns: The complement of the graph.
:rtype: :class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`
.. note::
Parallel edges and self-loops are never created,
even if the ``multigraph`` is set to ``True``
"""
raise TypeError("Invalid Input Type %s for graph" % type(graph))


@complement.register(PyDiGraph)
def _digraph_complement(graph):
return digraph_complement(graph)


@complement.register(PyGraph)
def _graph_complement(graph):
return graph_complement(graph)
1 change: 1 addition & 0 deletions src/digraph.rs
Expand Up @@ -112,6 +112,7 @@ use super::{
/// :meth:`PyDiGraph.add_parent` will avoid this overhead.
#[pyclass(module = "retworkx", subclass, gc)]
#[text_signature = "(/, check_cycle=False, multigraph=True)"]
#[derive(Clone)]
pub struct PyDiGraph {
pub graph: StableDiGraph<PyObject, PyObject>,
pub cycle_state: algo::DfsSpace<
Expand Down
1 change: 1 addition & 0 deletions src/graph.rs
Expand Up @@ -86,6 +86,7 @@ use petgraph::visit::{
///
#[pyclass(module = "retworkx", subclass, gc)]
#[text_signature = "(/, multigraph=True)"]
#[derive(Clone)]
pub struct PyGraph {
pub graph: StableUnGraph<PyObject, PyObject>,
pub node_removed: bool,
Expand Down
86 changes: 86 additions & 0 deletions src/lib.rs
Expand Up @@ -3069,6 +3069,90 @@ pub fn digraph_core_number(
_core_number(py, &graph.graph)
}

/// 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]
#[text_signature = "(graph, /)"]
fn graph_complement(
py: Python,
graph: &graph::PyGraph,
) -> PyResult<graph::PyGraph> {
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<NodeIndex> =
graph.graph.neighbors(node_a).collect();
for node_b in graph.graph.node_indices() {
if node_a != node_b && !old_neighbors.contains(&node_b) {
if !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]
#[text_signature = "(graph, /)"]
fn digraph_complement(
py: Python,
graph: &digraph::PyDiGraph,
) -> PyResult<digraph::PyDiGraph> {
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<NodeIndex> = 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.
Expand Down Expand Up @@ -3144,6 +3228,8 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(digraph_transitivity))?;
m.add_wrapped(wrap_pyfunction!(graph_core_number))?;
m.add_wrapped(wrap_pyfunction!(digraph_core_number))?;
m.add_wrapped(wrap_pyfunction!(graph_complement))?;
m.add_wrapped(wrap_pyfunction!(digraph_complement))?;
m.add_class::<digraph::PyDiGraph>()?;
m.add_class::<graph::PyGraph>()?;
m.add_class::<iterators::BFSSuccessors>()?;
Expand Down
82 changes: 82 additions & 0 deletions tests/digraph/test_complement.py
@@ -0,0 +1,82 @@
# 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 TestComplement(unittest.TestCase):
def test_null_graph(self):
graph = retworkx.PyDiGraph()
complement_graph = retworkx.complement(graph)
self.assertEqual(0, len(complement_graph.nodes()))
self.assertEqual(0, len(complement_graph.edges()))

def test_clique_directed(self):
N = 5
graph = retworkx.PyDiGraph()
graph.extend_from_edge_list(
[(i, j) for i in range(N) for j in range(N) if i != j]
)

complement_graph = retworkx.complement(graph)
self.assertEqual(graph.nodes(), complement_graph.nodes())
self.assertEqual(0, len(complement_graph.edges()))

def test_empty_directed(self):
N = 5
graph = retworkx.PyDiGraph()
graph.add_nodes_from([i for i in range(N)])

expected_graph = retworkx.PyDiGraph()
expected_graph.extend_from_edge_list(
[(i, j) for i in range(N) for j in range(N) if i != j]
)

complement_graph = retworkx.complement(graph)
self.assertTrue(
retworkx.is_isomorphic(
expected_graph,
complement_graph,
)
)

def test_complement_directed(self):
N = 8
graph = retworkx.PyDiGraph()
graph.extend_from_edge_list(
[
(i, j)
for i in range(N)
for j in range(N)
if i != j and (i + j) % 3 == 0
]
)

expected_graph = retworkx.PyDiGraph()
expected_graph.extend_from_edge_list(
[
(i, j)
for i in range(N)
for j in range(N)
if i != j and (i + j) % 3 != 0
]
)

complement_graph = retworkx.complement(graph)
self.assertTrue(
retworkx.is_isomorphic(
expected_graph,
complement_graph,
)
)
97 changes: 97 additions & 0 deletions tests/graph/test_complement.py
@@ -0,0 +1,97 @@
# 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 TestComplement(unittest.TestCase):
def test_clique(self):
N = 5
graph = retworkx.PyGraph()
graph.extend_from_edge_list(
[(i, j) for i in range(N) for j in range(N) if i < j]
)

complement_graph = retworkx.complement(graph)
self.assertEqual(graph.nodes(), complement_graph.nodes())
self.assertEqual(0, len(complement_graph.edges()))

def test_empty(self):
N = 5
graph = retworkx.PyGraph()
graph.add_nodes_from([i for i in range(N)])

expected_graph = retworkx.PyGraph()
expected_graph.extend_from_edge_list(
[(i, j) for i in range(N) for j in range(N) if i < j]
)

complement_graph = retworkx.complement(graph)
self.assertTrue(
retworkx.is_isomorphic(
expected_graph,
complement_graph,
)
)

def test_null_graph(self):
graph = retworkx.PyGraph()
complement_graph = retworkx.complement(graph)
self.assertEqual(0, len(complement_graph.nodes()))
self.assertEqual(0, len(complement_graph.edges()))

def test_complement(self):
N = 8
graph = retworkx.PyGraph()
graph.extend_from_edge_list(
[
(j, i)
for i in range(N)
for j in range(N)
if i < j and (i + j) % 3 == 0
]
)

expected_graph = retworkx.PyGraph()
expected_graph.extend_from_edge_list(
[
(i, j)
for i in range(N)
for j in range(N)
if i < j and (i + j) % 3 != 0
]
)

complement_graph = retworkx.complement(graph)
self.assertTrue(
retworkx.is_isomorphic(
expected_graph,
complement_graph,
)
)

def test_multigraph(self):
graph = retworkx.PyGraph(multigraph=True)
graph.extend_from_edge_list([(0, 0), (0, 1), (1, 1), (2, 2), (1, 0)])

expected_graph = retworkx.PyGraph(multigraph=True)
expected_graph.extend_from_edge_list([(0, 2), (1, 2)])

complement_graph = retworkx.complement(graph)
self.assertTrue(
retworkx.is_isomorphic(
expected_graph,
complement_graph,
)
)
5 changes: 5 additions & 0 deletions tests/releasenotes/notes/complement-361c90c7dda69df8.yaml
@@ -0,0 +1,5 @@
---
features:
- |
Added a new function :func:`~retworkx.complement` to calculate the graph complement
of a :class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`.

0 comments on commit 963704b

Please sign in to comment.