Skip to content

Commit c4e9432

Browse files
authored
Add Local Complementation (#1366)
* First implementation and possible impl. ideas as comments * Enable python interface to local_complement. First tests. * Only multigraphs and no digraphs Assure multigraphs are not allowed and local complementation is not defined for digraphs. Self loops are assumed to not exist in the provided grahps. Added test * redundant docstring sentence removed * Add method to docs, add release notes, add type annotations, remove digraph_local_complement, add&improve tests * update release notes
1 parent 1fb70e9 commit c4e9432

File tree

7 files changed

+165
-0
lines changed

7 files changed

+165
-0
lines changed

docs/source/api/pygraph_api_functions.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ typed API based on the data type.
3737
rustworkx.graph_transitivity
3838
rustworkx.graph_core_number
3939
rustworkx.graph_complement
40+
rustworkx.local_complement
4041
rustworkx.graph_union
4142
rustworkx.graph_tensor_product
4243
rustworkx.graph_token_swapper
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
features:
3+
- |
4+
Added a new function, :func:`~rustworkx.local_complement` which
5+
performs the local complementation of a node applied to a graph
6+
For example:
7+
8+
.. jupyter-execute::
9+
10+
import rustworkx
11+
12+
# Example taken from Figure 1 a) in https://arxiv.org/abs/1910.03969
13+
graph = rustworkx.PyGraph(multigraph=False)
14+
graph.extend_from_edge_list(
15+
[(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (2, 4), (3, 4), (3, 5)]
16+
)
17+
18+
complement_graph = rustworkx.local_complement(graph, 0)

rustworkx/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ from .rustworkx import chain_decomposition as chain_decomposition
8989
from .rustworkx import digraph_find_cycle as digraph_find_cycle
9090
from .rustworkx import digraph_complement as digraph_complement
9191
from .rustworkx import graph_complement as graph_complement
92+
from .rustworkx import local_complement as local_complement
9293
from .rustworkx import digraph_all_simple_paths as digraph_all_simple_paths
9394
from .rustworkx import graph_all_simple_paths as graph_all_simple_paths
9495
from .rustworkx import digraph_all_pairs_all_simple_paths as digraph_all_pairs_all_simple_paths

rustworkx/rustworkx.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ def graph_complement(
233233
graph: PyGraph[_S, _T],
234234
/,
235235
) -> PyGraph[_S, _T | None]: ...
236+
def local_complement(
237+
graph: PyGraph[_S, _T],
238+
node: int,
239+
/,
240+
) -> PyGraph[_S, _T | None]: ...
236241
def digraph_all_simple_paths(
237242
graph: PyDiGraph,
238243
origin: int,

src/connectivity/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use super::{
2121
};
2222

2323
use hashbrown::{HashMap, HashSet};
24+
use indexmap::IndexSet;
2425
use petgraph::algo;
2526
use petgraph::algo::condensation;
2627
use petgraph::graph::DiGraph;
@@ -561,6 +562,64 @@ pub fn digraph_complement(py: Python, graph: &digraph::PyDiGraph) -> PyResult<di
561562
Ok(complement_graph)
562563
}
563564

565+
/// Local complementation of a node applied to an undirected graph.
566+
///
567+
/// :param PyGraph graph: The graph to be used.
568+
/// :param int node: A node in the graph.
569+
///
570+
/// :returns: The locally complemented graph.
571+
/// :rtype: PyGraph
572+
///
573+
/// .. note::
574+
///
575+
/// This function assumes that there are no self loops
576+
/// in the provided graph.
577+
/// Returns an error if the :attr:`~rustworkx.PyGraph.multigraph`
578+
/// attribute is set to ``True``.
579+
#[pyfunction]
580+
#[pyo3(text_signature = "(graph, node, /)")]
581+
pub fn local_complement(
582+
py: Python,
583+
graph: &graph::PyGraph,
584+
node: usize,
585+
) -> PyResult<graph::PyGraph> {
586+
if graph.multigraph {
587+
return Err(PyValueError::new_err(
588+
"Local complementation not defined for multigraphs!",
589+
));
590+
}
591+
592+
let mut complement_graph = graph.clone(); // keep same node indices
593+
594+
let node = NodeIndex::new(node);
595+
if !graph.graph.contains_node(node) {
596+
return Err(InvalidNode::new_err(
597+
"The input index for 'node' is not a valid node index",
598+
));
599+
}
600+
601+
// Local complementation
602+
let node_neighbors: IndexSet<NodeIndex> = graph.graph.neighbors(node).collect();
603+
let node_neighbors_vec: Vec<&NodeIndex> = node_neighbors.iter().collect();
604+
for (i, neighbor_i) in node_neighbors_vec.iter().enumerate() {
605+
// Ensure checking pairs of neighbors is only done once
606+
let (_, nodes_tail) = node_neighbors_vec.split_at(i + 1);
607+
for neighbor_j in nodes_tail.iter() {
608+
let res = complement_graph.remove_edge(neighbor_i.index(), neighbor_j.index());
609+
match res {
610+
Ok(_) => (),
611+
Err(_) => {
612+
let _ = complement_graph
613+
.graph
614+
.add_edge(**neighbor_i, **neighbor_j, py.None());
615+
}
616+
}
617+
}
618+
}
619+
620+
Ok(complement_graph)
621+
}
622+
564623
/// Return all simple paths between 2 nodes in a PyGraph object
565624
///
566625
/// A simple path is a path with no repeated nodes.

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
585585
m.add_wrapped(wrap_pyfunction!(digraph_core_number))?;
586586
m.add_wrapped(wrap_pyfunction!(graph_complement))?;
587587
m.add_wrapped(wrap_pyfunction!(digraph_complement))?;
588+
m.add_wrapped(wrap_pyfunction!(local_complement))?;
588589
m.add_wrapped(wrap_pyfunction!(graph_random_layout))?;
589590
m.add_wrapped(wrap_pyfunction!(digraph_random_layout))?;
590591
m.add_wrapped(wrap_pyfunction!(graph_bipartite_layout))?;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import unittest
14+
15+
import rustworkx
16+
17+
18+
class TestLocalComplement(unittest.TestCase):
19+
def test_multigraph(self):
20+
graph = rustworkx.PyGraph(multigraph=True)
21+
node = graph.add_node("")
22+
with self.assertRaises(ValueError):
23+
rustworkx.local_complement(graph, node)
24+
25+
def test_invalid_node(self):
26+
graph = rustworkx.PyGraph(multigraph=False)
27+
node = graph.add_node("")
28+
with self.assertRaises(rustworkx.InvalidNode):
29+
rustworkx.local_complement(graph, node + 1)
30+
31+
def test_clique(self):
32+
N = 5
33+
graph = rustworkx.generators.complete_graph(N, multigraph=False)
34+
35+
for node in range(0, N):
36+
expected_graph = rustworkx.PyGraph(multigraph=False)
37+
expected_graph.extend_from_edge_list([(i, node) for i in range(0, N) if i != node])
38+
39+
complement_graph = rustworkx.local_complement(graph, node)
40+
41+
self.assertTrue(
42+
rustworkx.is_isomorphic(
43+
expected_graph,
44+
complement_graph,
45+
)
46+
)
47+
48+
def test_empty(self):
49+
N = 5
50+
graph = rustworkx.generators.empty_graph(N, multigraph=False)
51+
52+
expected_graph = rustworkx.generators.empty_graph(N, multigraph=False)
53+
54+
complement_graph = rustworkx.local_complement(graph, 0)
55+
self.assertTrue(
56+
rustworkx.is_isomorphic(
57+
expected_graph,
58+
complement_graph,
59+
)
60+
)
61+
62+
def test_local_complement(self):
63+
# Example took from https://arxiv.org/abs/1910.03969, figure 1
64+
graph = rustworkx.PyGraph(multigraph=False)
65+
graph.extend_from_edge_list(
66+
[(0, 1), (0, 3), (0, 5), (1, 2), (2, 3), (2, 4), (3, 4), (3, 5)]
67+
)
68+
69+
expected_graph = rustworkx.PyGraph(multigraph=False)
70+
expected_graph.extend_from_edge_list(
71+
[(0, 1), (0, 3), (0, 5), (1, 2), (1, 3), (1, 5), (2, 3), (2, 4), (3, 4)]
72+
)
73+
74+
complement_graph = rustworkx.local_complement(graph, 0)
75+
self.assertTrue(
76+
rustworkx.is_isomorphic(
77+
expected_graph,
78+
complement_graph,
79+
)
80+
)

0 commit comments

Comments
 (0)