Skip to content

Commit

Permalink
Add is_maximal_matching and is_matching functions
Browse files Browse the repository at this point in the history
This commit adds new functions for checking a provided matching set is
valid, is_matching(), and that a provided matching set is valid and
maximal, is_maximal_set(). This pairs with #229 and can be used to
partially check the output from the max_weight_matching function added
there. Equivalent functions were implemented in #229 using Python for
the tests in #229 and those tests should be updated to use these
functions instead.

Fixes #255
  • Loading branch information
mtreinish committed Feb 24, 2021
1 parent 69bd097 commit 05c7123
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Specific Graph Type Methods
retworkx.digraph_dfs_edges
retworkx.digraph_find_cycle
retworkx.digraph_union
retworkx.is_matching
retworkx.is_maximal_matching

Universal Functions
'''''''''''''''''''
Expand Down
8 changes: 8 additions & 0 deletions releasenotes/notes/add-is_matching-334626fe47e576be.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features:
- |
A new algorithm function, :func:`~retworkx.is_matching`, was added to
check if a matching set is valid for given graph.
- |
A new algorithm function, :func:`~retworkx.is_maxmimal_matching`, was added
to check if a matching set is valid and maximal for a given graph.
122 changes: 122 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2593,6 +2593,126 @@ pub fn digraph_find_cycle(
EdgeList { edges: cycle }
}

fn matching_combinations(
matching: &HashSet<(usize, usize)>,
) -> Vec<((usize, usize), (usize, usize))> {
let r: usize = 2;
let mut out_vec: Vec<((usize, usize), (usize, usize))> = Vec::new();
let pool: Vec<(usize, usize)> = matching.iter().cloned().collect();
let n = matching.len();
if n < r {
return out_vec;
}
let mut indices: Vec<usize> = (0..r).collect();
out_vec.push((pool[0], pool[1]));
let mut found;
let mut index = 0;
loop {
found = false;
for i in (0..r).rev() {
if indices[i] != i + n - r {
found = true;
index = i;
break;
}
}
if !found {
return out_vec;
}
indices[index] += 1;
for j in (index + 1)..r {
indices[j] = indices[j - 1] + 1
}
out_vec.push((pool[indices[0]], pool[indices[1]]));
}
}

fn _inner_is_matching(
graph: &graph::PyGraph,
matching: &HashSet<(usize, usize)>,
) -> bool {
let has_edge = |e: &(usize, usize)| -> bool {
graph
.graph
.contains_edge(NodeIndex::new(e.0), NodeIndex::new(e.1))
};

if !matching.iter().all(|e| has_edge(e)) {
return false;
}
let combinations = matching_combinations(matching);
combinations.iter().all(|&x| {
let tmp_set: HashSet<usize> =
[x.0 .0, x.0 .1, x.1 .0, x.1 .1].iter().cloned().collect();
tmp_set.len() == 4
})
}

/// Check if matching is valid for graph
///
/// A *matching* in a graph is a set of edges in which no two distinct
/// edges share a common endpoint.
///
/// :param PyDiGraph graph: The graph to check if the matching is valid for
/// :param set matching: A set of node index tuples for each edge in the
/// matching.
///
/// :returns: Whether the provided matching is a valid matching for the graph
/// :rtype: bool
#[pyfunction]
#[text_signature = "(graph, matching, /)"]
pub fn is_matching(
graph: &graph::PyGraph,
matching: HashSet<(usize, usize)>,
) -> bool {
_inner_is_matching(graph, &matching)
}

/// Check if a matching is a maximal matching for a graph
///
/// A *maximal matching* in a graph is a matching in which adding any
/// edge would cause the set to no longer be a valid matching.
///
/// :param PyDiGraph graph: The graph to check if the matching is maximal for.
/// :param set matching: A set of node index tuples for each edge in the
/// matching.
///
/// :returns: Whether the provided matching is a valid matching and whether it
/// is maximal or not.
/// :rtype: bool
#[pyfunction]
#[text_signature = "(graph, matching, /)"]
pub fn is_maximal_matching(
graph: &graph::PyGraph,
matching: HashSet<(usize, usize)>,
) -> bool {
if !_inner_is_matching(graph, &matching) {
return false;
}
let edge_list: HashSet<[usize; 2]> = graph
.edge_references()
.map(|edge| {
let mut tmp_array = [edge.source().index(), edge.target().index()];
tmp_array.sort_unstable();
tmp_array
})
.collect();
let matched_edges: HashSet<[usize; 2]> = matching
.iter()
.map(|edge| {
let mut tmp_array = [edge.0, edge.1];
tmp_array.sort_unstable();
tmp_array
})
.collect();
let mut unmatched_edges = edge_list.difference(&matched_edges);
unmatched_edges.all(|e| {
let mut tmp_set = matching.clone();
tmp_set.insert((e[0], e[1]));
!_inner_is_matching(graph, &tmp_set)
})
}

// 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 @@ -2661,6 +2781,8 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(digraph_find_cycle))?;
m.add_wrapped(wrap_pyfunction!(digraph_k_shortest_path_lengths))?;
m.add_wrapped(wrap_pyfunction!(graph_k_shortest_path_lengths))?;
m.add_wrapped(wrap_pyfunction!(is_matching))?;
m.add_wrapped(wrap_pyfunction!(is_maximal_matching))?;
m.add_class::<digraph::PyDiGraph>()?;
m.add_class::<graph::PyGraph>()?;
m.add_class::<iterators::BFSSuccessors>()?;
Expand Down
57 changes: 57 additions & 0 deletions tests/test_matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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 TestMatching(unittest.TestCase):

def test_valid(self):
graph = retworkx.generators.path_graph(4)
matching = {(0, 1), (2, 3)}
self.assertTrue(retworkx.is_maximal_matching(graph, matching))

def test_not_matching(self):
graph = retworkx.generators.path_graph(4)
matching = {(0, 1), (1, 2), (2, 3)}
self.assertFalse(retworkx.is_maximal_matching(graph, matching))

def test_not_maximal(self):
graph = retworkx.generators.path_graph(4)
matching = {(0, 1)}
self.assertFalse(retworkx.is_maximal_matching(graph, matching))

def test_is_matching_empty(self):
graph = retworkx.generators.path_graph(4)
matching = set()
self.assertTrue(retworkx.is_matching(graph, matching))

def test_is_matching_single_edge(self):
graph = retworkx.generators.path_graph(4)
matching = {(1, 2)}
self.assertTrue(retworkx.is_matching(graph, matching))

def test_is_matching_valid(self):
graph = retworkx.generators.path_graph(4)
matching = {(0, 1), (2, 3)}
self.assertTrue(retworkx.is_matching(graph, matching))

def test_is_matching_invalid(self):
graph = retworkx.generators.path_graph(4)
matching = {(0, 1), (1, 2), (2, 3)}
self.assertFalse(retworkx.is_matching(graph, matching))

def test_is_matching_invalid_edge(self):
graph = retworkx.generators.path_graph(4)
matching = {(0, 3), (1, 2)}
self.assertFalse(retworkx.is_matching(graph, matching))

0 comments on commit 05c7123

Please sign in to comment.