# test_matching.py - unit tests for bipartite matching algorithms # # Copyright 2015 Jeffrey Finkelstein , # Copyright 2019 Søren Fuglede Jørgensen # # This file is part of NetworkX. # # NetworkX is distributed under a BSD license; see LICENSE.txt for more # information. """Unit tests for the :mod:`networkx.algorithms.bipartite.matching` module.""" import itertools import networkx as nx import pytest from networkx.algorithms.bipartite.matching import eppstein_matching from networkx.algorithms.bipartite.matching import hopcroft_karp_matching from networkx.algorithms.bipartite.matching import maximum_matching from networkx.algorithms.bipartite.matching import minimum_weight_full_matching from networkx.algorithms.bipartite.matching import to_vertex_cover class TestMatching(): """Tests for bipartite matching algorithms.""" def setup(self): """Creates a bipartite graph for use in testing matching algorithms. The bipartite graph has a maximum cardinality matching that leaves vertex 1 and vertex 10 unmatched. The first six numbers are the left vertices and the next six numbers are the right vertices. """ self.simple_graph = nx.complete_bipartite_graph(2, 3) self.simple_solution = {0: 2, 1: 3, 2: 0, 3: 1} edges = [(0, 7), (0, 8), (2, 6), (2, 9), (3, 8), (4, 8), (4, 9), (5, 11)] self.top_nodes = set(range(6)) self.graph = nx.Graph() self.graph.add_nodes_from(range(12)) self.graph.add_edges_from(edges) # Example bipartite graph from issue 2127 G = nx.Graph() G.add_nodes_from([ (1, 'C'), (1, 'B'), (0, 'G'), (1, 'F'), (1, 'E'), (0, 'C'), (1, 'D'), (1, 'I'), (0, 'A'), (0, 'D'), (0, 'F'), (0, 'E'), (0, 'H'), (1, 'G'), (1, 'A'), (0, 'I'), (0, 'B'), (1, 'H'), ]) G.add_edge((1, 'C'), (0, 'A')) G.add_edge((1, 'B'), (0, 'A')) G.add_edge((0, 'G'), (1, 'I')) G.add_edge((0, 'G'), (1, 'H')) G.add_edge((1, 'F'), (0, 'A')) G.add_edge((1, 'F'), (0, 'C')) G.add_edge((1, 'F'), (0, 'E')) G.add_edge((1, 'E'), (0, 'A')) G.add_edge((1, 'E'), (0, 'C')) G.add_edge((0, 'C'), (1, 'D')) G.add_edge((0, 'C'), (1, 'I')) G.add_edge((0, 'C'), (1, 'G')) G.add_edge((0, 'C'), (1, 'H')) G.add_edge((1, 'D'), (0, 'A')) G.add_edge((1, 'I'), (0, 'A')) G.add_edge((1, 'I'), (0, 'E')) G.add_edge((0, 'A'), (1, 'G')) G.add_edge((0, 'A'), (1, 'H')) G.add_edge((0, 'E'), (1, 'G')) G.add_edge((0, 'E'), (1, 'H')) self.disconnected_graph = G def check_match(self, matching): """Asserts that the matching is what we expect from the bipartite graph constructed in the :meth:`setup` fixture. """ # For the sake of brevity, rename `matching` to `M`. M = matching matched_vertices = frozenset(itertools.chain(*M.items())) # Assert that the maximum number of vertices (10) is matched. assert matched_vertices == frozenset(range(12)) - {1, 10} # Assert that no vertex appears in two edges, or in other words, that # the matching (u, v) and (v, u) both appear in the matching # dictionary. assert all(u == M[M[u]] for u in range(12) if u in M) def check_vertex_cover(self, vertices): """Asserts that the given set of vertices is the vertex cover we expected from the bipartite graph constructed in the :meth:`setup` fixture. """ # By Konig's theorem, the number of edges in a maximum matching equals # the number of vertices in a minimum vertex cover. assert len(vertices) == 5 # Assert that the set is truly a vertex cover. for (u, v) in self.graph.edges(): assert u in vertices or v in vertices # TODO Assert that the vertices are the correct ones. def test_eppstein_matching(self): """Tests that David Eppstein's implementation of the Hopcroft--Karp algorithm produces a maximum cardinality matching. """ self.check_match(eppstein_matching(self.graph, self.top_nodes)) def test_hopcroft_karp_matching(self): """Tests that the Hopcroft--Karp algorithm produces a maximum cardinality matching in a bipartite graph. """ self.check_match(hopcroft_karp_matching(self.graph, self.top_nodes)) def test_to_vertex_cover(self): """Test for converting a maximum matching to a minimum vertex cover.""" matching = maximum_matching(self.graph, self.top_nodes) vertex_cover = to_vertex_cover(self.graph, matching, self.top_nodes) self.check_vertex_cover(vertex_cover) def test_eppstein_matching_simple(self): match = eppstein_matching(self.simple_graph) assert match == self.simple_solution def test_hopcroft_karp_matching_simple(self): match = hopcroft_karp_matching(self.simple_graph) assert match == self.simple_solution def test_eppstein_matching_disconnected(self): with pytest.raises(nx.AmbiguousSolution): match = eppstein_matching(self.disconnected_graph) def test_hopcroft_karp_matching_disconnected(self): with pytest.raises(nx.AmbiguousSolution): match = hopcroft_karp_matching(self.disconnected_graph) def test_issue_2127(self): """Test from issue 2127""" # Build the example DAG G = nx.DiGraph() G.add_edge("A", "C") G.add_edge("A", "B") G.add_edge("C", "E") G.add_edge("C", "D") G.add_edge("E", "G") G.add_edge("E", "F") G.add_edge("G", "I") G.add_edge("G", "H") tc = nx.transitive_closure(G) btc = nx.Graph() # Create a bipartite graph based on the transitive closure of G for v in tc.nodes(): btc.add_node((0, v)) btc.add_node((1, v)) for u, v in tc.edges(): btc.add_edge((0, u), (1, v)) top_nodes = {n for n in btc if n[0] == 0} matching = hopcroft_karp_matching(btc, top_nodes) vertex_cover = to_vertex_cover(btc, matching, top_nodes) independent_set = set(G) - {v for _, v in vertex_cover} assert {'B', 'D', 'F', 'I', 'H'} == independent_set def test_vertex_cover_issue_2384(self): G = nx.Graph([(0, 3), (1, 3), (1, 4), (2, 3)]) matching = maximum_matching(G) vertex_cover = to_vertex_cover(G, matching) for u, v in G.edges(): assert u in vertex_cover or v in vertex_cover def test_unorderable_nodes(self): a = object() b = object() c = object() d = object() e = object() G = nx.Graph([(a, d), (b, d), (b, e), (c, d)]) matching = maximum_matching(G) vertex_cover = to_vertex_cover(G, matching) for u, v in G.edges(): assert u in vertex_cover or v in vertex_cover def test_eppstein_matching(): """Test in accordance to issue #1927""" G = nx.Graph() G.add_nodes_from(['a', 2, 3, 4], bipartite=0) G.add_nodes_from([1, 'b', 'c'], bipartite=1) G.add_edges_from([('a', 1), ('a', 'b'), (2, 'b'), (2, 'c'), (3, 'c'), (4, 1)]) matching = eppstein_matching(G) assert len(matching) == len(maximum_matching(G)) assert all(x in set(matching.keys()) for x in set(matching.values())) class TestMinimumWeightFullMatching(object): @classmethod def setup_class(cls): global scipy scipy = pytest.importorskip('scipy') def test_minimum_weight_full_matching_square(self): G = nx.complete_bipartite_graph(3, 3) G.add_edge(0, 3, weight=400) G.add_edge(0, 4, weight=150) G.add_edge(0, 5, weight=400) G.add_edge(1, 3, weight=400) G.add_edge(1, 4, weight=450) G.add_edge(1, 5, weight=600) G.add_edge(2, 3, weight=300) G.add_edge(2, 4, weight=225) G.add_edge(2, 5, weight=300) matching = minimum_weight_full_matching(G) assert matching == {0: 4, 1: 3, 2: 5, 4: 0, 3: 1, 5: 2} def test_minimum_weight_full_matching_smaller_left(self): G = nx.complete_bipartite_graph(3, 4) G.add_edge(0, 3, weight=400) G.add_edge(0, 4, weight=150) G.add_edge(0, 5, weight=400) G.add_edge(0, 6, weight=1) G.add_edge(1, 3, weight=400) G.add_edge(1, 4, weight=450) G.add_edge(1, 5, weight=600) G.add_edge(1, 6, weight=2) G.add_edge(2, 3, weight=300) G.add_edge(2, 4, weight=225) G.add_edge(2, 5, weight=290) G.add_edge(2, 6, weight=3) matching = minimum_weight_full_matching(G) assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1} def test_minimum_weight_full_matching_smaller_top_nodes_right(self): G = nx.complete_bipartite_graph(3, 4) G.add_edge(0, 3, weight=400) G.add_edge(0, 4, weight=150) G.add_edge(0, 5, weight=400) G.add_edge(0, 6, weight=1) G.add_edge(1, 3, weight=400) G.add_edge(1, 4, weight=450) G.add_edge(1, 5, weight=600) G.add_edge(1, 6, weight=2) G.add_edge(2, 3, weight=300) G.add_edge(2, 4, weight=225) G.add_edge(2, 5, weight=290) G.add_edge(2, 6, weight=3) matching = minimum_weight_full_matching(G, top_nodes=[3, 4, 5, 6]) assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1} def test_minimum_weight_full_matching_smaller_right(self): G = nx.complete_bipartite_graph(4, 3) G.add_edge(0, 4, weight=400) G.add_edge(0, 5, weight=400) G.add_edge(0, 6, weight=300) G.add_edge(1, 4, weight=150) G.add_edge(1, 5, weight=450) G.add_edge(1, 6, weight=225) G.add_edge(2, 4, weight=400) G.add_edge(2, 5, weight=600) G.add_edge(2, 6, weight=290) G.add_edge(3, 4, weight=1) G.add_edge(3, 5, weight=2) G.add_edge(3, 6, weight=3) matching = minimum_weight_full_matching(G) assert matching == {1: 4, 2: 6, 3: 5, 4: 1, 5: 3, 6: 2} def test_minimum_weight_full_matching_negative_weights(self): G = nx.complete_bipartite_graph(2, 2) G.add_edge(0, 2, weight=-2) G.add_edge(0, 3, weight=0.2) G.add_edge(1, 2, weight=-2) G.add_edge(1, 3, weight=0.3) matching = minimum_weight_full_matching(G) assert matching == {0: 3, 1: 2, 2: 1, 3: 0} def test_minimum_weight_full_matching_different_weight_key(self): G = nx.complete_bipartite_graph(2, 2) G.add_edge(0, 2, mass=2) G.add_edge(0, 3, mass=0.2) G.add_edge(1, 2, mass=1) G.add_edge(1, 3, mass=2) matching = minimum_weight_full_matching(G, weight='mass') assert matching == {0: 3, 1: 2, 2: 1, 3: 0} def test_minimum_weight_full_matching_requires_complete_input(self): with pytest.raises(ValueError): G = nx.Graph() G.add_nodes_from([1, 2, 3, 4], bipartite=0) G.add_nodes_from(['a', 'b', 'c'], bipartite=1) G.add_edges_from([(1, 'a'), (1, 'b'), (2, 'b'), (2, 'c'), (3, 'c'), (4, 'a')]) minimum_weight_full_matching(G)