123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609 |
- import math
- from operator import itemgetter
- import pytest
- np = pytest.importorskip("numpy")
- import networkx as nx
- from networkx.algorithms.tree import branchings, recognition
- #
- # Explicitly discussed examples from Edmonds paper.
- #
- # Used in Figures A-F.
- #
- # fmt: off
- G_array = np.array([
- # 0 1 2 3 4 5 6 7 8
- [0, 0, 12, 0, 12, 0, 0, 0, 0], # 0
- [4, 0, 0, 0, 0, 13, 0, 0, 0], # 1
- [0, 17, 0, 21, 0, 12, 0, 0, 0], # 2
- [5, 0, 0, 0, 17, 0, 18, 0, 0], # 3
- [0, 0, 0, 0, 0, 0, 0, 12, 0], # 4
- [0, 0, 0, 0, 0, 0, 14, 0, 12], # 5
- [0, 0, 21, 0, 0, 0, 0, 0, 15], # 6
- [0, 0, 0, 19, 0, 0, 15, 0, 0], # 7
- [0, 0, 0, 0, 0, 0, 0, 18, 0], # 8
- ], dtype=int)
- # fmt: on
- def G1():
- G = nx.from_numpy_array(G_array, create_using=nx.MultiDiGraph)
- return G
- def G2():
- # Now we shift all the weights by -10.
- # Should not affect optimal arborescence, but does affect optimal branching.
- Garr = G_array.copy()
- Garr[np.nonzero(Garr)] -= 10
- G = nx.from_numpy_array(Garr, create_using=nx.MultiDiGraph)
- return G
- # An optimal branching for G1 that is also a spanning arborescence. So it is
- # also an optimal spanning arborescence.
- #
- optimal_arborescence_1 = [
- (0, 2, 12),
- (2, 1, 17),
- (2, 3, 21),
- (1, 5, 13),
- (3, 4, 17),
- (3, 6, 18),
- (6, 8, 15),
- (8, 7, 18),
- ]
- # For G2, the optimal branching of G1 (with shifted weights) is no longer
- # an optimal branching, but it is still an optimal spanning arborescence
- # (just with shifted weights). An optimal branching for G2 is similar to what
- # appears in figure G (this is greedy_subopt_branching_1a below), but with the
- # edge (3, 0, 5), which is now (3, 0, -5), removed. Thus, the optimal branching
- # is not a spanning arborescence. The code finds optimal_branching_2a.
- # An alternative and equivalent branching is optimal_branching_2b. We would
- # need to modify the code to iterate through all equivalent optimal branchings.
- #
- # These are maximal branchings or arborescences.
- optimal_branching_2a = [
- (5, 6, 4),
- (6, 2, 11),
- (6, 8, 5),
- (8, 7, 8),
- (2, 1, 7),
- (2, 3, 11),
- (3, 4, 7),
- ]
- optimal_branching_2b = [
- (8, 7, 8),
- (7, 3, 9),
- (3, 4, 7),
- (3, 6, 8),
- (6, 2, 11),
- (2, 1, 7),
- (1, 5, 3),
- ]
- optimal_arborescence_2 = [
- (0, 2, 2),
- (2, 1, 7),
- (2, 3, 11),
- (1, 5, 3),
- (3, 4, 7),
- (3, 6, 8),
- (6, 8, 5),
- (8, 7, 8),
- ]
- # Two suboptimal maximal branchings on G1 obtained from a greedy algorithm.
- # 1a matches what is shown in Figure G in Edmonds's paper.
- greedy_subopt_branching_1a = [
- (5, 6, 14),
- (6, 2, 21),
- (6, 8, 15),
- (8, 7, 18),
- (2, 1, 17),
- (2, 3, 21),
- (3, 0, 5),
- (3, 4, 17),
- ]
- greedy_subopt_branching_1b = [
- (8, 7, 18),
- (7, 6, 15),
- (6, 2, 21),
- (2, 1, 17),
- (2, 3, 21),
- (1, 5, 13),
- (3, 0, 5),
- (3, 4, 17),
- ]
- def build_branching(edges):
- G = nx.DiGraph()
- for u, v, weight in edges:
- G.add_edge(u, v, weight=weight)
- return G
- def sorted_edges(G, attr="weight", default=1):
- edges = [(u, v, data.get(attr, default)) for (u, v, data) in G.edges(data=True)]
- edges = sorted(edges, key=lambda x: (x[2], x[1], x[0]))
- return edges
- def assert_equal_branchings(G1, G2, attr="weight", default=1):
- edges1 = list(G1.edges(data=True))
- edges2 = list(G2.edges(data=True))
- assert len(edges1) == len(edges2)
- # Grab the weights only.
- e1 = sorted_edges(G1, attr, default)
- e2 = sorted_edges(G2, attr, default)
- for a, b in zip(e1, e2):
- assert a[:2] == b[:2]
- np.testing.assert_almost_equal(a[2], b[2])
- ################
- def test_optimal_branching1():
- G = build_branching(optimal_arborescence_1)
- assert recognition.is_arborescence(G), True
- assert branchings.branching_weight(G) == 131
- def test_optimal_branching2a():
- G = build_branching(optimal_branching_2a)
- assert recognition.is_arborescence(G), True
- assert branchings.branching_weight(G) == 53
- def test_optimal_branching2b():
- G = build_branching(optimal_branching_2b)
- assert recognition.is_arborescence(G), True
- assert branchings.branching_weight(G) == 53
- def test_optimal_arborescence2():
- G = build_branching(optimal_arborescence_2)
- assert recognition.is_arborescence(G), True
- assert branchings.branching_weight(G) == 51
- def test_greedy_suboptimal_branching1a():
- G = build_branching(greedy_subopt_branching_1a)
- assert recognition.is_arborescence(G), True
- assert branchings.branching_weight(G) == 128
- def test_greedy_suboptimal_branching1b():
- G = build_branching(greedy_subopt_branching_1b)
- assert recognition.is_arborescence(G), True
- assert branchings.branching_weight(G) == 127
- def test_greedy_max1():
- # Standard test.
- #
- G = G1()
- B = branchings.greedy_branching(G)
- # There are only two possible greedy branchings. The sorting is such
- # that it should equal the second suboptimal branching: 1b.
- B_ = build_branching(greedy_subopt_branching_1b)
- assert_equal_branchings(B, B_)
- def test_greedy_branching_kwarg_kind():
- G = G1()
- with pytest.raises(nx.NetworkXException, match="Unknown value for `kind`."):
- B = branchings.greedy_branching(G, kind="lol")
- def test_greedy_branching_for_unsortable_nodes():
- G = nx.DiGraph()
- G.add_weighted_edges_from([((2, 3), 5, 1), (3, "a", 1), (2, 4, 5)])
- edges = [(u, v, data.get("weight", 1)) for (u, v, data) in G.edges(data=True)]
- with pytest.raises(TypeError):
- edges.sort(key=itemgetter(2, 0, 1), reverse=True)
- B = branchings.greedy_branching(G, kind="max").edges(data=True)
- assert list(B) == [
- ((2, 3), 5, {"weight": 1}),
- (3, "a", {"weight": 1}),
- (2, 4, {"weight": 5}),
- ]
- def test_greedy_max2():
- # Different default weight.
- #
- G = G1()
- del G[1][0][0]["weight"]
- B = branchings.greedy_branching(G, default=6)
- # Chosen so that edge (3,0,5) is not selected and (1,0,6) is instead.
- edges = [
- (1, 0, 6),
- (1, 5, 13),
- (7, 6, 15),
- (2, 1, 17),
- (3, 4, 17),
- (8, 7, 18),
- (2, 3, 21),
- (6, 2, 21),
- ]
- B_ = build_branching(edges)
- assert_equal_branchings(B, B_)
- def test_greedy_max3():
- # All equal weights.
- #
- G = G1()
- B = branchings.greedy_branching(G, attr=None)
- # This is mostly arbitrary...the output was generated by running the algo.
- edges = [
- (2, 1, 1),
- (3, 0, 1),
- (3, 4, 1),
- (5, 8, 1),
- (6, 2, 1),
- (7, 3, 1),
- (7, 6, 1),
- (8, 7, 1),
- ]
- B_ = build_branching(edges)
- assert_equal_branchings(B, B_, default=1)
- def test_greedy_min():
- G = G1()
- B = branchings.greedy_branching(G, kind="min")
- edges = [
- (1, 0, 4),
- (0, 2, 12),
- (0, 4, 12),
- (2, 5, 12),
- (4, 7, 12),
- (5, 8, 12),
- (5, 6, 14),
- (7, 3, 19),
- ]
- B_ = build_branching(edges)
- assert_equal_branchings(B, B_)
- def test_edmonds1_maxbranch():
- G = G1()
- x = branchings.maximum_branching(G)
- x_ = build_branching(optimal_arborescence_1)
- assert_equal_branchings(x, x_)
- def test_edmonds1_maxarbor():
- G = G1()
- x = branchings.maximum_spanning_arborescence(G)
- x_ = build_branching(optimal_arborescence_1)
- assert_equal_branchings(x, x_)
- def test_edmonds2_maxbranch():
- G = G2()
- x = branchings.maximum_branching(G)
- x_ = build_branching(optimal_branching_2a)
- assert_equal_branchings(x, x_)
- def test_edmonds2_maxarbor():
- G = G2()
- x = branchings.maximum_spanning_arborescence(G)
- x_ = build_branching(optimal_arborescence_2)
- assert_equal_branchings(x, x_)
- def test_edmonds2_minarbor():
- G = G1()
- x = branchings.minimum_spanning_arborescence(G)
- # This was obtained from algorithm. Need to verify it independently.
- # Branch weight is: 96
- edges = [
- (3, 0, 5),
- (0, 2, 12),
- (0, 4, 12),
- (2, 5, 12),
- (4, 7, 12),
- (5, 8, 12),
- (5, 6, 14),
- (2, 1, 17),
- ]
- x_ = build_branching(edges)
- assert_equal_branchings(x, x_)
- def test_edmonds3_minbranch1():
- G = G1()
- x = branchings.minimum_branching(G)
- edges = []
- x_ = build_branching(edges)
- assert_equal_branchings(x, x_)
- def test_edmonds3_minbranch2():
- G = G1()
- G.add_edge(8, 9, weight=-10)
- x = branchings.minimum_branching(G)
- edges = [(8, 9, -10)]
- x_ = build_branching(edges)
- assert_equal_branchings(x, x_)
- # Need more tests
- def test_mst():
- # Make sure we get the same results for undirected graphs.
- # Example from: https://en.wikipedia.org/wiki/Kruskal's_algorithm
- G = nx.Graph()
- edgelist = [
- (0, 3, [("weight", 5)]),
- (0, 1, [("weight", 7)]),
- (1, 3, [("weight", 9)]),
- (1, 2, [("weight", 8)]),
- (1, 4, [("weight", 7)]),
- (3, 4, [("weight", 15)]),
- (3, 5, [("weight", 6)]),
- (2, 4, [("weight", 5)]),
- (4, 5, [("weight", 8)]),
- (4, 6, [("weight", 9)]),
- (5, 6, [("weight", 11)]),
- ]
- G.add_edges_from(edgelist)
- G = G.to_directed()
- x = branchings.minimum_spanning_arborescence(G)
- edges = [
- ({0, 1}, 7),
- ({0, 3}, 5),
- ({3, 5}, 6),
- ({1, 4}, 7),
- ({4, 2}, 5),
- ({4, 6}, 9),
- ]
- assert x.number_of_edges() == len(edges)
- for u, v, d in x.edges(data=True):
- assert ({u, v}, d["weight"]) in edges
- def test_mixed_nodetypes():
- # Smoke test to make sure no TypeError is raised for mixed node types.
- G = nx.Graph()
- edgelist = [(0, 3, [("weight", 5)]), (0, "1", [("weight", 5)])]
- G.add_edges_from(edgelist)
- G = G.to_directed()
- x = branchings.minimum_spanning_arborescence(G)
- def test_edmonds1_minbranch():
- # Using -G_array and min should give the same as optimal_arborescence_1,
- # but with all edges negative.
- edges = [(u, v, -w) for (u, v, w) in optimal_arborescence_1]
- G = nx.from_numpy_array(-G_array, create_using=nx.DiGraph)
- # Quickly make sure max branching is empty.
- x = branchings.maximum_branching(G)
- x_ = build_branching([])
- assert_equal_branchings(x, x_)
- # Now test the min branching.
- x = branchings.minimum_branching(G)
- x_ = build_branching(edges)
- assert_equal_branchings(x, x_)
- def test_edge_attribute_preservation_normal_graph():
- # Test that edge attributes are preserved when finding an optimum graph
- # using the Edmonds class for normal graphs.
- G = nx.Graph()
- edgelist = [
- (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
- (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
- (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
- ]
- G.add_edges_from(edgelist)
- ed = branchings.Edmonds(G)
- B = ed.find_optimum("weight", preserve_attrs=True, seed=1)
- assert B[0][1]["otherattr"] == 1
- assert B[0][1]["otherattr2"] == 3
- def test_edge_attribute_preservation_multigraph():
- # Test that edge attributes are preserved when finding an optimum graph
- # using the Edmonds class for multigraphs.
- G = nx.MultiGraph()
- edgelist = [
- (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
- (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
- (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
- ]
- G.add_edges_from(edgelist * 2) # Make sure we have duplicate edge paths
- ed = branchings.Edmonds(G)
- B = ed.find_optimum("weight", preserve_attrs=True)
- assert B[0][1][0]["otherattr"] == 1
- assert B[0][1][0]["otherattr2"] == 3
- def test_Edmond_kind():
- G = nx.MultiGraph()
- edgelist = [
- (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
- (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
- (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
- ]
- G.add_edges_from(edgelist * 2) # Make sure we have duplicate edge paths
- ed = branchings.Edmonds(G)
- with pytest.raises(nx.NetworkXException, match="Unknown value for `kind`."):
- ed.find_optimum(kind="lol", preserve_attrs=True)
- def test_MultiDiGraph_EdgeKey():
- # test if more than one edges has the same key
- G = branchings.MultiDiGraph_EdgeKey()
- G.add_edge(1, 2, "A")
- with pytest.raises(Exception, match="Key 'A' is already in use."):
- G.add_edge(3, 4, "A")
- # test if invalid edge key was specified
- with pytest.raises(KeyError, match="Invalid edge key 'B'"):
- G.remove_edge_with_key("B")
- # test remove_edge_with_key works
- if G.remove_edge_with_key("A"):
- assert list(G.edges(data=True)) == []
- # test that remove_edges_from doesn't work
- G.add_edge(1, 3, "A")
- with pytest.raises(NotImplementedError):
- G.remove_edges_from([(1, 3)])
- def test_edge_attribute_discard():
- # Test that edge attributes are discarded if we do not specify to keep them
- G = nx.Graph()
- edgelist = [
- (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
- (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
- (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
- ]
- G.add_edges_from(edgelist)
- ed = branchings.Edmonds(G)
- B = ed.find_optimum("weight", preserve_attrs=False)
- edge_dict = B[0][1]
- with pytest.raises(KeyError):
- _ = edge_dict["otherattr"]
- def test_partition_spanning_arborescence():
- """
- Test that we can generate minimum spanning arborescences which respect the
- given partition.
- """
- G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
- G[3][0]["partition"] = nx.EdgePartition.EXCLUDED
- G[2][3]["partition"] = nx.EdgePartition.INCLUDED
- G[7][3]["partition"] = nx.EdgePartition.EXCLUDED
- G[0][2]["partition"] = nx.EdgePartition.EXCLUDED
- G[6][2]["partition"] = nx.EdgePartition.INCLUDED
- actual_edges = [
- (0, 4, 12),
- (1, 0, 4),
- (1, 5, 13),
- (2, 3, 21),
- (4, 7, 12),
- (5, 6, 14),
- (5, 8, 12),
- (6, 2, 21),
- ]
- B = branchings.minimum_spanning_arborescence(G, partition="partition")
- assert_equal_branchings(build_branching(actual_edges), B)
- def test_arborescence_iterator_min():
- """
- Tests the arborescence iterator.
- A brute force method found 680 arboresecences in this graph.
- This test will not verify all of them individually, but will check two
- things
- * The iterator returns 680 arboresecences
- * The weight of the arborescences is non-strictly increasing
- for more information please visit
- https://mjschwenne.github.io/2021/06/10/implementing-the-iterators.html
- """
- G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
- arborescence_count = 0
- arborescence_weight = -math.inf
- for B in branchings.ArborescenceIterator(G):
- arborescence_count += 1
- new_arborescence_weight = B.size(weight="weight")
- assert new_arborescence_weight >= arborescence_weight
- arborescence_weight = new_arborescence_weight
- assert arborescence_count == 680
- def test_arborescence_iterator_max():
- """
- Tests the arborescence iterator.
- A brute force method found 680 arboresecences in this graph.
- This test will not verify all of them individually, but will check two
- things
- * The iterator returns 680 arboresecences
- * The weight of the arborescences is non-strictly decreasing
- for more information please visit
- https://mjschwenne.github.io/2021/06/10/implementing-the-iterators.html
- """
- G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
- arborescence_count = 0
- arborescence_weight = math.inf
- for B in branchings.ArborescenceIterator(G, minimum=False):
- arborescence_count += 1
- new_arborescence_weight = B.size(weight="weight")
- assert new_arborescence_weight <= arborescence_weight
- arborescence_weight = new_arborescence_weight
- assert arborescence_count == 680
- def test_arborescence_iterator_initial_partition():
- """
- Tests the arborescence iterator with three included edges and three excluded
- in the initial partition.
- A brute force method similar to the one used in the above tests found that
- there are 16 arborescences which contain the included edges and not the
- excluded edges.
- """
- G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
- included_edges = [(1, 0), (5, 6), (8, 7)]
- excluded_edges = [(0, 2), (3, 6), (1, 5)]
- arborescence_count = 0
- arborescence_weight = -math.inf
- for B in branchings.ArborescenceIterator(
- G, init_partition=(included_edges, excluded_edges)
- ):
- arborescence_count += 1
- new_arborescence_weight = B.size(weight="weight")
- assert new_arborescence_weight >= arborescence_weight
- arborescence_weight = new_arborescence_weight
- for e in included_edges:
- assert e in B.edges
- for e in excluded_edges:
- assert e not in B.edges
- assert arborescence_count == 16
|