test_cycles.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. from itertools import chain, islice, tee
  2. from random import shuffle
  3. import pytest
  4. import networkx
  5. import networkx as nx
  6. from networkx.algorithms import find_cycle, minimum_cycle_basis
  7. from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE
  8. class TestCycles:
  9. @classmethod
  10. def setup_class(cls):
  11. G = networkx.Graph()
  12. nx.add_cycle(G, [0, 1, 2, 3])
  13. nx.add_cycle(G, [0, 3, 4, 5])
  14. nx.add_cycle(G, [0, 1, 6, 7, 8])
  15. G.add_edge(8, 9)
  16. cls.G = G
  17. def is_cyclic_permutation(self, a, b):
  18. n = len(a)
  19. if len(b) != n:
  20. return False
  21. l = a + a
  22. return any(l[i : i + n] == b for i in range(n))
  23. def test_cycle_basis(self):
  24. G = self.G
  25. cy = networkx.cycle_basis(G, 0)
  26. sort_cy = sorted(sorted(c) for c in cy)
  27. assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
  28. cy = networkx.cycle_basis(G, 1)
  29. sort_cy = sorted(sorted(c) for c in cy)
  30. assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
  31. cy = networkx.cycle_basis(G, 9)
  32. sort_cy = sorted(sorted(c) for c in cy)
  33. assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]]
  34. # test disconnected graphs
  35. nx.add_cycle(G, "ABC")
  36. cy = networkx.cycle_basis(G, 9)
  37. sort_cy = sorted(sorted(c) for c in cy[:-1]) + [sorted(cy[-1])]
  38. assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5], ["A", "B", "C"]]
  39. def test_cycle_basis2(self):
  40. with pytest.raises(nx.NetworkXNotImplemented):
  41. G = nx.DiGraph()
  42. cy = networkx.cycle_basis(G, 0)
  43. def test_cycle_basis3(self):
  44. with pytest.raises(nx.NetworkXNotImplemented):
  45. G = nx.MultiGraph()
  46. cy = networkx.cycle_basis(G, 0)
  47. def test_cycle_basis_self_loop(self):
  48. """Tests the function for graphs with self loops"""
  49. G = nx.Graph()
  50. nx.add_cycle(G, [0, 1, 2, 3])
  51. nx.add_cycle(G, [0, 0, 6, 2])
  52. cy = nx.cycle_basis(G)
  53. sort_cy = sorted(sorted(c) for c in cy)
  54. assert sort_cy == [[0], [0, 1, 2], [0, 2, 3], [0, 2, 6]]
  55. def test_simple_cycles(self):
  56. edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
  57. G = nx.DiGraph(edges)
  58. cc = sorted(nx.simple_cycles(G))
  59. ca = [[0], [0, 1, 2], [0, 2], [1, 2], [2]]
  60. assert len(cc) == len(ca)
  61. for c in cc:
  62. assert any(self.is_cyclic_permutation(c, rc) for rc in ca)
  63. def test_unsortable(self):
  64. # this test ensures that graphs whose nodes without an intrinsic
  65. # ordering do not cause issues
  66. G = nx.DiGraph()
  67. nx.add_cycle(G, ["a", 1])
  68. c = list(nx.simple_cycles(G))
  69. assert len(c) == 1
  70. def test_simple_cycles_small(self):
  71. G = nx.DiGraph()
  72. nx.add_cycle(G, [1, 2, 3])
  73. c = sorted(nx.simple_cycles(G))
  74. assert len(c) == 1
  75. assert self.is_cyclic_permutation(c[0], [1, 2, 3])
  76. nx.add_cycle(G, [10, 20, 30])
  77. cc = sorted(nx.simple_cycles(G))
  78. assert len(cc) == 2
  79. ca = [[1, 2, 3], [10, 20, 30]]
  80. for c in cc:
  81. assert any(self.is_cyclic_permutation(c, rc) for rc in ca)
  82. def test_simple_cycles_empty(self):
  83. G = nx.DiGraph()
  84. assert list(nx.simple_cycles(G)) == []
  85. def worst_case_graph(self, k):
  86. # see figure 1 in Johnson's paper
  87. # this graph has exactly 3k simple cycles
  88. G = nx.DiGraph()
  89. for n in range(2, k + 2):
  90. G.add_edge(1, n)
  91. G.add_edge(n, k + 2)
  92. G.add_edge(2 * k + 1, 1)
  93. for n in range(k + 2, 2 * k + 2):
  94. G.add_edge(n, 2 * k + 2)
  95. G.add_edge(n, n + 1)
  96. G.add_edge(2 * k + 3, k + 2)
  97. for n in range(2 * k + 3, 3 * k + 3):
  98. G.add_edge(2 * k + 2, n)
  99. G.add_edge(n, 3 * k + 3)
  100. G.add_edge(3 * k + 3, 2 * k + 2)
  101. return G
  102. def test_worst_case_graph(self):
  103. # see figure 1 in Johnson's paper
  104. for k in range(3, 10):
  105. G = self.worst_case_graph(k)
  106. l = len(list(nx.simple_cycles(G)))
  107. assert l == 3 * k
  108. def test_recursive_simple_and_not(self):
  109. for k in range(2, 10):
  110. G = self.worst_case_graph(k)
  111. cc = sorted(nx.simple_cycles(G))
  112. rcc = sorted(nx.recursive_simple_cycles(G))
  113. assert len(cc) == len(rcc)
  114. for c in cc:
  115. assert any(self.is_cyclic_permutation(c, r) for r in rcc)
  116. for rc in rcc:
  117. assert any(self.is_cyclic_permutation(rc, c) for c in cc)
  118. def test_simple_graph_with_reported_bug(self):
  119. G = nx.DiGraph()
  120. edges = [
  121. (0, 2),
  122. (0, 3),
  123. (1, 0),
  124. (1, 3),
  125. (2, 1),
  126. (2, 4),
  127. (3, 2),
  128. (3, 4),
  129. (4, 0),
  130. (4, 1),
  131. (4, 5),
  132. (5, 0),
  133. (5, 1),
  134. (5, 2),
  135. (5, 3),
  136. ]
  137. G.add_edges_from(edges)
  138. cc = sorted(nx.simple_cycles(G))
  139. assert len(cc) == 26
  140. rcc = sorted(nx.recursive_simple_cycles(G))
  141. assert len(cc) == len(rcc)
  142. for c in cc:
  143. assert any(self.is_cyclic_permutation(c, rc) for rc in rcc)
  144. for rc in rcc:
  145. assert any(self.is_cyclic_permutation(rc, c) for c in cc)
  146. def pairwise(iterable):
  147. a, b = tee(iterable)
  148. next(b, None)
  149. return zip(a, b)
  150. def cycle_edges(c):
  151. return pairwise(chain(c, islice(c, 1)))
  152. def directed_cycle_edgeset(c):
  153. return frozenset(cycle_edges(c))
  154. def undirected_cycle_edgeset(c):
  155. if len(c) == 1:
  156. return frozenset(cycle_edges(c))
  157. return frozenset(map(frozenset, cycle_edges(c)))
  158. def multigraph_cycle_edgeset(c):
  159. if len(c) <= 2:
  160. return frozenset(cycle_edges(c))
  161. else:
  162. return frozenset(map(frozenset, cycle_edges(c)))
  163. class TestCycleEnumeration:
  164. @staticmethod
  165. def K(n):
  166. return nx.complete_graph(n)
  167. @staticmethod
  168. def D(n):
  169. return nx.complete_graph(n).to_directed()
  170. @staticmethod
  171. def edgeset_function(g):
  172. if g.is_directed():
  173. return directed_cycle_edgeset
  174. elif g.is_multigraph():
  175. return multigraph_cycle_edgeset
  176. else:
  177. return undirected_cycle_edgeset
  178. def check_cycle(self, g, c, es, cache, source, original_c, length_bound, chordless):
  179. if length_bound is not None and len(c) > length_bound:
  180. raise RuntimeError(
  181. f"computed cycle {original_c} exceeds length bound {length_bound}"
  182. )
  183. if source == "computed":
  184. if es in cache:
  185. raise RuntimeError(
  186. f"computed cycle {original_c} has already been found!"
  187. )
  188. else:
  189. cache[es] = tuple(original_c)
  190. else:
  191. if es in cache:
  192. cache.pop(es)
  193. else:
  194. raise RuntimeError(f"expected cycle {original_c} was not computed")
  195. if not all(g.has_edge(*e) for e in es):
  196. raise RuntimeError(
  197. f"{source} claimed cycle {original_c} is not a cycle of g"
  198. )
  199. if chordless and len(g.subgraph(c).edges) > len(c):
  200. raise RuntimeError(f"{source} cycle {original_c} is not chordless")
  201. def check_cycle_algorithm(
  202. self,
  203. g,
  204. expected_cycles,
  205. length_bound=None,
  206. chordless=False,
  207. algorithm=None,
  208. ):
  209. if algorithm is None:
  210. algorithm = nx.chordless_cycles if chordless else nx.simple_cycles
  211. # note: we shuffle the labels of g to rule out accidentally-correct
  212. # behavior which occurred during the development of chordless cycle
  213. # enumeration algorithms
  214. relabel = list(range(len(g)))
  215. shuffle(relabel)
  216. label = dict(zip(g, relabel))
  217. unlabel = dict(zip(relabel, g))
  218. h = nx.relabel_nodes(g, label, copy=True)
  219. edgeset = self.edgeset_function(h)
  220. params = {}
  221. if length_bound is not None:
  222. params["length_bound"] = length_bound
  223. cycle_cache = {}
  224. for c in algorithm(h, **params):
  225. original_c = [unlabel[x] for x in c]
  226. es = edgeset(c)
  227. self.check_cycle(
  228. h, c, es, cycle_cache, "computed", original_c, length_bound, chordless
  229. )
  230. if isinstance(expected_cycles, int):
  231. if len(cycle_cache) != expected_cycles:
  232. raise RuntimeError(
  233. f"expected {expected_cycles} cycles, got {len(cycle_cache)}"
  234. )
  235. return
  236. for original_c in expected_cycles:
  237. c = [label[x] for x in original_c]
  238. es = edgeset(c)
  239. self.check_cycle(
  240. h, c, es, cycle_cache, "expected", original_c, length_bound, chordless
  241. )
  242. if len(cycle_cache):
  243. for c in cycle_cache.values():
  244. raise RuntimeError(
  245. f"computed cycle {c} is valid but not in the expected cycle set!"
  246. )
  247. def check_cycle_enumeration_integer_sequence(
  248. self,
  249. g_family,
  250. cycle_counts,
  251. length_bound=None,
  252. chordless=False,
  253. algorithm=None,
  254. ):
  255. for g, num_cycles in zip(g_family, cycle_counts):
  256. self.check_cycle_algorithm(
  257. g,
  258. num_cycles,
  259. length_bound=length_bound,
  260. chordless=chordless,
  261. algorithm=algorithm,
  262. )
  263. def test_directed_chordless_cycle_digons(self):
  264. g = nx.DiGraph()
  265. nx.add_cycle(g, range(5))
  266. nx.add_cycle(g, range(5)[::-1])
  267. g.add_edge(0, 0)
  268. expected_cycles = [(0,), (1, 2), (2, 3), (3, 4)]
  269. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  270. self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=2)
  271. expected_cycles = [c for c in expected_cycles if len(c) < 2]
  272. self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=1)
  273. def test_directed_chordless_cycle_undirected(self):
  274. g = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (5, 1), (0, 2)])
  275. expected_cycles = [(0, 2, 3, 4, 5), (1, 2, 3, 4, 5)]
  276. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  277. g = nx.DiGraph()
  278. nx.add_cycle(g, range(5))
  279. nx.add_cycle(g, range(4, 9))
  280. g.add_edge(7, 3)
  281. expected_cycles = [(0, 1, 2, 3, 4), (3, 4, 5, 6, 7), (4, 5, 6, 7, 8)]
  282. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  283. g.add_edge(3, 7)
  284. expected_cycles = [(0, 1, 2, 3, 4), (3, 7), (4, 5, 6, 7, 8)]
  285. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  286. expected_cycles = [(3, 7)]
  287. self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=4)
  288. g.remove_edge(7, 3)
  289. expected_cycles = [(0, 1, 2, 3, 4), (4, 5, 6, 7, 8)]
  290. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  291. g = nx.DiGraph((i, j) for i in range(10) for j in range(i))
  292. expected_cycles = []
  293. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  294. def test_chordless_cycles_directed(self):
  295. G = nx.DiGraph()
  296. nx.add_cycle(G, range(5))
  297. nx.add_cycle(G, range(4, 12))
  298. expected = [[*range(5)], [*range(4, 12)]]
  299. self.check_cycle_algorithm(G, expected, chordless=True)
  300. self.check_cycle_algorithm(
  301. G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
  302. )
  303. G.add_edge(7, 3)
  304. expected.append([*range(3, 8)])
  305. self.check_cycle_algorithm(G, expected, chordless=True)
  306. self.check_cycle_algorithm(
  307. G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
  308. )
  309. G.add_edge(3, 7)
  310. expected[-1] = [7, 3]
  311. self.check_cycle_algorithm(G, expected, chordless=True)
  312. self.check_cycle_algorithm(
  313. G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
  314. )
  315. expected.pop()
  316. G.remove_edge(7, 3)
  317. self.check_cycle_algorithm(G, expected, chordless=True)
  318. self.check_cycle_algorithm(
  319. G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
  320. )
  321. def test_directed_chordless_cycle_diclique(self):
  322. g_family = [self.D(n) for n in range(10)]
  323. expected_cycles = [(n * n - n) // 2 for n in range(10)]
  324. self.check_cycle_enumeration_integer_sequence(
  325. g_family, expected_cycles, chordless=True
  326. )
  327. expected_cycles = [(n * n - n) // 2 for n in range(10)]
  328. self.check_cycle_enumeration_integer_sequence(
  329. g_family, expected_cycles, length_bound=2
  330. )
  331. def test_directed_chordless_loop_blockade(self):
  332. g = nx.DiGraph((i, i) for i in range(10))
  333. nx.add_cycle(g, range(10))
  334. expected_cycles = [(i,) for i in range(10)]
  335. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  336. self.check_cycle_algorithm(g, expected_cycles, length_bound=1)
  337. g = nx.MultiDiGraph(g)
  338. g.add_edges_from((i, i) for i in range(0, 10, 2))
  339. expected_cycles = [(i,) for i in range(1, 10, 2)]
  340. self.check_cycle_algorithm(g, expected_cycles, chordless=True)
  341. def test_simple_cycles_notable_clique_sequences(self):
  342. # A000292: Number of labeled graphs on n+3 nodes that are triangles.
  343. g_family = [self.K(n) for n in range(2, 12)]
  344. expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220]
  345. self.check_cycle_enumeration_integer_sequence(
  346. g_family, expected, length_bound=3
  347. )
  348. def triangles(g, **kwargs):
  349. yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 3)
  350. # directed complete graphs have twice as many triangles thanks to reversal
  351. g_family = [self.D(n) for n in range(2, 12)]
  352. expected = [2 * e for e in expected]
  353. self.check_cycle_enumeration_integer_sequence(
  354. g_family, expected, length_bound=3, algorithm=triangles
  355. )
  356. def four_cycles(g, **kwargs):
  357. yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 4)
  358. # A050534: the number of 4-cycles in the complete graph K_{n+1}
  359. expected = [0, 0, 0, 3, 15, 45, 105, 210, 378, 630, 990]
  360. g_family = [self.K(n) for n in range(1, 12)]
  361. self.check_cycle_enumeration_integer_sequence(
  362. g_family, expected, length_bound=4, algorithm=four_cycles
  363. )
  364. # directed complete graphs have twice as many 4-cycles thanks to reversal
  365. expected = [2 * e for e in expected]
  366. g_family = [self.D(n) for n in range(1, 15)]
  367. self.check_cycle_enumeration_integer_sequence(
  368. g_family, expected, length_bound=4, algorithm=four_cycles
  369. )
  370. # A006231: the number of elementary circuits in a complete directed graph with n nodes
  371. expected = [0, 1, 5, 20, 84, 409, 2365]
  372. g_family = [self.D(n) for n in range(1, 8)]
  373. self.check_cycle_enumeration_integer_sequence(g_family, expected)
  374. # A002807: Number of cycles in the complete graph on n nodes K_{n}.
  375. expected = [0, 0, 0, 1, 7, 37, 197, 1172]
  376. g_family = [self.K(n) for n in range(8)]
  377. self.check_cycle_enumeration_integer_sequence(g_family, expected)
  378. def test_directed_chordless_cycle_parallel_multiedges(self):
  379. g = nx.MultiGraph()
  380. nx.add_cycle(g, range(5))
  381. expected = [[*range(5)]]
  382. self.check_cycle_algorithm(g, expected, chordless=True)
  383. nx.add_cycle(g, range(5))
  384. expected = [*cycle_edges(range(5))]
  385. self.check_cycle_algorithm(g, expected, chordless=True)
  386. nx.add_cycle(g, range(5))
  387. expected = []
  388. self.check_cycle_algorithm(g, expected, chordless=True)
  389. g = nx.MultiDiGraph()
  390. nx.add_cycle(g, range(5))
  391. expected = [[*range(5)]]
  392. self.check_cycle_algorithm(g, expected, chordless=True)
  393. nx.add_cycle(g, range(5))
  394. self.check_cycle_algorithm(g, [], chordless=True)
  395. nx.add_cycle(g, range(5))
  396. self.check_cycle_algorithm(g, [], chordless=True)
  397. g = nx.MultiDiGraph()
  398. nx.add_cycle(g, range(5))
  399. nx.add_cycle(g, range(5)[::-1])
  400. expected = [*cycle_edges(range(5))]
  401. self.check_cycle_algorithm(g, expected, chordless=True)
  402. nx.add_cycle(g, range(5))
  403. self.check_cycle_algorithm(g, [], chordless=True)
  404. def test_chordless_cycles_graph(self):
  405. G = nx.Graph()
  406. nx.add_cycle(G, range(5))
  407. nx.add_cycle(G, range(4, 12))
  408. expected = [[*range(5)], [*range(4, 12)]]
  409. self.check_cycle_algorithm(G, expected, chordless=True)
  410. self.check_cycle_algorithm(
  411. G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
  412. )
  413. G.add_edge(7, 3)
  414. expected.append([*range(3, 8)])
  415. expected.append([4, 3, 7, 8, 9, 10, 11])
  416. self.check_cycle_algorithm(G, expected, chordless=True)
  417. self.check_cycle_algorithm(
  418. G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True
  419. )
  420. def test_chordless_cycles_giant_hamiltonian(self):
  421. # ... o - e - o - e - o ... # o = odd, e = even
  422. # ... ---/ \-----/ \--- ... # <-- "long" edges
  423. #
  424. # each long edge belongs to exactly one triangle, and one giant cycle
  425. # of length n/2. The remaining edges each belong to a triangle
  426. n = 1000
  427. assert n % 2 == 0
  428. G = nx.Graph()
  429. for v in range(n):
  430. if not v % 2:
  431. G.add_edge(v, (v + 2) % n)
  432. G.add_edge(v, (v + 1) % n)
  433. expected = [[*range(0, n, 2)]] + [
  434. [x % n for x in range(i, i + 3)] for i in range(0, n, 2)
  435. ]
  436. self.check_cycle_algorithm(G, expected, chordless=True)
  437. self.check_cycle_algorithm(
  438. G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
  439. )
  440. # ... o -> e -> o -> e -> o ... # o = odd, e = even
  441. # ... <---/ \---<---/ \---< ... # <-- "long" edges
  442. #
  443. # this time, we orient the short and long edges in opposition
  444. # the cycle structure of this graph is the same, but we need to reverse
  445. # the long one in our representation. Also, we need to drop the size
  446. # because our partitioning algorithm uses strongly connected components
  447. # instead of separating graphs by their strong articulation points
  448. n = 100
  449. assert n % 2 == 0
  450. G = nx.DiGraph()
  451. for v in range(n):
  452. G.add_edge(v, (v + 1) % n)
  453. if not v % 2:
  454. G.add_edge((v + 2) % n, v)
  455. expected = [[*range(n - 2, -2, -2)]] + [
  456. [x % n for x in range(i, i + 3)] for i in range(0, n, 2)
  457. ]
  458. self.check_cycle_algorithm(G, expected, chordless=True)
  459. self.check_cycle_algorithm(
  460. G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True
  461. )
  462. def test_simple_cycles_acyclic_tournament(self):
  463. n = 10
  464. G = nx.DiGraph((x, y) for x in range(n) for y in range(x))
  465. self.check_cycle_algorithm(G, [])
  466. self.check_cycle_algorithm(G, [], chordless=True)
  467. for k in range(n + 1):
  468. self.check_cycle_algorithm(G, [], length_bound=k)
  469. self.check_cycle_algorithm(G, [], length_bound=k, chordless=True)
  470. def test_simple_cycles_graph(self):
  471. testG = nx.cycle_graph(8)
  472. cyc1 = tuple(range(8))
  473. self.check_cycle_algorithm(testG, [cyc1])
  474. testG.add_edge(4, -1)
  475. nx.add_path(testG, [3, -2, -3, -4])
  476. self.check_cycle_algorithm(testG, [cyc1])
  477. testG.update(nx.cycle_graph(range(8, 16)))
  478. cyc2 = tuple(range(8, 16))
  479. self.check_cycle_algorithm(testG, [cyc1, cyc2])
  480. testG.update(nx.cycle_graph(range(4, 12)))
  481. cyc3 = tuple(range(4, 12))
  482. expected = {
  483. (0, 1, 2, 3, 4, 5, 6, 7), # cyc1
  484. (8, 9, 10, 11, 12, 13, 14, 15), # cyc2
  485. (4, 5, 6, 7, 8, 9, 10, 11), # cyc3
  486. (4, 5, 6, 7, 8, 15, 14, 13, 12, 11), # cyc2 + cyc3
  487. (0, 1, 2, 3, 4, 11, 10, 9, 8, 7), # cyc1 + cyc3
  488. (0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7), # cyc1 + cyc2 + cyc3
  489. }
  490. self.check_cycle_algorithm(testG, expected)
  491. assert len(expected) == (2**3 - 1) - 1 # 1 disjoint comb: cyc1 + cyc2
  492. # Basis size = 5 (2 loops overlapping gives 5 small loops
  493. # E
  494. # / \ Note: A-F = 10-15
  495. # 1-2-3-4-5
  496. # / | | \ cyc1=012DAB -- left
  497. # 0 D F 6 cyc2=234E -- top
  498. # \ | | / cyc3=45678F -- right
  499. # B-A-9-8-7 cyc4=89AC -- bottom
  500. # \ / cyc5=234F89AD -- middle
  501. # C
  502. #
  503. # combinations of 5 basis elements: 2^5 - 1 (one includes no cycles)
  504. #
  505. # disjoint combs: (11 total) not simple cycles
  506. # Any pair not including cyc5 => choose(4, 2) = 6
  507. # Any triple not including cyc5 => choose(4, 3) = 4
  508. # Any quad not including cyc5 => choose(4, 4) = 1
  509. #
  510. # we expect 31 - 11 = 20 simple cycles
  511. #
  512. testG = nx.cycle_graph(12)
  513. testG.update(nx.cycle_graph([12, 10, 13, 2, 14, 4, 15, 8]).edges)
  514. expected = (2**5 - 1) - 11 # 11 disjoint combinations
  515. self.check_cycle_algorithm(testG, expected)
  516. def test_simple_cycles_bounded(self):
  517. # iteratively construct a cluster of nested cycles running in the same direction
  518. # there should be one cycle of every length
  519. d = nx.DiGraph()
  520. expected = []
  521. for n in range(10):
  522. nx.add_cycle(d, range(n))
  523. expected.append(n)
  524. for k, e in enumerate(expected):
  525. self.check_cycle_algorithm(d, e, length_bound=k)
  526. # iteratively construct a path of undirected cycles, connected at articulation
  527. # points. there should be one cycle of every length except 2: no digons
  528. g = nx.Graph()
  529. top = 0
  530. expected = []
  531. for n in range(10):
  532. expected.append(n if n < 2 else n - 1)
  533. if n == 2:
  534. # no digons in undirected graphs
  535. continue
  536. nx.add_cycle(g, range(top, top + n))
  537. top += n
  538. for k, e in enumerate(expected):
  539. self.check_cycle_algorithm(g, e, length_bound=k)
  540. def test_simple_cycles_bound_corner_cases(self):
  541. G = nx.cycle_graph(4)
  542. DG = nx.cycle_graph(4, create_using=nx.DiGraph)
  543. assert list(nx.simple_cycles(G, length_bound=0)) == []
  544. assert list(nx.simple_cycles(DG, length_bound=0)) == []
  545. assert list(nx.chordless_cycles(G, length_bound=0)) == []
  546. assert list(nx.chordless_cycles(DG, length_bound=0)) == []
  547. def test_simple_cycles_bound_error(self):
  548. with pytest.raises(ValueError):
  549. G = nx.DiGraph()
  550. for c in nx.simple_cycles(G, -1):
  551. assert False
  552. with pytest.raises(ValueError):
  553. G = nx.Graph()
  554. for c in nx.simple_cycles(G, -1):
  555. assert False
  556. with pytest.raises(ValueError):
  557. G = nx.Graph()
  558. for c in nx.chordless_cycles(G, -1):
  559. assert False
  560. with pytest.raises(ValueError):
  561. G = nx.DiGraph()
  562. for c in nx.chordless_cycles(G, -1):
  563. assert False
  564. def test_chordless_cycles_clique(self):
  565. g_family = [self.K(n) for n in range(2, 15)]
  566. expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 286, 364]
  567. self.check_cycle_enumeration_integer_sequence(
  568. g_family, expected, chordless=True
  569. )
  570. # directed cliques have as many digons as undirected graphs have edges
  571. expected = [(n * n - n) // 2 for n in range(15)]
  572. g_family = [self.D(n) for n in range(15)]
  573. self.check_cycle_enumeration_integer_sequence(
  574. g_family, expected, chordless=True
  575. )
  576. # These tests might fail with hash randomization since they depend on
  577. # edge_dfs. For more information, see the comments in:
  578. # networkx/algorithms/traversal/tests/test_edgedfs.py
  579. class TestFindCycle:
  580. @classmethod
  581. def setup_class(cls):
  582. cls.nodes = [0, 1, 2, 3]
  583. cls.edges = [(-1, 0), (0, 1), (1, 0), (1, 0), (2, 1), (3, 1)]
  584. def test_graph_nocycle(self):
  585. G = nx.Graph(self.edges)
  586. pytest.raises(nx.exception.NetworkXNoCycle, find_cycle, G, self.nodes)
  587. def test_graph_cycle(self):
  588. G = nx.Graph(self.edges)
  589. G.add_edge(2, 0)
  590. x = list(find_cycle(G, self.nodes))
  591. x_ = [(0, 1), (1, 2), (2, 0)]
  592. assert x == x_
  593. def test_graph_orientation_none(self):
  594. G = nx.Graph(self.edges)
  595. G.add_edge(2, 0)
  596. x = list(find_cycle(G, self.nodes, orientation=None))
  597. x_ = [(0, 1), (1, 2), (2, 0)]
  598. assert x == x_
  599. def test_graph_orientation_original(self):
  600. G = nx.Graph(self.edges)
  601. G.add_edge(2, 0)
  602. x = list(find_cycle(G, self.nodes, orientation="original"))
  603. x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 0, FORWARD)]
  604. assert x == x_
  605. def test_digraph(self):
  606. G = nx.DiGraph(self.edges)
  607. x = list(find_cycle(G, self.nodes))
  608. x_ = [(0, 1), (1, 0)]
  609. assert x == x_
  610. def test_digraph_orientation_none(self):
  611. G = nx.DiGraph(self.edges)
  612. x = list(find_cycle(G, self.nodes, orientation=None))
  613. x_ = [(0, 1), (1, 0)]
  614. assert x == x_
  615. def test_digraph_orientation_original(self):
  616. G = nx.DiGraph(self.edges)
  617. x = list(find_cycle(G, self.nodes, orientation="original"))
  618. x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
  619. assert x == x_
  620. def test_multigraph(self):
  621. G = nx.MultiGraph(self.edges)
  622. x = list(find_cycle(G, self.nodes))
  623. x_ = [(0, 1, 0), (1, 0, 1)] # or (1, 0, 2)
  624. # Hash randomization...could be any edge.
  625. assert x[0] == x_[0]
  626. assert x[1][:2] == x_[1][:2]
  627. def test_multidigraph(self):
  628. G = nx.MultiDiGraph(self.edges)
  629. x = list(find_cycle(G, self.nodes))
  630. x_ = [(0, 1, 0), (1, 0, 0)] # (1, 0, 1)
  631. assert x[0] == x_[0]
  632. assert x[1][:2] == x_[1][:2]
  633. def test_digraph_ignore(self):
  634. G = nx.DiGraph(self.edges)
  635. x = list(find_cycle(G, self.nodes, orientation="ignore"))
  636. x_ = [(0, 1, FORWARD), (1, 0, FORWARD)]
  637. assert x == x_
  638. def test_digraph_reverse(self):
  639. G = nx.DiGraph(self.edges)
  640. x = list(find_cycle(G, self.nodes, orientation="reverse"))
  641. x_ = [(1, 0, REVERSE), (0, 1, REVERSE)]
  642. assert x == x_
  643. def test_multidigraph_ignore(self):
  644. G = nx.MultiDiGraph(self.edges)
  645. x = list(find_cycle(G, self.nodes, orientation="ignore"))
  646. x_ = [(0, 1, 0, FORWARD), (1, 0, 0, FORWARD)] # or (1, 0, 1, 1)
  647. assert x[0] == x_[0]
  648. assert x[1][:2] == x_[1][:2]
  649. assert x[1][3] == x_[1][3]
  650. def test_multidigraph_ignore2(self):
  651. # Loop traversed an edge while ignoring its orientation.
  652. G = nx.MultiDiGraph([(0, 1), (1, 2), (1, 2)])
  653. x = list(find_cycle(G, [0, 1, 2], orientation="ignore"))
  654. x_ = [(1, 2, 0, FORWARD), (1, 2, 1, REVERSE)]
  655. assert x == x_
  656. def test_multidigraph_original(self):
  657. # Node 2 doesn't need to be searched again from visited from 4.
  658. # The goal here is to cover the case when 2 to be researched from 4,
  659. # when 4 is visited from the first time (so we must make sure that 4
  660. # is not visited from 2, and hence, we respect the edge orientation).
  661. G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 3), (4, 2)])
  662. pytest.raises(
  663. nx.exception.NetworkXNoCycle,
  664. find_cycle,
  665. G,
  666. [0, 1, 2, 3, 4],
  667. orientation="original",
  668. )
  669. def test_dag(self):
  670. G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
  671. pytest.raises(
  672. nx.exception.NetworkXNoCycle, find_cycle, G, orientation="original"
  673. )
  674. x = list(find_cycle(G, orientation="ignore"))
  675. assert x == [(0, 1, FORWARD), (1, 2, FORWARD), (0, 2, REVERSE)]
  676. def test_prev_explored(self):
  677. # https://github.com/networkx/networkx/issues/2323
  678. G = nx.DiGraph()
  679. G.add_edges_from([(1, 0), (2, 0), (1, 2), (2, 1)])
  680. pytest.raises(nx.NetworkXNoCycle, find_cycle, G, source=0)
  681. x = list(nx.find_cycle(G, 1))
  682. x_ = [(1, 2), (2, 1)]
  683. assert x == x_
  684. x = list(nx.find_cycle(G, 2))
  685. x_ = [(2, 1), (1, 2)]
  686. assert x == x_
  687. x = list(nx.find_cycle(G))
  688. x_ = [(1, 2), (2, 1)]
  689. assert x == x_
  690. def test_no_cycle(self):
  691. # https://github.com/networkx/networkx/issues/2439
  692. G = nx.DiGraph()
  693. G.add_edges_from([(1, 2), (2, 0), (3, 1), (3, 2)])
  694. pytest.raises(nx.NetworkXNoCycle, find_cycle, G, source=0)
  695. pytest.raises(nx.NetworkXNoCycle, find_cycle, G)
  696. def assert_basis_equal(a, b):
  697. assert sorted(a) == sorted(b)
  698. class TestMinimumCycles:
  699. @classmethod
  700. def setup_class(cls):
  701. T = nx.Graph()
  702. nx.add_cycle(T, [1, 2, 3, 4], weight=1)
  703. T.add_edge(2, 4, weight=5)
  704. cls.diamond_graph = T
  705. def test_unweighted_diamond(self):
  706. mcb = minimum_cycle_basis(self.diamond_graph)
  707. assert_basis_equal([sorted(c) for c in mcb], [[1, 2, 4], [2, 3, 4]])
  708. def test_weighted_diamond(self):
  709. mcb = minimum_cycle_basis(self.diamond_graph, weight="weight")
  710. assert_basis_equal([sorted(c) for c in mcb], [[1, 2, 4], [1, 2, 3, 4]])
  711. def test_dimensionality(self):
  712. # checks |MCB|=|E|-|V|+|NC|
  713. ntrial = 10
  714. for _ in range(ntrial):
  715. rg = nx.erdos_renyi_graph(10, 0.3)
  716. nnodes = rg.number_of_nodes()
  717. nedges = rg.number_of_edges()
  718. ncomp = nx.number_connected_components(rg)
  719. dim_mcb = len(minimum_cycle_basis(rg))
  720. assert dim_mcb == nedges - nnodes + ncomp
  721. def test_complete_graph(self):
  722. cg = nx.complete_graph(5)
  723. mcb = minimum_cycle_basis(cg)
  724. assert all(len(cycle) == 3 for cycle in mcb)
  725. def test_tree_graph(self):
  726. tg = nx.balanced_tree(3, 3)
  727. assert not minimum_cycle_basis(tg)