# -*- coding: utf-8 -*- """Greedy coloring test suite. """ __author__ = "\n".join(["Christian Olsson ", "Jan Aagaard Meier ", "Henrik Haugbølle ", "Jake VanderPlas "]) import networkx as nx import pytest is_coloring = nx.algorithms.coloring.equitable_coloring.is_coloring is_equitable = nx.algorithms.coloring.equitable_coloring.is_equitable ALL_STRATEGIES = [ 'largest_first', 'random_sequential', 'smallest_last', 'independent_set', 'connected_sequential_bfs', 'connected_sequential_dfs', 'connected_sequential', 'saturation_largest_first', 'DSATUR', ] # List of strategies where interchange=True results in an error INTERCHANGE_INVALID = [ 'independent_set', 'saturation_largest_first', 'DSATUR' ] class TestColoring: def test_basic_cases(self): def check_basic_case(graph_func, n_nodes, strategy, interchange): graph = graph_func() coloring = nx.coloring.greedy_color(graph, strategy=strategy, interchange=interchange) assert verify_length(coloring, n_nodes) assert verify_coloring(graph, coloring) for graph_func, n_nodes in BASIC_TEST_CASES.items(): for interchange in [True, False]: for strategy in ALL_STRATEGIES: check_basic_case(graph_func, n_nodes, strategy, False) if strategy not in INTERCHANGE_INVALID: check_basic_case(graph_func, n_nodes, strategy, True) def test_special_cases(self): def check_special_case(strategy, graph_func, interchange, colors): graph = graph_func() coloring = nx.coloring.greedy_color(graph, strategy=strategy, interchange=interchange) if not hasattr(colors, '__len__'): colors = [colors] assert any(verify_length(coloring, n_colors) for n_colors in colors) assert verify_coloring(graph, coloring) for strategy, arglist in SPECIAL_TEST_CASES.items(): for args in arglist: check_special_case(strategy, args[0], args[1], args[2]) def test_interchange_invalid(self): graph = one_node_graph() for strategy in INTERCHANGE_INVALID: pytest.raises(nx.NetworkXPointlessConcept, nx.coloring.greedy_color, graph, strategy=strategy, interchange=True) def test_bad_inputs(self): graph = one_node_graph() pytest.raises(nx.NetworkXError, nx.coloring.greedy_color, graph, strategy='invalid strategy') def test_strategy_as_function(self): graph = lf_shc() colors_1 = nx.coloring.greedy_color(graph, 'largest_first') colors_2 = nx.coloring.greedy_color(graph, nx.coloring.strategy_largest_first) assert colors_1 == colors_2 def test_seed_argument(self): graph = lf_shc() rs = nx.coloring.strategy_random_sequential c1 = nx.coloring.greedy_color(graph, lambda g, c: rs(g, c, seed=1)) for u, v in graph.edges: assert c1[u] != c1[v] def test_is_coloring(self): G = nx.Graph() G.add_edges_from([(0, 1), (1, 2)]) coloring = {0: 0, 1: 1, 2: 0} assert is_coloring(G, coloring) coloring[0] = 1 assert not is_coloring(G, coloring) assert not is_equitable(G, coloring) def test_is_equitable(self): G = nx.Graph() G.add_edges_from([(0, 1), (1, 2)]) coloring = {0: 0, 1: 1, 2: 0} assert is_equitable(G, coloring) G.add_edges_from([(2, 3), (2, 4), (2, 5)]) coloring[3] = 1 coloring[4] = 1 coloring[5] = 1 assert is_coloring(G, coloring) assert not is_equitable(G, coloring) def test_num_colors(self): G = nx.Graph() G.add_edges_from([(0, 1), (0, 2), (0, 3)]) pytest.raises(nx.NetworkXAlgorithmError, nx.coloring.equitable_color, G, 2) def test_equitable_color(self): G = nx.fast_gnp_random_graph(n=10, p=0.2, seed=42) coloring = nx.coloring.equitable_color(G, max_degree(G) + 1) assert is_equitable(G, coloring) def test_equitable_color_empty(self): G = nx.empty_graph() coloring = nx.coloring.equitable_color(G, max_degree(G) + 1) assert is_equitable(G, coloring) def test_equitable_color_large(self): G = nx.fast_gnp_random_graph(100, 0.1, seed=42) coloring = nx.coloring.equitable_color(G, max_degree(G) + 1) assert is_equitable(G, coloring, num_colors=max_degree(G) + 1) def test_case_V_plus_not_in_A_cal(self): # Hand crafted case to avoid the easy case. L = { 0: [2, 5], 1: [3, 4], 2: [0, 8], 3: [1, 7], 4: [1, 6], 5: [0, 6], 6: [4, 5], 7: [3], 8: [2], } F = { # Color 0 0: 0, 1: 0, # Color 1 2: 1, 3: 1, 4: 1, 5: 1, # Color 2 6: 2, 7: 2, 8: 2, } C = nx.algorithms.coloring.equitable_coloring.make_C_from_F(F) N = nx.algorithms.coloring.equitable_coloring.make_N_from_L_C(L, C) H = nx.algorithms.coloring.equitable_coloring.make_H_from_C_N(C, N) nx.algorithms.coloring.equitable_coloring.procedure_P( V_minus=0, V_plus=1, N=N, H=H, F=F, C=C, L=L ) check_state(L=L, N=N, H=H, F=F, C=C) def test_cast_no_solo(self): L = { 0: [8, 9], 1: [10, 11], 2: [8], 3: [9], 4: [10, 11], 5: [8], 6: [9], 7: [10, 11], 8: [0, 2, 5], 9: [0, 3, 6], 10: [1, 4, 7], 11: [1, 4, 7], } F = { 0: 0, 1: 0, 2: 2, 3: 2, 4: 2, 5: 3, 6: 3, 7: 3, 8: 1, 9: 1, 10: 1, 11: 1, } C = nx.algorithms.coloring.equitable_coloring.make_C_from_F(F) N = nx.algorithms.coloring.equitable_coloring.make_N_from_L_C(L, C) H = nx.algorithms.coloring.equitable_coloring.make_H_from_C_N(C, N) nx.algorithms.coloring.equitable_coloring.procedure_P( V_minus=0, V_plus=1, N=N, H=H, F=F, C=C, L=L ) check_state(L=L, N=N, H=H, F=F, C=C) def test_hard_prob(self): # Tests for two levels of recursion. num_colors, s = 5, 5 G = nx.Graph() G.add_edges_from( [(0, 10), (0, 11), (0, 12), (0, 23), (10, 4), (10, 9), (10, 20), (11, 4), (11, 8), (11, 16), (12, 9), (12, 22), (12, 23), (23, 7), (1, 17), (1, 18), (1, 19), (1, 24), (17, 5), (17, 13), (17, 22), (18, 5), (19, 5), (19, 6), (19, 8), (24, 7), (24, 16), (2, 4), (2, 13), (2, 14), (2, 15), (4, 6), (13, 5), (13, 21), (14, 6), (14, 15), (15, 6), (15, 21), (3, 16), (3, 20), (3, 21), (3, 22), (16, 8), (20, 8), (21, 9), (22, 7)] ) F = {node: node // s for node in range(num_colors * s)} F[s - 1] = num_colors - 1 params = make_params_from_graph(G=G, F=F) nx.algorithms.coloring.equitable_coloring.procedure_P( V_minus=0, V_plus=num_colors - 1, **params ) check_state(**params) def test_hardest_prob(self): # Tests for two levels of recursion. num_colors, s = 10, 4 G = nx.Graph() G.add_edges_from( [(0, 19), (0, 24), (0, 29), (0, 30), (0, 35), (19, 3), (19, 7), (19, 9), (19, 15), (19, 21), (19, 24), (19, 30), (19, 38), (24, 5), (24, 11), (24, 13), (24, 20), (24, 30), (24, 37), (24, 38), (29, 6), (29, 10), (29, 13), (29, 15), (29, 16), (29, 17), (29, 20), (29, 26), (30, 6), (30, 10), (30, 15), (30, 22), (30, 23), (30, 39), (35, 6), (35, 9), (35, 14), (35, 18), (35, 22), (35, 23), (35, 25), (35, 27), (1, 20), (1, 26), (1, 31), (1, 34), (1, 38), (20, 4), (20, 8), (20, 14), (20, 18), (20, 28), (20, 33), (26, 7), (26, 10), (26, 14), (26, 18), (26, 21), (26, 32), (26, 39), (31, 5), (31, 8), (31, 13), (31, 16), (31, 17), (31, 21), (31, 25), (31, 27), (34, 7), (34, 8), (34, 13), (34, 18), (34, 22), (34, 23), (34, 25), (34, 27), (38, 4), (38, 9), (38, 12), (38, 14), (38, 21), (38, 27), (2, 3), (2, 18), (2, 21), (2, 28), (2, 32), (2, 33), (2, 36), (2, 37), (2, 39), (3, 5), (3, 9), (3, 13), (3, 22), (3, 23), (3, 25), (3, 27), (18, 6), (18, 11), (18, 15), (18, 39), (21, 4), (21, 10), (21, 14), (21, 36), (28, 6), (28, 10), (28, 14), (28, 16), (28, 17), (28, 25), (28, 27), (32, 5), (32, 10), (32, 12), (32, 16), (32, 17), (32, 22), (32, 23), (33, 7), (33, 10), (33, 12), (33, 16), (33, 17), (33, 25), (33, 27), (36, 5), (36, 8), (36, 15), (36, 16), (36, 17), (36, 25), (36, 27), (37, 5), (37, 11), (37, 15), (37, 16), (37, 17), (37, 22), (37, 23), (39, 7), (39, 8), (39, 15), (39, 22), (39, 23)] ) F = {node: node // s for node in range(num_colors * s)} F[s - 1] = num_colors - 1 # V- = 0, V+ = num_colors - 1 params = make_params_from_graph(G=G, F=F) nx.algorithms.coloring.equitable_coloring.procedure_P( V_minus=0, V_plus=num_colors - 1, **params ) check_state(**params) # ############################ Utility functions ############################ def verify_coloring(graph, coloring): for node in graph.nodes(): if node not in coloring: return False color = coloring[node] for neighbor in graph.neighbors(node): if coloring[neighbor] == color: return False return True def verify_length(coloring, expected): coloring = dict_to_sets(coloring) return len(coloring) == expected def dict_to_sets(colors): if len(colors) == 0: return [] k = max(colors.values()) + 1 sets = [set() for _ in range(k)] for (node, color) in colors.items(): sets[color].add(node) return sets # ############################ Graph Generation ############################ def empty_graph(): return nx.Graph() def one_node_graph(): graph = nx.Graph() graph.add_nodes_from([1]) return graph def two_node_graph(): graph = nx.Graph() graph.add_nodes_from([1, 2]) graph.add_edges_from([(1, 2)]) return graph def three_node_clique(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3]) graph.add_edges_from([(1, 2), (1, 3), (2, 3)]) return graph def disconnected(): graph = nx.Graph() graph.add_edges_from([ (1, 2), (2, 3), (4, 5), (5, 6) ]) return graph def rs_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4]) graph.add_edges_from([ (1, 2), (2, 3), (3, 4) ]) return graph def slf_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7]) graph.add_edges_from([ (1, 2), (1, 5), (1, 6), (2, 3), (2, 7), (3, 4), (3, 7), (4, 5), (4, 6), (5, 6) ]) return graph def slf_hc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8]) graph.add_edges_from([ (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 6), (5, 7), (5, 8), (6, 7), (6, 8), (7, 8) ]) return graph def lf_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6]) graph.add_edges_from([ (6, 1), (1, 4), (4, 3), (3, 2), (2, 5) ]) return graph def lf_hc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7]) graph.add_edges_from([ (1, 7), (1, 6), (1, 3), (1, 4), (7, 2), (2, 6), (2, 3), (2, 5), (5, 3), (5, 4), (4, 3) ]) return graph def sl_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6]) graph.add_edges_from([ (1, 2), (1, 3), (2, 3), (1, 4), (2, 5), (3, 6), (4, 5), (4, 6), (5, 6) ]) return graph def sl_hc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8]) graph.add_edges_from([ (1, 2), (1, 3), (1, 5), (1, 7), (2, 3), (2, 4), (2, 8), (8, 4), (8, 6), (8, 7), (7, 5), (7, 6), (3, 4), (4, 6), (6, 5), (5, 3) ]) return graph def gis_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4]) graph.add_edges_from([ (1, 2), (2, 3), (3, 4) ]) return graph def gis_hc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6]) graph.add_edges_from([ (1, 5), (2, 5), (3, 6), (4, 6), (5, 6) ]) return graph def cs_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5]) graph.add_edges_from([ (1, 2), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (4, 5) ]) return graph def rsi_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6]) graph.add_edges_from([ (1, 2), (1, 5), (1, 6), (2, 3), (3, 4), (4, 5), (4, 6), (5, 6) ]) return graph def lfi_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7]) graph.add_edges_from([ (1, 2), (1, 5), (1, 6), (2, 3), (2, 7), (3, 4), (3, 7), (4, 5), (4, 6), (5, 6) ]) return graph def lfi_hc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9]) graph.add_edges_from([ (1, 2), (1, 5), (1, 6), (1, 7), (2, 3), (2, 8), (2, 9), (3, 4), (3, 8), (3, 9), (4, 5), (4, 6), (4, 7), (5, 6) ]) return graph def sli_shc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7]) graph.add_edges_from([ (1, 2), (1, 3), (1, 5), (1, 7), (2, 3), (2, 6), (3, 4), (4, 5), (4, 6), (5, 7), (6, 7) ]) return graph def sli_hc(): graph = nx.Graph() graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9]) graph.add_edges_from([ (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 7), (2, 8), (2, 9), (3, 6), (3, 7), (3, 9), (4, 5), (4, 6), (4, 8), (4, 9), (5, 6), (5, 7), (5, 8), (6, 7), (6, 9), (7, 8), (8, 9) ]) return graph # -------------------------------------------------------------------------- # Basic tests for all strategies # For each basic graph function, specify the number of expected colors. BASIC_TEST_CASES = {empty_graph: 0, one_node_graph: 1, two_node_graph: 2, disconnected: 2, three_node_clique: 3} # -------------------------------------------------------------------------- # Special test cases. Each strategy has a list of tuples of the form # (graph function, interchange, valid # of colors) SPECIAL_TEST_CASES = { 'random_sequential': [ (rs_shc, False, (2, 3)), (rs_shc, True, 2), (rsi_shc, True, (3, 4))], 'saturation_largest_first': [ (slf_shc, False, (3, 4)), (slf_hc, False, 4)], 'largest_first': [ (lf_shc, False, (2, 3)), (lf_hc, False, 4), (lf_shc, True, 2), (lf_hc, True, 3), (lfi_shc, True, (3, 4)), (lfi_hc, True, 4)], 'smallest_last': [ (sl_shc, False, (3, 4)), (sl_hc, False, 5), (sl_shc, True, 3), (sl_hc, True, 4), (sli_shc, True, (3, 4)), (sli_hc, True, 5)], 'independent_set': [ (gis_shc, False, (2, 3)), (gis_hc, False, 3)], 'connected_sequential': [ (cs_shc, False, (3, 4)), (cs_shc, True, 3)], 'connected_sequential_dfs': [ (cs_shc, False, (3, 4))], } # -------------------------------------------------------------------------- # Helper functions to test # (graph function, interchange, valid # of colors) def check_state(L, N, H, F, C): s = len(C[0]) num_colors = len(C.keys()) assert all(u in L[v] for u in L.keys() for v in L[u]) assert all(F[u] != F[v] for u in L.keys() for v in L[u]) assert all(len(L[u]) < num_colors for u in L.keys()) assert all(len(C[x]) == s for x in C) assert all(H[(c1, c2)] >= 0 for c1 in C.keys() for c2 in C.keys()) assert all(N[(u, F[u])] == 0 for u in F.keys()) def max_degree(G): """Get the maximum degree of any node in G.""" return max([G.degree(node) for node in G.nodes]) if len(G.nodes) > 0 else 0 def make_params_from_graph(G, F): """Returns {N, L, H, C} from the given graph.""" num_nodes = len(G) L = {u: [] for u in range(num_nodes)} for (u, v) in G.edges: L[u].append(v) L[v].append(u) C = nx.algorithms.coloring.equitable_coloring.make_C_from_F(F) N = nx.algorithms.coloring.equitable_coloring.make_N_from_L_C(L, C) H = nx.algorithms.coloring.equitable_coloring.make_H_from_C_N(C, N) return { 'N': N, 'F': F, 'C': C, 'H': H, 'L': L, }