test_branchings.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. import math
  2. from operator import itemgetter
  3. import pytest
  4. np = pytest.importorskip("numpy")
  5. import networkx as nx
  6. from networkx.algorithms.tree import branchings, recognition
  7. #
  8. # Explicitly discussed examples from Edmonds paper.
  9. #
  10. # Used in Figures A-F.
  11. #
  12. # fmt: off
  13. G_array = np.array([
  14. # 0 1 2 3 4 5 6 7 8
  15. [0, 0, 12, 0, 12, 0, 0, 0, 0], # 0
  16. [4, 0, 0, 0, 0, 13, 0, 0, 0], # 1
  17. [0, 17, 0, 21, 0, 12, 0, 0, 0], # 2
  18. [5, 0, 0, 0, 17, 0, 18, 0, 0], # 3
  19. [0, 0, 0, 0, 0, 0, 0, 12, 0], # 4
  20. [0, 0, 0, 0, 0, 0, 14, 0, 12], # 5
  21. [0, 0, 21, 0, 0, 0, 0, 0, 15], # 6
  22. [0, 0, 0, 19, 0, 0, 15, 0, 0], # 7
  23. [0, 0, 0, 0, 0, 0, 0, 18, 0], # 8
  24. ], dtype=int)
  25. # fmt: on
  26. def G1():
  27. G = nx.from_numpy_array(G_array, create_using=nx.MultiDiGraph)
  28. return G
  29. def G2():
  30. # Now we shift all the weights by -10.
  31. # Should not affect optimal arborescence, but does affect optimal branching.
  32. Garr = G_array.copy()
  33. Garr[np.nonzero(Garr)] -= 10
  34. G = nx.from_numpy_array(Garr, create_using=nx.MultiDiGraph)
  35. return G
  36. # An optimal branching for G1 that is also a spanning arborescence. So it is
  37. # also an optimal spanning arborescence.
  38. #
  39. optimal_arborescence_1 = [
  40. (0, 2, 12),
  41. (2, 1, 17),
  42. (2, 3, 21),
  43. (1, 5, 13),
  44. (3, 4, 17),
  45. (3, 6, 18),
  46. (6, 8, 15),
  47. (8, 7, 18),
  48. ]
  49. # For G2, the optimal branching of G1 (with shifted weights) is no longer
  50. # an optimal branching, but it is still an optimal spanning arborescence
  51. # (just with shifted weights). An optimal branching for G2 is similar to what
  52. # appears in figure G (this is greedy_subopt_branching_1a below), but with the
  53. # edge (3, 0, 5), which is now (3, 0, -5), removed. Thus, the optimal branching
  54. # is not a spanning arborescence. The code finds optimal_branching_2a.
  55. # An alternative and equivalent branching is optimal_branching_2b. We would
  56. # need to modify the code to iterate through all equivalent optimal branchings.
  57. #
  58. # These are maximal branchings or arborescences.
  59. optimal_branching_2a = [
  60. (5, 6, 4),
  61. (6, 2, 11),
  62. (6, 8, 5),
  63. (8, 7, 8),
  64. (2, 1, 7),
  65. (2, 3, 11),
  66. (3, 4, 7),
  67. ]
  68. optimal_branching_2b = [
  69. (8, 7, 8),
  70. (7, 3, 9),
  71. (3, 4, 7),
  72. (3, 6, 8),
  73. (6, 2, 11),
  74. (2, 1, 7),
  75. (1, 5, 3),
  76. ]
  77. optimal_arborescence_2 = [
  78. (0, 2, 2),
  79. (2, 1, 7),
  80. (2, 3, 11),
  81. (1, 5, 3),
  82. (3, 4, 7),
  83. (3, 6, 8),
  84. (6, 8, 5),
  85. (8, 7, 8),
  86. ]
  87. # Two suboptimal maximal branchings on G1 obtained from a greedy algorithm.
  88. # 1a matches what is shown in Figure G in Edmonds's paper.
  89. greedy_subopt_branching_1a = [
  90. (5, 6, 14),
  91. (6, 2, 21),
  92. (6, 8, 15),
  93. (8, 7, 18),
  94. (2, 1, 17),
  95. (2, 3, 21),
  96. (3, 0, 5),
  97. (3, 4, 17),
  98. ]
  99. greedy_subopt_branching_1b = [
  100. (8, 7, 18),
  101. (7, 6, 15),
  102. (6, 2, 21),
  103. (2, 1, 17),
  104. (2, 3, 21),
  105. (1, 5, 13),
  106. (3, 0, 5),
  107. (3, 4, 17),
  108. ]
  109. def build_branching(edges):
  110. G = nx.DiGraph()
  111. for u, v, weight in edges:
  112. G.add_edge(u, v, weight=weight)
  113. return G
  114. def sorted_edges(G, attr="weight", default=1):
  115. edges = [(u, v, data.get(attr, default)) for (u, v, data) in G.edges(data=True)]
  116. edges = sorted(edges, key=lambda x: (x[2], x[1], x[0]))
  117. return edges
  118. def assert_equal_branchings(G1, G2, attr="weight", default=1):
  119. edges1 = list(G1.edges(data=True))
  120. edges2 = list(G2.edges(data=True))
  121. assert len(edges1) == len(edges2)
  122. # Grab the weights only.
  123. e1 = sorted_edges(G1, attr, default)
  124. e2 = sorted_edges(G2, attr, default)
  125. for a, b in zip(e1, e2):
  126. assert a[:2] == b[:2]
  127. np.testing.assert_almost_equal(a[2], b[2])
  128. ################
  129. def test_optimal_branching1():
  130. G = build_branching(optimal_arborescence_1)
  131. assert recognition.is_arborescence(G), True
  132. assert branchings.branching_weight(G) == 131
  133. def test_optimal_branching2a():
  134. G = build_branching(optimal_branching_2a)
  135. assert recognition.is_arborescence(G), True
  136. assert branchings.branching_weight(G) == 53
  137. def test_optimal_branching2b():
  138. G = build_branching(optimal_branching_2b)
  139. assert recognition.is_arborescence(G), True
  140. assert branchings.branching_weight(G) == 53
  141. def test_optimal_arborescence2():
  142. G = build_branching(optimal_arborescence_2)
  143. assert recognition.is_arborescence(G), True
  144. assert branchings.branching_weight(G) == 51
  145. def test_greedy_suboptimal_branching1a():
  146. G = build_branching(greedy_subopt_branching_1a)
  147. assert recognition.is_arborescence(G), True
  148. assert branchings.branching_weight(G) == 128
  149. def test_greedy_suboptimal_branching1b():
  150. G = build_branching(greedy_subopt_branching_1b)
  151. assert recognition.is_arborescence(G), True
  152. assert branchings.branching_weight(G) == 127
  153. def test_greedy_max1():
  154. # Standard test.
  155. #
  156. G = G1()
  157. B = branchings.greedy_branching(G)
  158. # There are only two possible greedy branchings. The sorting is such
  159. # that it should equal the second suboptimal branching: 1b.
  160. B_ = build_branching(greedy_subopt_branching_1b)
  161. assert_equal_branchings(B, B_)
  162. def test_greedy_branching_kwarg_kind():
  163. G = G1()
  164. with pytest.raises(nx.NetworkXException, match="Unknown value for `kind`."):
  165. B = branchings.greedy_branching(G, kind="lol")
  166. def test_greedy_branching_for_unsortable_nodes():
  167. G = nx.DiGraph()
  168. G.add_weighted_edges_from([((2, 3), 5, 1), (3, "a", 1), (2, 4, 5)])
  169. edges = [(u, v, data.get("weight", 1)) for (u, v, data) in G.edges(data=True)]
  170. with pytest.raises(TypeError):
  171. edges.sort(key=itemgetter(2, 0, 1), reverse=True)
  172. B = branchings.greedy_branching(G, kind="max").edges(data=True)
  173. assert list(B) == [
  174. ((2, 3), 5, {"weight": 1}),
  175. (3, "a", {"weight": 1}),
  176. (2, 4, {"weight": 5}),
  177. ]
  178. def test_greedy_max2():
  179. # Different default weight.
  180. #
  181. G = G1()
  182. del G[1][0][0]["weight"]
  183. B = branchings.greedy_branching(G, default=6)
  184. # Chosen so that edge (3,0,5) is not selected and (1,0,6) is instead.
  185. edges = [
  186. (1, 0, 6),
  187. (1, 5, 13),
  188. (7, 6, 15),
  189. (2, 1, 17),
  190. (3, 4, 17),
  191. (8, 7, 18),
  192. (2, 3, 21),
  193. (6, 2, 21),
  194. ]
  195. B_ = build_branching(edges)
  196. assert_equal_branchings(B, B_)
  197. def test_greedy_max3():
  198. # All equal weights.
  199. #
  200. G = G1()
  201. B = branchings.greedy_branching(G, attr=None)
  202. # This is mostly arbitrary...the output was generated by running the algo.
  203. edges = [
  204. (2, 1, 1),
  205. (3, 0, 1),
  206. (3, 4, 1),
  207. (5, 8, 1),
  208. (6, 2, 1),
  209. (7, 3, 1),
  210. (7, 6, 1),
  211. (8, 7, 1),
  212. ]
  213. B_ = build_branching(edges)
  214. assert_equal_branchings(B, B_, default=1)
  215. def test_greedy_min():
  216. G = G1()
  217. B = branchings.greedy_branching(G, kind="min")
  218. edges = [
  219. (1, 0, 4),
  220. (0, 2, 12),
  221. (0, 4, 12),
  222. (2, 5, 12),
  223. (4, 7, 12),
  224. (5, 8, 12),
  225. (5, 6, 14),
  226. (7, 3, 19),
  227. ]
  228. B_ = build_branching(edges)
  229. assert_equal_branchings(B, B_)
  230. def test_edmonds1_maxbranch():
  231. G = G1()
  232. x = branchings.maximum_branching(G)
  233. x_ = build_branching(optimal_arborescence_1)
  234. assert_equal_branchings(x, x_)
  235. def test_edmonds1_maxarbor():
  236. G = G1()
  237. x = branchings.maximum_spanning_arborescence(G)
  238. x_ = build_branching(optimal_arborescence_1)
  239. assert_equal_branchings(x, x_)
  240. def test_edmonds2_maxbranch():
  241. G = G2()
  242. x = branchings.maximum_branching(G)
  243. x_ = build_branching(optimal_branching_2a)
  244. assert_equal_branchings(x, x_)
  245. def test_edmonds2_maxarbor():
  246. G = G2()
  247. x = branchings.maximum_spanning_arborescence(G)
  248. x_ = build_branching(optimal_arborescence_2)
  249. assert_equal_branchings(x, x_)
  250. def test_edmonds2_minarbor():
  251. G = G1()
  252. x = branchings.minimum_spanning_arborescence(G)
  253. # This was obtained from algorithm. Need to verify it independently.
  254. # Branch weight is: 96
  255. edges = [
  256. (3, 0, 5),
  257. (0, 2, 12),
  258. (0, 4, 12),
  259. (2, 5, 12),
  260. (4, 7, 12),
  261. (5, 8, 12),
  262. (5, 6, 14),
  263. (2, 1, 17),
  264. ]
  265. x_ = build_branching(edges)
  266. assert_equal_branchings(x, x_)
  267. def test_edmonds3_minbranch1():
  268. G = G1()
  269. x = branchings.minimum_branching(G)
  270. edges = []
  271. x_ = build_branching(edges)
  272. assert_equal_branchings(x, x_)
  273. def test_edmonds3_minbranch2():
  274. G = G1()
  275. G.add_edge(8, 9, weight=-10)
  276. x = branchings.minimum_branching(G)
  277. edges = [(8, 9, -10)]
  278. x_ = build_branching(edges)
  279. assert_equal_branchings(x, x_)
  280. # Need more tests
  281. def test_mst():
  282. # Make sure we get the same results for undirected graphs.
  283. # Example from: https://en.wikipedia.org/wiki/Kruskal's_algorithm
  284. G = nx.Graph()
  285. edgelist = [
  286. (0, 3, [("weight", 5)]),
  287. (0, 1, [("weight", 7)]),
  288. (1, 3, [("weight", 9)]),
  289. (1, 2, [("weight", 8)]),
  290. (1, 4, [("weight", 7)]),
  291. (3, 4, [("weight", 15)]),
  292. (3, 5, [("weight", 6)]),
  293. (2, 4, [("weight", 5)]),
  294. (4, 5, [("weight", 8)]),
  295. (4, 6, [("weight", 9)]),
  296. (5, 6, [("weight", 11)]),
  297. ]
  298. G.add_edges_from(edgelist)
  299. G = G.to_directed()
  300. x = branchings.minimum_spanning_arborescence(G)
  301. edges = [
  302. ({0, 1}, 7),
  303. ({0, 3}, 5),
  304. ({3, 5}, 6),
  305. ({1, 4}, 7),
  306. ({4, 2}, 5),
  307. ({4, 6}, 9),
  308. ]
  309. assert x.number_of_edges() == len(edges)
  310. for u, v, d in x.edges(data=True):
  311. assert ({u, v}, d["weight"]) in edges
  312. def test_mixed_nodetypes():
  313. # Smoke test to make sure no TypeError is raised for mixed node types.
  314. G = nx.Graph()
  315. edgelist = [(0, 3, [("weight", 5)]), (0, "1", [("weight", 5)])]
  316. G.add_edges_from(edgelist)
  317. G = G.to_directed()
  318. x = branchings.minimum_spanning_arborescence(G)
  319. def test_edmonds1_minbranch():
  320. # Using -G_array and min should give the same as optimal_arborescence_1,
  321. # but with all edges negative.
  322. edges = [(u, v, -w) for (u, v, w) in optimal_arborescence_1]
  323. G = nx.from_numpy_array(-G_array, create_using=nx.DiGraph)
  324. # Quickly make sure max branching is empty.
  325. x = branchings.maximum_branching(G)
  326. x_ = build_branching([])
  327. assert_equal_branchings(x, x_)
  328. # Now test the min branching.
  329. x = branchings.minimum_branching(G)
  330. x_ = build_branching(edges)
  331. assert_equal_branchings(x, x_)
  332. def test_edge_attribute_preservation_normal_graph():
  333. # Test that edge attributes are preserved when finding an optimum graph
  334. # using the Edmonds class for normal graphs.
  335. G = nx.Graph()
  336. edgelist = [
  337. (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
  338. (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
  339. (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
  340. ]
  341. G.add_edges_from(edgelist)
  342. ed = branchings.Edmonds(G)
  343. B = ed.find_optimum("weight", preserve_attrs=True, seed=1)
  344. assert B[0][1]["otherattr"] == 1
  345. assert B[0][1]["otherattr2"] == 3
  346. def test_edge_attribute_preservation_multigraph():
  347. # Test that edge attributes are preserved when finding an optimum graph
  348. # using the Edmonds class for multigraphs.
  349. G = nx.MultiGraph()
  350. edgelist = [
  351. (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
  352. (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
  353. (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
  354. ]
  355. G.add_edges_from(edgelist * 2) # Make sure we have duplicate edge paths
  356. ed = branchings.Edmonds(G)
  357. B = ed.find_optimum("weight", preserve_attrs=True)
  358. assert B[0][1][0]["otherattr"] == 1
  359. assert B[0][1][0]["otherattr2"] == 3
  360. def test_Edmond_kind():
  361. G = nx.MultiGraph()
  362. edgelist = [
  363. (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
  364. (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
  365. (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
  366. ]
  367. G.add_edges_from(edgelist * 2) # Make sure we have duplicate edge paths
  368. ed = branchings.Edmonds(G)
  369. with pytest.raises(nx.NetworkXException, match="Unknown value for `kind`."):
  370. ed.find_optimum(kind="lol", preserve_attrs=True)
  371. def test_MultiDiGraph_EdgeKey():
  372. # test if more than one edges has the same key
  373. G = branchings.MultiDiGraph_EdgeKey()
  374. G.add_edge(1, 2, "A")
  375. with pytest.raises(Exception, match="Key 'A' is already in use."):
  376. G.add_edge(3, 4, "A")
  377. # test if invalid edge key was specified
  378. with pytest.raises(KeyError, match="Invalid edge key 'B'"):
  379. G.remove_edge_with_key("B")
  380. # test remove_edge_with_key works
  381. if G.remove_edge_with_key("A"):
  382. assert list(G.edges(data=True)) == []
  383. # test that remove_edges_from doesn't work
  384. G.add_edge(1, 3, "A")
  385. with pytest.raises(NotImplementedError):
  386. G.remove_edges_from([(1, 3)])
  387. def test_edge_attribute_discard():
  388. # Test that edge attributes are discarded if we do not specify to keep them
  389. G = nx.Graph()
  390. edgelist = [
  391. (0, 1, [("weight", 5), ("otherattr", 1), ("otherattr2", 3)]),
  392. (0, 2, [("weight", 5), ("otherattr", 2), ("otherattr2", 2)]),
  393. (1, 2, [("weight", 6), ("otherattr", 3), ("otherattr2", 1)]),
  394. ]
  395. G.add_edges_from(edgelist)
  396. ed = branchings.Edmonds(G)
  397. B = ed.find_optimum("weight", preserve_attrs=False)
  398. edge_dict = B[0][1]
  399. with pytest.raises(KeyError):
  400. _ = edge_dict["otherattr"]
  401. def test_partition_spanning_arborescence():
  402. """
  403. Test that we can generate minimum spanning arborescences which respect the
  404. given partition.
  405. """
  406. G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
  407. G[3][0]["partition"] = nx.EdgePartition.EXCLUDED
  408. G[2][3]["partition"] = nx.EdgePartition.INCLUDED
  409. G[7][3]["partition"] = nx.EdgePartition.EXCLUDED
  410. G[0][2]["partition"] = nx.EdgePartition.EXCLUDED
  411. G[6][2]["partition"] = nx.EdgePartition.INCLUDED
  412. actual_edges = [
  413. (0, 4, 12),
  414. (1, 0, 4),
  415. (1, 5, 13),
  416. (2, 3, 21),
  417. (4, 7, 12),
  418. (5, 6, 14),
  419. (5, 8, 12),
  420. (6, 2, 21),
  421. ]
  422. B = branchings.minimum_spanning_arborescence(G, partition="partition")
  423. assert_equal_branchings(build_branching(actual_edges), B)
  424. def test_arborescence_iterator_min():
  425. """
  426. Tests the arborescence iterator.
  427. A brute force method found 680 arboresecences in this graph.
  428. This test will not verify all of them individually, but will check two
  429. things
  430. * The iterator returns 680 arboresecences
  431. * The weight of the arborescences is non-strictly increasing
  432. for more information please visit
  433. https://mjschwenne.github.io/2021/06/10/implementing-the-iterators.html
  434. """
  435. G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
  436. arborescence_count = 0
  437. arborescence_weight = -math.inf
  438. for B in branchings.ArborescenceIterator(G):
  439. arborescence_count += 1
  440. new_arborescence_weight = B.size(weight="weight")
  441. assert new_arborescence_weight >= arborescence_weight
  442. arborescence_weight = new_arborescence_weight
  443. assert arborescence_count == 680
  444. def test_arborescence_iterator_max():
  445. """
  446. Tests the arborescence iterator.
  447. A brute force method found 680 arboresecences in this graph.
  448. This test will not verify all of them individually, but will check two
  449. things
  450. * The iterator returns 680 arboresecences
  451. * The weight of the arborescences is non-strictly decreasing
  452. for more information please visit
  453. https://mjschwenne.github.io/2021/06/10/implementing-the-iterators.html
  454. """
  455. G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
  456. arborescence_count = 0
  457. arborescence_weight = math.inf
  458. for B in branchings.ArborescenceIterator(G, minimum=False):
  459. arborescence_count += 1
  460. new_arborescence_weight = B.size(weight="weight")
  461. assert new_arborescence_weight <= arborescence_weight
  462. arborescence_weight = new_arborescence_weight
  463. assert arborescence_count == 680
  464. def test_arborescence_iterator_initial_partition():
  465. """
  466. Tests the arborescence iterator with three included edges and three excluded
  467. in the initial partition.
  468. A brute force method similar to the one used in the above tests found that
  469. there are 16 arborescences which contain the included edges and not the
  470. excluded edges.
  471. """
  472. G = nx.from_numpy_array(G_array, create_using=nx.DiGraph)
  473. included_edges = [(1, 0), (5, 6), (8, 7)]
  474. excluded_edges = [(0, 2), (3, 6), (1, 5)]
  475. arborescence_count = 0
  476. arborescence_weight = -math.inf
  477. for B in branchings.ArborescenceIterator(
  478. G, init_partition=(included_edges, excluded_edges)
  479. ):
  480. arborescence_count += 1
  481. new_arborescence_weight = B.size(weight="weight")
  482. assert new_arborescence_weight >= arborescence_weight
  483. arborescence_weight = new_arborescence_weight
  484. for e in included_edges:
  485. assert e in B.edges
  486. for e in excluded_edges:
  487. assert e not in B.edges
  488. assert arborescence_count == 16