test_treewidth.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import itertools
  2. import networkx as nx
  3. from networkx.algorithms.approximation import (
  4. treewidth_min_degree,
  5. treewidth_min_fill_in,
  6. )
  7. from networkx.algorithms.approximation.treewidth import (
  8. MinDegreeHeuristic,
  9. min_fill_in_heuristic,
  10. )
  11. def is_tree_decomp(graph, decomp):
  12. """Check if the given tree decomposition is valid."""
  13. for x in graph.nodes():
  14. appear_once = False
  15. for bag in decomp.nodes():
  16. if x in bag:
  17. appear_once = True
  18. break
  19. assert appear_once
  20. # Check if each connected pair of nodes are at least once together in a bag
  21. for x, y in graph.edges():
  22. appear_together = False
  23. for bag in decomp.nodes():
  24. if x in bag and y in bag:
  25. appear_together = True
  26. break
  27. assert appear_together
  28. # Check if the nodes associated with vertex v form a connected subset of T
  29. for v in graph.nodes():
  30. subset = []
  31. for bag in decomp.nodes():
  32. if v in bag:
  33. subset.append(bag)
  34. sub_graph = decomp.subgraph(subset)
  35. assert nx.is_connected(sub_graph)
  36. class TestTreewidthMinDegree:
  37. """Unit tests for the min_degree function"""
  38. @classmethod
  39. def setup_class(cls):
  40. """Setup for different kinds of trees"""
  41. cls.complete = nx.Graph()
  42. cls.complete.add_edge(1, 2)
  43. cls.complete.add_edge(2, 3)
  44. cls.complete.add_edge(1, 3)
  45. cls.small_tree = nx.Graph()
  46. cls.small_tree.add_edge(1, 3)
  47. cls.small_tree.add_edge(4, 3)
  48. cls.small_tree.add_edge(2, 3)
  49. cls.small_tree.add_edge(3, 5)
  50. cls.small_tree.add_edge(5, 6)
  51. cls.small_tree.add_edge(5, 7)
  52. cls.small_tree.add_edge(6, 7)
  53. cls.deterministic_graph = nx.Graph()
  54. cls.deterministic_graph.add_edge(0, 1) # deg(0) = 1
  55. cls.deterministic_graph.add_edge(1, 2) # deg(1) = 2
  56. cls.deterministic_graph.add_edge(2, 3)
  57. cls.deterministic_graph.add_edge(2, 4) # deg(2) = 3
  58. cls.deterministic_graph.add_edge(3, 4)
  59. cls.deterministic_graph.add_edge(3, 5)
  60. cls.deterministic_graph.add_edge(3, 6) # deg(3) = 4
  61. cls.deterministic_graph.add_edge(4, 5)
  62. cls.deterministic_graph.add_edge(4, 6)
  63. cls.deterministic_graph.add_edge(4, 7) # deg(4) = 5
  64. cls.deterministic_graph.add_edge(5, 6)
  65. cls.deterministic_graph.add_edge(5, 7)
  66. cls.deterministic_graph.add_edge(5, 8)
  67. cls.deterministic_graph.add_edge(5, 9) # deg(5) = 6
  68. cls.deterministic_graph.add_edge(6, 7)
  69. cls.deterministic_graph.add_edge(6, 8)
  70. cls.deterministic_graph.add_edge(6, 9) # deg(6) = 6
  71. cls.deterministic_graph.add_edge(7, 8)
  72. cls.deterministic_graph.add_edge(7, 9) # deg(7) = 5
  73. cls.deterministic_graph.add_edge(8, 9) # deg(8) = 4
  74. def test_petersen_graph(self):
  75. """Test Petersen graph tree decomposition result"""
  76. G = nx.petersen_graph()
  77. _, decomp = treewidth_min_degree(G)
  78. is_tree_decomp(G, decomp)
  79. def test_small_tree_treewidth(self):
  80. """Test small tree
  81. Test if the computed treewidth of the known self.small_tree is 2.
  82. As we know which value we can expect from our heuristic, values other
  83. than two are regressions
  84. """
  85. G = self.small_tree
  86. # the order of removal should be [1,2,4]3[5,6,7]
  87. # (with [] denoting any order of the containing nodes)
  88. # resulting in treewidth 2 for the heuristic
  89. treewidth, _ = treewidth_min_fill_in(G)
  90. assert treewidth == 2
  91. def test_heuristic_abort(self):
  92. """Test heuristic abort condition for fully connected graph"""
  93. graph = {}
  94. for u in self.complete:
  95. graph[u] = set()
  96. for v in self.complete[u]:
  97. if u != v: # ignore self-loop
  98. graph[u].add(v)
  99. deg_heuristic = MinDegreeHeuristic(graph)
  100. node = deg_heuristic.best_node(graph)
  101. if node is None:
  102. pass
  103. else:
  104. assert False
  105. def test_empty_graph(self):
  106. """Test empty graph"""
  107. G = nx.Graph()
  108. _, _ = treewidth_min_degree(G)
  109. def test_two_component_graph(self):
  110. G = nx.Graph()
  111. G.add_node(1)
  112. G.add_node(2)
  113. treewidth, _ = treewidth_min_degree(G)
  114. assert treewidth == 0
  115. def test_not_sortable_nodes(self):
  116. G = nx.Graph([(0, "a")])
  117. treewidth_min_degree(G)
  118. def test_heuristic_first_steps(self):
  119. """Test first steps of min_degree heuristic"""
  120. graph = {
  121. n: set(self.deterministic_graph[n]) - {n} for n in self.deterministic_graph
  122. }
  123. deg_heuristic = MinDegreeHeuristic(graph)
  124. elim_node = deg_heuristic.best_node(graph)
  125. print(f"Graph {graph}:")
  126. steps = []
  127. while elim_node is not None:
  128. print(f"Removing {elim_node}:")
  129. steps.append(elim_node)
  130. nbrs = graph[elim_node]
  131. for u, v in itertools.permutations(nbrs, 2):
  132. if v not in graph[u]:
  133. graph[u].add(v)
  134. for u in graph:
  135. if elim_node in graph[u]:
  136. graph[u].remove(elim_node)
  137. del graph[elim_node]
  138. print(f"Graph {graph}:")
  139. elim_node = deg_heuristic.best_node(graph)
  140. # check only the first 5 elements for equality
  141. assert steps[:5] == [0, 1, 2, 3, 4]
  142. class TestTreewidthMinFillIn:
  143. """Unit tests for the treewidth_min_fill_in function."""
  144. @classmethod
  145. def setup_class(cls):
  146. """Setup for different kinds of trees"""
  147. cls.complete = nx.Graph()
  148. cls.complete.add_edge(1, 2)
  149. cls.complete.add_edge(2, 3)
  150. cls.complete.add_edge(1, 3)
  151. cls.small_tree = nx.Graph()
  152. cls.small_tree.add_edge(1, 2)
  153. cls.small_tree.add_edge(2, 3)
  154. cls.small_tree.add_edge(3, 4)
  155. cls.small_tree.add_edge(1, 4)
  156. cls.small_tree.add_edge(2, 4)
  157. cls.small_tree.add_edge(4, 5)
  158. cls.small_tree.add_edge(5, 6)
  159. cls.small_tree.add_edge(5, 7)
  160. cls.small_tree.add_edge(6, 7)
  161. cls.deterministic_graph = nx.Graph()
  162. cls.deterministic_graph.add_edge(1, 2)
  163. cls.deterministic_graph.add_edge(1, 3)
  164. cls.deterministic_graph.add_edge(3, 4)
  165. cls.deterministic_graph.add_edge(2, 4)
  166. cls.deterministic_graph.add_edge(3, 5)
  167. cls.deterministic_graph.add_edge(4, 5)
  168. cls.deterministic_graph.add_edge(3, 6)
  169. cls.deterministic_graph.add_edge(5, 6)
  170. def test_petersen_graph(self):
  171. """Test Petersen graph tree decomposition result"""
  172. G = nx.petersen_graph()
  173. _, decomp = treewidth_min_fill_in(G)
  174. is_tree_decomp(G, decomp)
  175. def test_small_tree_treewidth(self):
  176. """Test if the computed treewidth of the known self.small_tree is 2"""
  177. G = self.small_tree
  178. # the order of removal should be [1,2,4]3[5,6,7]
  179. # (with [] denoting any order of the containing nodes)
  180. # resulting in treewidth 2 for the heuristic
  181. treewidth, _ = treewidth_min_fill_in(G)
  182. assert treewidth == 2
  183. def test_heuristic_abort(self):
  184. """Test if min_fill_in returns None for fully connected graph"""
  185. graph = {}
  186. for u in self.complete:
  187. graph[u] = set()
  188. for v in self.complete[u]:
  189. if u != v: # ignore self-loop
  190. graph[u].add(v)
  191. next_node = min_fill_in_heuristic(graph)
  192. if next_node is None:
  193. pass
  194. else:
  195. assert False
  196. def test_empty_graph(self):
  197. """Test empty graph"""
  198. G = nx.Graph()
  199. _, _ = treewidth_min_fill_in(G)
  200. def test_two_component_graph(self):
  201. G = nx.Graph()
  202. G.add_node(1)
  203. G.add_node(2)
  204. treewidth, _ = treewidth_min_fill_in(G)
  205. assert treewidth == 0
  206. def test_not_sortable_nodes(self):
  207. G = nx.Graph([(0, "a")])
  208. treewidth_min_fill_in(G)
  209. def test_heuristic_first_steps(self):
  210. """Test first steps of min_fill_in heuristic"""
  211. graph = {
  212. n: set(self.deterministic_graph[n]) - {n} for n in self.deterministic_graph
  213. }
  214. print(f"Graph {graph}:")
  215. elim_node = min_fill_in_heuristic(graph)
  216. steps = []
  217. while elim_node is not None:
  218. print(f"Removing {elim_node}:")
  219. steps.append(elim_node)
  220. nbrs = graph[elim_node]
  221. for u, v in itertools.permutations(nbrs, 2):
  222. if v not in graph[u]:
  223. graph[u].add(v)
  224. for u in graph:
  225. if elim_node in graph[u]:
  226. graph[u].remove(elim_node)
  227. del graph[elim_node]
  228. print(f"Graph {graph}:")
  229. elim_node = min_fill_in_heuristic(graph)
  230. # check only the first 2 elements for equality
  231. assert steps[:2] == [6, 5]