from itertools import chain, islice, tee from random import shuffle import pytest import networkx import networkx as nx from networkx.algorithms import find_cycle, minimum_cycle_basis from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE class TestCycles: @classmethod def setup_class(cls): G = networkx.Graph() nx.add_cycle(G, [0, 1, 2, 3]) nx.add_cycle(G, [0, 3, 4, 5]) nx.add_cycle(G, [0, 1, 6, 7, 8]) G.add_edge(8, 9) cls.G = G def is_cyclic_permutation(self, a, b): n = len(a) if len(b) != n: return False l = a + a return any(l[i : i + n] == b for i in range(n)) def test_cycle_basis(self): G = self.G cy = networkx.cycle_basis(G, 0) sort_cy = sorted(sorted(c) for c in cy) assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]] cy = networkx.cycle_basis(G, 1) sort_cy = sorted(sorted(c) for c in cy) assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]] cy = networkx.cycle_basis(G, 9) sort_cy = sorted(sorted(c) for c in cy) assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]] # test disconnected graphs nx.add_cycle(G, "ABC") cy = networkx.cycle_basis(G, 9) sort_cy = sorted(sorted(c) for c in cy[:-1]) + [sorted(cy[-1])] assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5], ["A", "B", "C"]] def test_cycle_basis2(self): with pytest.raises(nx.NetworkXNotImplemented): G = nx.DiGraph() cy = networkx.cycle_basis(G, 0) def test_cycle_basis3(self): with pytest.raises(nx.NetworkXNotImplemented): G = nx.MultiGraph() cy = networkx.cycle_basis(G, 0) def test_cycle_basis_self_loop(self): """Tests the function for graphs with self loops""" G = nx.Graph() nx.add_cycle(G, [0, 1, 2, 3]) nx.add_cycle(G, [0, 0, 6, 2]) cy = nx.cycle_basis(G) sort_cy = sorted(sorted(c) for c in cy) assert sort_cy == [[0], [0, 1, 2], [0, 2, 3], [0, 2, 6]] def test_simple_cycles(self): edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] G = nx.DiGraph(edges) cc = sorted(nx.simple_cycles(G)) ca = [[0], [0, 1, 2], [0, 2], [1, 2], [2]] assert len(cc) == len(ca) for c in cc: assert any(self.is_cyclic_permutation(c, rc) for rc in ca) def test_unsortable(self): # this test ensures that graphs whose nodes without an intrinsic # ordering do not cause issues G = nx.DiGraph() nx.add_cycle(G, ["a", 1]) c = list(nx.simple_cycles(G)) assert len(c) == 1 def test_simple_cycles_small(self): G = nx.DiGraph() nx.add_cycle(G, [1, 2, 3]) c = sorted(nx.simple_cycles(G)) assert len(c) == 1 assert self.is_cyclic_permutation(c[0], [1, 2, 3]) nx.add_cycle(G, [10, 20, 30]) cc = sorted(nx.simple_cycles(G)) assert len(cc) == 2 ca = [[1, 2, 3], [10, 20, 30]] for c in cc: assert any(self.is_cyclic_permutation(c, rc) for rc in ca) def test_simple_cycles_empty(self): G = nx.DiGraph() assert list(nx.simple_cycles(G)) == [] def worst_case_graph(self, k): # see figure 1 in Johnson's paper # this graph has exactly 3k simple cycles G = nx.DiGraph() for n in range(2, k + 2): G.add_edge(1, n) G.add_edge(n, k + 2) G.add_edge(2 * k + 1, 1) for n in range(k + 2, 2 * k + 2): G.add_edge(n, 2 * k + 2) G.add_edge(n, n + 1) G.add_edge(2 * k + 3, k + 2) for n in range(2 * k + 3, 3 * k + 3): G.add_edge(2 * k + 2, n) G.add_edge(n, 3 * k + 3) G.add_edge(3 * k + 3, 2 * k + 2) return G def test_worst_case_graph(self): # see figure 1 in Johnson's paper for k in range(3, 10): G = self.worst_case_graph(k) l = len(list(nx.simple_cycles(G))) assert l == 3 * k def test_recursive_simple_and_not(self): for k in range(2, 10): G = self.worst_case_graph(k) cc = sorted(nx.simple_cycles(G)) rcc = sorted(nx.recursive_simple_cycles(G)) assert len(cc) == len(rcc) for c in cc: assert any(self.is_cyclic_permutation(c, r) for r in rcc) for rc in rcc: assert any(self.is_cyclic_permutation(rc, c) for c in cc) def test_simple_graph_with_reported_bug(self): G = nx.DiGraph() edges = [ (0, 2), (0, 3), (1, 0), (1, 3), (2, 1), (2, 4), (3, 2), (3, 4), (4, 0), (4, 1), (4, 5), (5, 0), (5, 1), (5, 2), (5, 3), ] G.add_edges_from(edges) cc = sorted(nx.simple_cycles(G)) assert len(cc) == 26 rcc = sorted(nx.recursive_simple_cycles(G)) assert len(cc) == len(rcc) for c in cc: assert any(self.is_cyclic_permutation(c, rc) for rc in rcc) for rc in rcc: assert any(self.is_cyclic_permutation(rc, c) for c in cc) def pairwise(iterable): a, b = tee(iterable) next(b, None) return zip(a, b) def cycle_edges(c): return pairwise(chain(c, islice(c, 1))) def directed_cycle_edgeset(c): return frozenset(cycle_edges(c)) def undirected_cycle_edgeset(c): if len(c) == 1: return frozenset(cycle_edges(c)) return frozenset(map(frozenset, cycle_edges(c))) def multigraph_cycle_edgeset(c): if len(c) <= 2: return frozenset(cycle_edges(c)) else: return frozenset(map(frozenset, cycle_edges(c))) class TestCycleEnumeration: @staticmethod def K(n): return nx.complete_graph(n) @staticmethod def D(n): return nx.complete_graph(n).to_directed() @staticmethod def edgeset_function(g): if g.is_directed(): return directed_cycle_edgeset elif g.is_multigraph(): return multigraph_cycle_edgeset else: return undirected_cycle_edgeset def check_cycle(self, g, c, es, cache, source, original_c, length_bound, chordless): if length_bound is not None and len(c) > length_bound: raise RuntimeError( f"computed cycle {original_c} exceeds length bound {length_bound}" ) if source == "computed": if es in cache: raise RuntimeError( f"computed cycle {original_c} has already been found!" ) else: cache[es] = tuple(original_c) else: if es in cache: cache.pop(es) else: raise RuntimeError(f"expected cycle {original_c} was not computed") if not all(g.has_edge(*e) for e in es): raise RuntimeError( f"{source} claimed cycle {original_c} is not a cycle of g" ) if chordless and len(g.subgraph(c).edges) > len(c): raise RuntimeError(f"{source} cycle {original_c} is not chordless") def check_cycle_algorithm( self, g, expected_cycles, length_bound=None, chordless=False, algorithm=None, ): if algorithm is None: algorithm = nx.chordless_cycles if chordless else nx.simple_cycles # note: we shuffle the labels of g to rule out accidentally-correct # behavior which occurred during the development of chordless cycle # enumeration algorithms relabel = list(range(len(g))) shuffle(relabel) label = dict(zip(g, relabel)) unlabel = dict(zip(relabel, g)) h = nx.relabel_nodes(g, label, copy=True) edgeset = self.edgeset_function(h) params = {} if length_bound is not None: params["length_bound"] = length_bound cycle_cache = {} for c in algorithm(h, **params): original_c = [unlabel[x] for x in c] es = edgeset(c) self.check_cycle( h, c, es, cycle_cache, "computed", original_c, length_bound, chordless ) if isinstance(expected_cycles, int): if len(cycle_cache) != expected_cycles: raise RuntimeError( f"expected {expected_cycles} cycles, got {len(cycle_cache)}" ) return for original_c in expected_cycles: c = [label[x] for x in original_c] es = edgeset(c) self.check_cycle( h, c, es, cycle_cache, "expected", original_c, length_bound, chordless ) if len(cycle_cache): for c in cycle_cache.values(): raise RuntimeError( f"computed cycle {c} is valid but not in the expected cycle set!" ) def check_cycle_enumeration_integer_sequence( self, g_family, cycle_counts, length_bound=None, chordless=False, algorithm=None, ): for g, num_cycles in zip(g_family, cycle_counts): self.check_cycle_algorithm( g, num_cycles, length_bound=length_bound, chordless=chordless, algorithm=algorithm, ) def test_directed_chordless_cycle_digons(self): g = nx.DiGraph() nx.add_cycle(g, range(5)) nx.add_cycle(g, range(5)[::-1]) g.add_edge(0, 0) expected_cycles = [(0,), (1, 2), (2, 3), (3, 4)] self.check_cycle_algorithm(g, expected_cycles, chordless=True) self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=2) expected_cycles = [c for c in expected_cycles if len(c) < 2] self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=1) def test_directed_chordless_cycle_undirected(self): g = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (5, 1), (0, 2)]) expected_cycles = [(0, 2, 3, 4, 5), (1, 2, 3, 4, 5)] self.check_cycle_algorithm(g, expected_cycles, chordless=True) g = nx.DiGraph() nx.add_cycle(g, range(5)) nx.add_cycle(g, range(4, 9)) g.add_edge(7, 3) expected_cycles = [(0, 1, 2, 3, 4), (3, 4, 5, 6, 7), (4, 5, 6, 7, 8)] self.check_cycle_algorithm(g, expected_cycles, chordless=True) g.add_edge(3, 7) expected_cycles = [(0, 1, 2, 3, 4), (3, 7), (4, 5, 6, 7, 8)] self.check_cycle_algorithm(g, expected_cycles, chordless=True) expected_cycles = [(3, 7)] self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=4) g.remove_edge(7, 3) expected_cycles = [(0, 1, 2, 3, 4), (4, 5, 6, 7, 8)] self.check_cycle_algorithm(g, expected_cycles, chordless=True) g = nx.DiGraph((i, j) for i in range(10) for j in range(i)) expected_cycles = [] self.check_cycle_algorithm(g, expected_cycles, chordless=True) def test_chordless_cycles_directed(self): G = nx.DiGraph() nx.add_cycle(G, range(5)) nx.add_cycle(G, range(4, 12)) expected = [[*range(5)], [*range(4, 12)]] self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True ) G.add_edge(7, 3) expected.append([*range(3, 8)]) self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True ) G.add_edge(3, 7) expected[-1] = [7, 3] self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True ) expected.pop() G.remove_edge(7, 3) self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True ) def test_directed_chordless_cycle_diclique(self): g_family = [self.D(n) for n in range(10)] expected_cycles = [(n * n - n) // 2 for n in range(10)] self.check_cycle_enumeration_integer_sequence( g_family, expected_cycles, chordless=True ) expected_cycles = [(n * n - n) // 2 for n in range(10)] self.check_cycle_enumeration_integer_sequence( g_family, expected_cycles, length_bound=2 ) def test_directed_chordless_loop_blockade(self): g = nx.DiGraph((i, i) for i in range(10)) nx.add_cycle(g, range(10)) expected_cycles = [(i,) for i in range(10)] self.check_cycle_algorithm(g, expected_cycles, chordless=True) self.check_cycle_algorithm(g, expected_cycles, length_bound=1) g = nx.MultiDiGraph(g) g.add_edges_from((i, i) for i in range(0, 10, 2)) expected_cycles = [(i,) for i in range(1, 10, 2)] self.check_cycle_algorithm(g, expected_cycles, chordless=True) def test_simple_cycles_notable_clique_sequences(self): # A000292: Number of labeled graphs on n+3 nodes that are triangles. g_family = [self.K(n) for n in range(2, 12)] expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220] self.check_cycle_enumeration_integer_sequence( g_family, expected, length_bound=3 ) def triangles(g, **kwargs): yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 3) # directed complete graphs have twice as many triangles thanks to reversal g_family = [self.D(n) for n in range(2, 12)] expected = [2 * e for e in expected] self.check_cycle_enumeration_integer_sequence( g_family, expected, length_bound=3, algorithm=triangles ) def four_cycles(g, **kwargs): yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 4) # A050534: the number of 4-cycles in the complete graph K_{n+1} expected = [0, 0, 0, 3, 15, 45, 105, 210, 378, 630, 990] g_family = [self.K(n) for n in range(1, 12)] self.check_cycle_enumeration_integer_sequence( g_family, expected, length_bound=4, algorithm=four_cycles ) # directed complete graphs have twice as many 4-cycles thanks to reversal expected = [2 * e for e in expected] g_family = [self.D(n) for n in range(1, 15)] self.check_cycle_enumeration_integer_sequence( g_family, expected, length_bound=4, algorithm=four_cycles ) # A006231: the number of elementary circuits in a complete directed graph with n nodes expected = [0, 1, 5, 20, 84, 409, 2365] g_family = [self.D(n) for n in range(1, 8)] self.check_cycle_enumeration_integer_sequence(g_family, expected) # A002807: Number of cycles in the complete graph on n nodes K_{n}. expected = [0, 0, 0, 1, 7, 37, 197, 1172] g_family = [self.K(n) for n in range(8)] self.check_cycle_enumeration_integer_sequence(g_family, expected) def test_directed_chordless_cycle_parallel_multiedges(self): g = nx.MultiGraph() nx.add_cycle(g, range(5)) expected = [[*range(5)]] self.check_cycle_algorithm(g, expected, chordless=True) nx.add_cycle(g, range(5)) expected = [*cycle_edges(range(5))] self.check_cycle_algorithm(g, expected, chordless=True) nx.add_cycle(g, range(5)) expected = [] self.check_cycle_algorithm(g, expected, chordless=True) g = nx.MultiDiGraph() nx.add_cycle(g, range(5)) expected = [[*range(5)]] self.check_cycle_algorithm(g, expected, chordless=True) nx.add_cycle(g, range(5)) self.check_cycle_algorithm(g, [], chordless=True) nx.add_cycle(g, range(5)) self.check_cycle_algorithm(g, [], chordless=True) g = nx.MultiDiGraph() nx.add_cycle(g, range(5)) nx.add_cycle(g, range(5)[::-1]) expected = [*cycle_edges(range(5))] self.check_cycle_algorithm(g, expected, chordless=True) nx.add_cycle(g, range(5)) self.check_cycle_algorithm(g, [], chordless=True) def test_chordless_cycles_graph(self): G = nx.Graph() nx.add_cycle(G, range(5)) nx.add_cycle(G, range(4, 12)) expected = [[*range(5)], [*range(4, 12)]] self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True ) G.add_edge(7, 3) expected.append([*range(3, 8)]) expected.append([4, 3, 7, 8, 9, 10, 11]) self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True ) def test_chordless_cycles_giant_hamiltonian(self): # ... o - e - o - e - o ... # o = odd, e = even # ... ---/ \-----/ \--- ... # <-- "long" edges # # each long edge belongs to exactly one triangle, and one giant cycle # of length n/2. The remaining edges each belong to a triangle n = 1000 assert n % 2 == 0 G = nx.Graph() for v in range(n): if not v % 2: G.add_edge(v, (v + 2) % n) G.add_edge(v, (v + 1) % n) expected = [[*range(0, n, 2)]] + [ [x % n for x in range(i, i + 3)] for i in range(0, n, 2) ] self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True ) # ... o -> e -> o -> e -> o ... # o = odd, e = even # ... <---/ \---<---/ \---< ... # <-- "long" edges # # this time, we orient the short and long edges in opposition # the cycle structure of this graph is the same, but we need to reverse # the long one in our representation. Also, we need to drop the size # because our partitioning algorithm uses strongly connected components # instead of separating graphs by their strong articulation points n = 100 assert n % 2 == 0 G = nx.DiGraph() for v in range(n): G.add_edge(v, (v + 1) % n) if not v % 2: G.add_edge((v + 2) % n, v) expected = [[*range(n - 2, -2, -2)]] + [ [x % n for x in range(i, i + 3)] for i in range(0, n, 2) ] self.check_cycle_algorithm(G, expected, chordless=True) self.check_cycle_algorithm( G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True ) def test_simple_cycles_acyclic_tournament(self): n = 10 G = nx.DiGraph((x, y) for x in range(n) for y in range(x)) self.check_cycle_algorithm(G, []) self.check_cycle_algorithm(G, [], chordless=True) for k in range(n + 1): self.check_cycle_algorithm(G, [], length_bound=k) self.check_cycle_algorithm(G, [], length_bound=k, chordless=True) def test_simple_cycles_graph(self): testG = nx.cycle_graph(8) cyc1 = tuple(range(8)) self.check_cycle_algorithm(testG, [cyc1]) testG.add_edge(4, -1) nx.add_path(testG, [3, -2, -3, -4]) self.check_cycle_algorithm(testG, [cyc1]) testG.update(nx.cycle_graph(range(8, 16))) cyc2 = tuple(range(8, 16)) self.check_cycle_algorithm(testG, [cyc1, cyc2]) testG.update(nx.cycle_graph(range(4, 12))) cyc3 = tuple(range(4, 12)) expected = { (0, 1, 2, 3, 4, 5, 6, 7), # cyc1 (8, 9, 10, 11, 12, 13, 14, 15), # cyc2 (4, 5, 6, 7, 8, 9, 10, 11), # cyc3 (4, 5, 6, 7, 8, 15, 14, 13, 12, 11), # cyc2 + cyc3 (0, 1, 2, 3, 4, 11, 10, 9, 8, 7), # cyc1 + cyc3 (0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7), # cyc1 + cyc2 + cyc3 } self.check_cycle_algorithm(testG, expected) assert len(expected) == (2**3 - 1) - 1 # 1 disjoint comb: cyc1 + cyc2 # Basis size = 5 (2 loops overlapping gives 5 small loops # E # / \ Note: A-F = 10-15 # 1-2-3-4-5 # / | | \ cyc1=012DAB -- left # 0 D F 6 cyc2=234E -- top # \ | | / cyc3=45678F -- right # B-A-9-8-7 cyc4=89AC -- bottom # \ / cyc5=234F89AD -- middle # C # # combinations of 5 basis elements: 2^5 - 1 (one includes no cycles) # # disjoint combs: (11 total) not simple cycles # Any pair not including cyc5 => choose(4, 2) = 6 # Any triple not including cyc5 => choose(4, 3) = 4 # Any quad not including cyc5 => choose(4, 4) = 1 # # we expect 31 - 11 = 20 simple cycles # testG = nx.cycle_graph(12) testG.update(nx.cycle_graph([12, 10, 13, 2, 14, 4, 15, 8]).edges) expected = (2**5 - 1) - 11 # 11 disjoint combinations self.check_cycle_algorithm(testG, expected) def test_simple_cycles_bounded(self): # iteratively construct a cluster of nested cycles running in the same direction # there should be one cycle of every length d = nx.DiGraph() expected = [] for n in range(10): nx.add_cycle(d, range(n)) expected.append(n) for k, e in enumerate(expected): self.check_cycle_algorithm(d, e, length_bound=k) # iteratively construct a path of undirected cycles, connected at articulation # points. there should be one cycle of every length except 2: no digons g = nx.Graph() top = 0 expected = [] for n in range(10): expected.append(n if n < 2 else n - 1) if n == 2: # no digons in undirected graphs continue nx.add_cycle(g, range(top, top + n)) top += n for k, e in enumerate(expected): self.check_cycle_algorithm(g, e, length_bound=k) def test_simple_cycles_bound_corner_cases(self): G = nx.cycle_graph(4) DG = nx.cycle_graph(4, create_using=nx.DiGraph) assert list(nx.simple_cycles(G, length_bound=0)) == [] assert list(nx.simple_cycles(DG, length_bound=0)) == [] assert list(nx.chordless_cycles(G, length_bound=0)) == [] assert list(nx.chordless_cycles(DG, length_bound=0)) == [] def test_simple_cycles_bound_error(self): with pytest.raises(ValueError): G = nx.DiGraph() for c in nx.simple_cycles(G, -1): assert False with pytest.raises(ValueError): G = nx.Graph() for c in nx.simple_cycles(G, -1): assert False with pytest.raises(ValueError): G = nx.Graph() for c in nx.chordless_cycles(G, -1): assert False with pytest.raises(ValueError): G = nx.DiGraph() for c in nx.chordless_cycles(G, -1): assert False def test_chordless_cycles_clique(self): g_family = [self.K(n) for n in range(2, 15)] expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 286, 364] self.check_cycle_enumeration_integer_sequence( g_family, expected, chordless=True ) # directed cliques have as many digons as undirected graphs have edges expected = [(n * n - n) // 2 for n in range(15)] g_family = [self.D(n) for n in range(15)] self.check_cycle_enumeration_integer_sequence( g_family, expected, chordless=True ) # These tests might fail with hash randomization since they depend on # edge_dfs. For more information, see the comments in: # networkx/algorithms/traversal/tests/test_edgedfs.py class TestFindCycle: @classmethod def setup_class(cls): cls.nodes = [0, 1, 2, 3] cls.edges = [(-1, 0), (0, 1), (1, 0), (1, 0), (2, 1), (3, 1)] def test_graph_nocycle(self): G = nx.Graph(self.edges) pytest.raises(nx.exception.NetworkXNoCycle, find_cycle, G, self.nodes) def test_graph_cycle(self): G = nx.Graph(self.edges) G.add_edge(2, 0) x = list(find_cycle(G, self.nodes)) x_ = [(0, 1), (1, 2), (2, 0)] assert x == x_ def test_graph_orientation_none(self): G = nx.Graph(self.edges) G.add_edge(2, 0) x = list(find_cycle(G, self.nodes, orientation=None)) x_ = [(0, 1), (1, 2), (2, 0)] assert x == x_ def test_graph_orientation_original(self): G = nx.Graph(self.edges) G.add_edge(2, 0) x = list(find_cycle(G, self.nodes, orientation="original")) x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 0, FORWARD)] assert x == x_ def test_digraph(self): G = nx.DiGraph(self.edges) x = list(find_cycle(G, self.nodes)) x_ = [(0, 1), (1, 0)] assert x == x_ def test_digraph_orientation_none(self): G = nx.DiGraph(self.edges) x = list(find_cycle(G, self.nodes, orientation=None)) x_ = [(0, 1), (1, 0)] assert x == x_ def test_digraph_orientation_original(self): G = nx.DiGraph(self.edges) x = list(find_cycle(G, self.nodes, orientation="original")) x_ = [(0, 1, FORWARD), (1, 0, FORWARD)] assert x == x_ def test_multigraph(self): G = nx.MultiGraph(self.edges) x = list(find_cycle(G, self.nodes)) x_ = [(0, 1, 0), (1, 0, 1)] # or (1, 0, 2) # Hash randomization...could be any edge. assert x[0] == x_[0] assert x[1][:2] == x_[1][:2] def test_multidigraph(self): G = nx.MultiDiGraph(self.edges) x = list(find_cycle(G, self.nodes)) x_ = [(0, 1, 0), (1, 0, 0)] # (1, 0, 1) assert x[0] == x_[0] assert x[1][:2] == x_[1][:2] def test_digraph_ignore(self): G = nx.DiGraph(self.edges) x = list(find_cycle(G, self.nodes, orientation="ignore")) x_ = [(0, 1, FORWARD), (1, 0, FORWARD)] assert x == x_ def test_digraph_reverse(self): G = nx.DiGraph(self.edges) x = list(find_cycle(G, self.nodes, orientation="reverse")) x_ = [(1, 0, REVERSE), (0, 1, REVERSE)] assert x == x_ def test_multidigraph_ignore(self): G = nx.MultiDiGraph(self.edges) x = list(find_cycle(G, self.nodes, orientation="ignore")) x_ = [(0, 1, 0, FORWARD), (1, 0, 0, FORWARD)] # or (1, 0, 1, 1) assert x[0] == x_[0] assert x[1][:2] == x_[1][:2] assert x[1][3] == x_[1][3] def test_multidigraph_ignore2(self): # Loop traversed an edge while ignoring its orientation. G = nx.MultiDiGraph([(0, 1), (1, 2), (1, 2)]) x = list(find_cycle(G, [0, 1, 2], orientation="ignore")) x_ = [(1, 2, 0, FORWARD), (1, 2, 1, REVERSE)] assert x == x_ def test_multidigraph_original(self): # Node 2 doesn't need to be searched again from visited from 4. # The goal here is to cover the case when 2 to be researched from 4, # when 4 is visited from the first time (so we must make sure that 4 # is not visited from 2, and hence, we respect the edge orientation). G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 3), (4, 2)]) pytest.raises( nx.exception.NetworkXNoCycle, find_cycle, G, [0, 1, 2, 3, 4], orientation="original", ) def test_dag(self): G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) pytest.raises( nx.exception.NetworkXNoCycle, find_cycle, G, orientation="original" ) x = list(find_cycle(G, orientation="ignore")) assert x == [(0, 1, FORWARD), (1, 2, FORWARD), (0, 2, REVERSE)] def test_prev_explored(self): # https://github.com/networkx/networkx/issues/2323 G = nx.DiGraph() G.add_edges_from([(1, 0), (2, 0), (1, 2), (2, 1)]) pytest.raises(nx.NetworkXNoCycle, find_cycle, G, source=0) x = list(nx.find_cycle(G, 1)) x_ = [(1, 2), (2, 1)] assert x == x_ x = list(nx.find_cycle(G, 2)) x_ = [(2, 1), (1, 2)] assert x == x_ x = list(nx.find_cycle(G)) x_ = [(1, 2), (2, 1)] assert x == x_ def test_no_cycle(self): # https://github.com/networkx/networkx/issues/2439 G = nx.DiGraph() G.add_edges_from([(1, 2), (2, 0), (3, 1), (3, 2)]) pytest.raises(nx.NetworkXNoCycle, find_cycle, G, source=0) pytest.raises(nx.NetworkXNoCycle, find_cycle, G) def assert_basis_equal(a, b): assert sorted(a) == sorted(b) class TestMinimumCycles: @classmethod def setup_class(cls): T = nx.Graph() nx.add_cycle(T, [1, 2, 3, 4], weight=1) T.add_edge(2, 4, weight=5) cls.diamond_graph = T def test_unweighted_diamond(self): mcb = minimum_cycle_basis(self.diamond_graph) assert_basis_equal([sorted(c) for c in mcb], [[1, 2, 4], [2, 3, 4]]) def test_weighted_diamond(self): mcb = minimum_cycle_basis(self.diamond_graph, weight="weight") assert_basis_equal([sorted(c) for c in mcb], [[1, 2, 4], [1, 2, 3, 4]]) def test_dimensionality(self): # checks |MCB|=|E|-|V|+|NC| ntrial = 10 for _ in range(ntrial): rg = nx.erdos_renyi_graph(10, 0.3) nnodes = rg.number_of_nodes() nedges = rg.number_of_edges() ncomp = nx.number_connected_components(rg) dim_mcb = len(minimum_cycle_basis(rg)) assert dim_mcb == nedges - nnodes + ncomp def test_complete_graph(self): cg = nx.complete_graph(5) mcb = minimum_cycle_basis(cg) assert all(len(cycle) == 3 for cycle in mcb) def test_tree_graph(self): tg = nx.balanced_tree(3, 3) assert not minimum_cycle_basis(tg)