diff --git a/cyaron/graph.py b/cyaron/graph.py index 3775b47..f804447 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -1,9 +1,12 @@ -from .utils import * -from .vector import Vector +import itertools +import math import random -from typing import TypeVar, Callable +from typing import (Callable, Counter, Iterable, List, Optional, Sequence, + Tuple, TypeVar, Union, cast) + +from .utils import * -__all__ = ["Edge", "Graph"] +__all__ = ["Edge", "Graph", "SwitchGraph"] class Edge: @@ -34,6 +37,212 @@ def unweighted_edge(edge): return '%d %d' % (edge.start, edge.end) +class SwitchGraph: + """A graph which can switch edges quickly""" + + directed: bool + __edges: Counter[Tuple[int, int]] + + def get_edges(self): + """ + Return a list of edges in the graph. + """ + ret: List[Tuple[int, int]] = [] + for k, c in self.__edges.items(): + if self.directed or k[0] <= k[1]: + ret.extend(itertools.repeat(k, c)) + return sorted(ret) + + def edge_count(self): + """ + Return the count of edges in the graph. + """ + val = 0 + for k, c in self.__edges.items(): + if k[0] <= k[1]: + val += c + return val + + def __insert(self, u: int, v: int): + self.__edges[(u, v)] += 1 + + def __remove(self, u: int, v: int): + self.__edges[(u, v)] -= 1 + if self.__edges[(u, v)] == 0: + self.__edges.pop((u, v)) + + def insert(self, u: int, v: int): + """ + Add edge (u, v) to the graph. + """ + self.__insert(u, v) + if not self.directed and u != v: + self.__insert(v, u) # pylint: disable=W1114 + + def remove(self, u: int, v: int): + """ + Remove edge (u, v) from the graph. + """ + self.__remove(u, v) + if not self.directed and u != v: + self.__remove(v, u) # pylint: disable=W1114 + + def __init__(self, + edges: Iterable[Union[Edge, Tuple[int, int]]], + directed: bool = True): + self.directed = directed + self.__edges = Counter() + for e in edges: + if isinstance(e, Edge): + self.insert(e.start, e.end) + else: + self.insert(e[0], e[1]) + + def switch(self, *, self_loop: bool = False, repeated_edges: bool = False): + """ + Mutates the current directed graph by swapping pairs of edges, + without impacting the degree sequence. + + A switch is a general term for a small change in the structure of a graph, + achieved by swapping small numbers of edges. + + Returns: + If a switch was performed, then return True. + If the switch was rejected, then return False. + """ + first, second = random.choices(list(self.__edges.keys()), + list(self.__edges.values()), + k=2) + x1, y1 = first if self.directed else sorted(first) + x2, y2 = second if self.directed else sorted(second) + + if self_loop: + if x1 == x2 or y1 == y2: + return False + else: + if {x1, y1} & {x2, y2} != set(): + return False + + if not repeated_edges: + if (x1, y2) in self.__edges or (x2, y1) in self.__edges: + return False + + self.remove(x1, y1) + self.insert(x1, y2) + self.remove(x2, y2) + self.insert(x2, y1) + + return True + + @staticmethod + def from_directed_degree_sequence(degree_sequence: Sequence[Tuple[int, + int]], + *, + self_loop: bool = False, + repeated_edges: bool = False): + """ + Generate a directed graph greedily based on the degree sequence. + + Args: + degree_sequence: The degree sequence of the graph. + self_loop: Whether to allow self loops or not. + repeated_edges: Whether to allow repeated edges or not. + """ + if any(x < 0 or y < 0 for (x, y) in degree_sequence): + raise ValueError("Degree sequence is not graphical.") + + x, y = zip(*degree_sequence) + if sum(x) != sum(y): + raise ValueError("Degree sequence is not graphical.") + + ret = SwitchGraph((), True) + + if len(degree_sequence) == 0: + return ret + + degseq = [[sout, sin, vn] + for vn, (sin, sout) in enumerate(degree_sequence, 1)] + degseq.sort(reverse=True) + + try: + while max(s[1] for s in degseq) > 0: + kk = [i for i in range(len(degseq)) if degseq[i][1] > 0] + _, in_deg, vto = degseq[kk[0]] + degseq[kk[0]][1] = 0 + j = 0 + while in_deg: + _, _, vfrom = degseq[j] + if vto == vfrom and not self_loop: + j += 1 + _, _, vfrom = degseq[j] + while in_deg and degseq[j][0]: + in_deg -= 1 + degseq[j][0] -= 1 + ret.insert(vfrom, vto) + if not repeated_edges: + break + j += 1 + degseq.sort(reverse=True) + except IndexError as e: + raise ValueError("Degree sequence is not graphical.") from e + + return ret + + @staticmethod + def from_undirected_degree_sequence(degree_sequence: Sequence[int], + *, + self_loop: bool = False, + repeated_edges: bool = False): + """ + Generate an undirected graph greedily based on the degree sequence. + + Args: + degree_sequence: The degree sequence of the graph. + self_loop: Whether to allow self loops or not. + repeated_edges: Whether to allow repeated edges or not. + """ + if any(x < 0 for x in degree_sequence): + raise ValueError("Degree sequence is not graphical.") + + if sum(degree_sequence) % 2 != 0: + raise ValueError("Degree sequence is not graphical.") + + if len(degree_sequence) == 0: + return SwitchGraph((), False) + + degseq = [[deg, i] for i, deg in enumerate(degree_sequence, 1)] + degseq.sort(reverse=True) + + edges: List[Tuple[int, int]] = [] + try: + while len(edges) * 2 < sum(degree_sequence): + deg, x = degseq[0] + degseq[0][0] = 0 + if self_loop: + while deg > 1: + deg -= 2 + edges.append((x, x)) + if not repeated_edges: + break + y = 1 + while deg: + while deg and degseq[y][0]: + deg -= 1 + degseq[y][0] -= 1 + edges.append((x, degseq[y][1])) + if not repeated_edges: + break + y += 1 + degseq.sort(reverse=True) + except IndexError as e: + raise ValueError("Degree sequence is not graphical.") from e + + return SwitchGraph(edges, False) + + def __iter__(self): + return self.__edges.elements() + + class Graph: """Class Graph: A class of the graph """ @@ -51,10 +260,7 @@ def edge_count(self): """edge_count(self) -> int Return the count of the edges in the graph. """ - cnt = sum(len(node) for node in self.edges) - if not self.directed: - cnt //= 2 - return cnt + return len(list(self.iterate_edges())) def to_matrix(self, **kwargs): """to_matrix(self, **kwargs) -> GraphMatrix @@ -326,6 +532,56 @@ def graph(point_count, edge_count, **kwargs): i += 1 return graph + @staticmethod + def from_degree_sequence(degree_sequence: Union[Sequence[Tuple[int, int]], + Sequence[int]], + n_iter: Optional[int] = None, + *, + self_loop: bool = False, + repeated_edges: bool = False, + weight_limit: Union[int, Tuple[int, + int]] = (1, 1), + weight_gen: Optional[Callable[[], int]] = None, + iter_limit: int = int(1e6)): + if len(degree_sequence) == 0: + return Graph(0) + if isinstance(weight_limit, int): + weight_limit = (1, weight_limit) + if weight_gen is None: + weight_gen = lambda: random.randint(*weight_limit) + if isinstance(degree_sequence[0], int): + directed = False + sg = SwitchGraph.from_undirected_degree_sequence( + cast(Sequence[int], degree_sequence), + self_loop=self_loop, + repeated_edges=repeated_edges, + ) + else: + directed = True + sg = SwitchGraph.from_directed_degree_sequence( + cast(Sequence[Tuple[int, int]], degree_sequence), + self_loop=self_loop, + repeated_edges=repeated_edges, + ) + point_cnt = len(degree_sequence) + edge_cnt = sg.edge_count() + if n_iter is None: + n_iter = int( + Graph._estimate_upperbound( + point_cnt, + edge_cnt, + directed, + self_loop, + repeated_edges, + ) / math.log(edge_cnt)) + n_iter = min(n_iter, iter_limit) + for _ in range(n_iter): + sg.switch(self_loop=self_loop, repeated_edges=repeated_edges) + g = Graph(len(degree_sequence), directed) + for edge in sg.get_edges(): + g.add_edge(*edge, weight=weight_gen()) + return g + @staticmethod def DAG(point_count, edge_count, **kwargs): """DAG(point_count, edge_count, **kwargs) -> Graph @@ -535,6 +791,32 @@ def _calc_max_edge(point_count, directed, self_loop): max_edge += point_count return max_edge + @staticmethod + def _estimate_comb(n: int, k: int): + try: + return float( + sum(math.log(n - i) - math.log(i + 1) for i in range(k))) + except ValueError: + return 0.0 + + @staticmethod + def _estimate_upperbound( + point_count: int, + edge_count: int, + directed: bool, + self_loop: bool, + repeated_edges: bool, + ): + tot_edge = point_count * (point_count - 1) + if not directed: + tot_edge //= 2 + if self_loop: + tot_edge += point_count + if repeated_edges: + return Graph._estimate_comb(edge_count + tot_edge - 1, edge_count) + else: + return Graph._estimate_comb(tot_edge, edge_count) + @staticmethod def forest(point_count, tree_count, **kwargs): """ diff --git a/cyaron/tests/graph_test.py b/cyaron/tests/graph_test.py index 55a63a5..61ef470 100644 --- a/cyaron/tests/graph_test.py +++ b/cyaron/tests/graph_test.py @@ -1,227 +1,266 @@ -import unittest -from cyaron import Graph -from random import randint - - -class UnionFindSet: - - def __init__(self, size): - self.father = [0] + [i + 1 for i in range(size)] - - def get_father(self, node): - if self.father[node] == node: - return node - else: - self.father[node] = self.get_father(self.father[node]) - return self.father[node] - - def merge(self, l, r): - l = self.get_father(l) - r = self.get_father(r) - self.father[l] = r - - def test_same(self, l, r): - return self.get_father(l) == self.get_father(r) - - -def tarjan(graph, n): - - def new_array(len, val=0): - return [val for _ in range(len + 1)] - - instack = new_array(n, False) - low = new_array(n) - dfn = new_array(n, 0) - stap = new_array(n) - belong = new_array(n) - var = [0, 0, 0] # cnt, bc, stop - - # cnt = bc = stop = 0 - - def dfs(cur): - var[0] += 1 - dfn[cur] = low[cur] = var[0] - instack[cur] = True - stap[var[2]] = cur - var[2] += 1 - - for v in graph.edges[cur]: - if dfn[v.end] == 0: - dfs(v.end) - low[cur] = min(low[cur], low[v.end]) - elif instack[v.end]: - low[cur] = min(low[cur], dfn[v.end]) - - if dfn[cur] == low[cur]: - v = cur + 1 # set v != cur - var[1] += 1 - while v != cur: - var[2] -= 1 - v = stap[var[2]] - instack[v] = False - belong[v] = var[1] - - for i in range(n): - if dfn[i + 1] == 0: - dfs(i + 1) - - return belong - - -class TestGraph(unittest.TestCase): - - def test_self_loop(self): - graph_size = 20 - for _ in range(20): - graph = Graph.graph(graph_size, - int(graph_size * 2), - self_loop=True) - has_self_loop = max( - [e.start == e.end for e in graph.iterate_edges()]) - if has_self_loop: - break - self.assertTrue(has_self_loop) - - for _ in range(10): - graph = Graph.graph(graph_size, - int(graph_size * 2), - self_loop=False) - self.assertFalse( - max([e.start == e.end for e in graph.iterate_edges()])) - - def test_repeated_edges(self): - graph_size = 20 - for _ in range(20): - graph = Graph.graph(graph_size, - int(graph_size * 2), - repeated_edges=True) - edges = [(e.start, e.end) for e in graph.iterate_edges()] - has_repeated_edges = len(edges) > len(set(edges)) - if has_repeated_edges: - break - self.assertTrue(has_repeated_edges) - - for _ in range(10): - graph = Graph.graph(graph_size, - int(graph_size * 2), - repeated_edges=False) - edges = [(e.start, e.end) for e in graph.iterate_edges()] - self.assertEqual(len(edges), len(set(edges))) - - def test_tree_connected(self): - graph_size = 20 - for _ in range(20): - ufs = UnionFindSet(graph_size) - tree = Graph.tree(graph_size) - for edge in tree.iterate_edges(): - ufs.merge(edge.start, edge.end) - for i in range(graph_size - 1): - self.assertTrue(ufs.test_same(i + 1, i + 2)) - - def test_DAG(self): - graph_size = 20 - for _ in range(10): # test 10 times - ufs = UnionFindSet(graph_size) - graph = Graph.DAG(graph_size, - int(graph_size * 1.6), - repeated_edges=False, - self_loop=False, - loop=True) - - self.assertEqual(len(list(graph.iterate_edges())), - int(graph_size * 1.6)) - - for edge in graph.iterate_edges(): - ufs.merge(edge.start, edge.end) - for i in range(graph_size - 1): - self.assertTrue(ufs.test_same(i + 1, i + 2)) - - def test_DAG_without_loop(self): - graph_size = 20 - for _ in range(10): # test 10 times - ufs = UnionFindSet(graph_size) - graph = Graph.DAG(graph_size, - int(graph_size * 1.6), - repeated_edges=False, - self_loop=False, - loop=False) - - self.assertEqual(len(list(graph.iterate_edges())), - int(graph_size * 1.6)) - - for edge in graph.iterate_edges(): - ufs.merge(edge.start, edge.end) - for i in range(graph_size - 1): - self.assertTrue(ufs.test_same(i + 1, i + 2)) - - belong = tarjan(graph, graph_size) - self.assertEqual(max(belong), graph_size) - - def test_undirected_graph(self): - graph_size = 20 - for _ in range(10): # test 10 times - ufs = UnionFindSet(graph_size) - graph = Graph.UDAG(graph_size, - int(graph_size * 1.6), - repeated_edges=False, - self_loop=False) - - self.assertEqual(len(list(graph.iterate_edges())), - int(graph_size * 1.6)) - - for edge in graph.iterate_edges(): - ufs.merge(edge.start, edge.end) - for i in range(graph_size - 1): - self.assertTrue(ufs.test_same(i + 1, i + 2)) - - def test_DAG_boundary(self): - with self.assertRaises( - Exception, - msg= - "the number of edges of connected graph must more than the number of nodes - 1" - ): - Graph.DAG(8, 6) - Graph.DAG(8, 7) - - def test_GraphMatrix(self): - g = Graph(3, True) - edge_set = [(2, 3, 3), (3, 3, 1), (2, 3, 7), (2, 3, 4), (3, 2, 1), - (1, 3, 3)] - for u, v, w in edge_set: - g.add_edge(u, v, weight=w) - self.assertEqual(str(g.to_matrix()), "-1 -1 3\n-1 -1 4\n-1 1 1") - self.assertEqual(str(g.to_matrix(default=0)), "0 0 3\n0 0 4\n0 1 1") - # lambda val, edge: edge.weight - gcd = lambda a, b: (gcd(b, a % b) if b else a) - lcm = lambda a, b: a * b // gcd(a, b) - merge1 = lambda v, e: v if v != -1 else e.weight - merge2 = lambda val, edge: max(edge.weight, val) - merge3 = lambda val, edge: min(edge.weight, val) - merge4 = lambda val, edge: gcd(val, edge.weight) - merge5 = lambda val, edge: lcm(val, edge.weight - ) if val else edge.weight - self.assertEqual(str(g.to_matrix(merge=merge1)), - "-1 -1 3\n-1 -1 3\n-1 1 1") - self.assertEqual(str(g.to_matrix(merge=merge2)), - "-1 -1 3\n-1 -1 7\n-1 1 1") - self.assertEqual(str(g.to_matrix(default=9, merge=merge3)), - "9 9 3\n9 9 3\n9 1 1") - self.assertEqual(str(g.to_matrix(default=0, merge=merge4)), - "0 0 3\n0 0 1\n0 1 1") - self.assertEqual(str(g.to_matrix(default=0, merge=merge5)), - "0 0 3\n0 0 84\n0 1 1") - - def test_forest(self): - for i in range(10): - size = randint(1, 100) - part_count = randint(1, size) - forest = Graph.forest(size, part_count) - dsu = UnionFindSet(size) - for edge in forest.iterate_edges(): - self.assertFalse(dsu.test_same(edge.start, edge.end)) - dsu.merge(edge.start, edge.end) - count = 0 - for i in range(1, size + 1): - if dsu.get_father(i) == i: - count += 1 - self.assertEqual(count, part_count) +import unittest +from cyaron import Graph +from random import randint + + +class UnionFindSet: + + def __init__(self, size): + self.father = [0] + [i + 1 for i in range(size)] + + def get_father(self, node): + if self.father[node] == node: + return node + else: + self.father[node] = self.get_father(self.father[node]) + return self.father[node] + + def merge(self, l, r): + l = self.get_father(l) + r = self.get_father(r) + self.father[l] = r + + def test_same(self, l, r): + return self.get_father(l) == self.get_father(r) + + +def tarjan(graph, n): + + def new_array(len, val=0): + return [val for _ in range(len + 1)] + + instack = new_array(n, False) + low = new_array(n) + dfn = new_array(n, 0) + stap = new_array(n) + belong = new_array(n) + var = [0, 0, 0] # cnt, bc, stop + + # cnt = bc = stop = 0 + + def dfs(cur): + var[0] += 1 + dfn[cur] = low[cur] = var[0] + instack[cur] = True + stap[var[2]] = cur + var[2] += 1 + + for v in graph.edges[cur]: + if dfn[v.end] == 0: + dfs(v.end) + low[cur] = min(low[cur], low[v.end]) + elif instack[v.end]: + low[cur] = min(low[cur], dfn[v.end]) + + if dfn[cur] == low[cur]: + v = cur + 1 # set v != cur + var[1] += 1 + while v != cur: + var[2] -= 1 + v = stap[var[2]] + instack[v] = False + belong[v] = var[1] + + for i in range(n): + if dfn[i + 1] == 0: + dfs(i + 1) + + return belong + + +class TestGraph(unittest.TestCase): + + def test_self_loop(self): + graph_size = 20 + for _ in range(20): + graph = Graph.graph(graph_size, + int(graph_size * 2), + self_loop=True) + has_self_loop = max( + [e.start == e.end for e in graph.iterate_edges()]) + if has_self_loop: + break + self.assertTrue(has_self_loop) + + for _ in range(10): + graph = Graph.graph(graph_size, + int(graph_size * 2), + self_loop=False) + self.assertFalse( + max([e.start == e.end for e in graph.iterate_edges()])) + + def test_repeated_edges(self): + graph_size = 20 + for _ in range(20): + graph = Graph.graph(graph_size, + int(graph_size * 2), + repeated_edges=True) + edges = [(e.start, e.end) for e in graph.iterate_edges()] + has_repeated_edges = len(edges) > len(set(edges)) + if has_repeated_edges: + break + self.assertTrue(has_repeated_edges) + + for _ in range(10): + graph = Graph.graph(graph_size, + int(graph_size * 2), + repeated_edges=False) + edges = [(e.start, e.end) for e in graph.iterate_edges()] + self.assertEqual(len(edges), len(set(edges))) + + def test_tree_connected(self): + graph_size = 20 + for _ in range(20): + ufs = UnionFindSet(graph_size) + tree = Graph.tree(graph_size) + for edge in tree.iterate_edges(): + ufs.merge(edge.start, edge.end) + for i in range(graph_size - 1): + self.assertTrue(ufs.test_same(i + 1, i + 2)) + + def test_DAG(self): + graph_size = 20 + for _ in range(10): # test 10 times + ufs = UnionFindSet(graph_size) + graph = Graph.DAG(graph_size, + int(graph_size * 1.6), + repeated_edges=False, + self_loop=False, + loop=True) + + self.assertEqual(len(list(graph.iterate_edges())), + int(graph_size * 1.6)) + + for edge in graph.iterate_edges(): + ufs.merge(edge.start, edge.end) + for i in range(graph_size - 1): + self.assertTrue(ufs.test_same(i + 1, i + 2)) + + def test_DAG_without_loop(self): + graph_size = 20 + for _ in range(10): # test 10 times + ufs = UnionFindSet(graph_size) + graph = Graph.DAG(graph_size, + int(graph_size * 1.6), + repeated_edges=False, + self_loop=False, + loop=False) + + self.assertEqual(len(list(graph.iterate_edges())), + int(graph_size * 1.6)) + + for edge in graph.iterate_edges(): + ufs.merge(edge.start, edge.end) + for i in range(graph_size - 1): + self.assertTrue(ufs.test_same(i + 1, i + 2)) + + belong = tarjan(graph, graph_size) + self.assertEqual(max(belong), graph_size) + + def test_undirected_graph(self): + graph_size = 20 + for _ in range(10): # test 10 times + ufs = UnionFindSet(graph_size) + graph = Graph.UDAG(graph_size, + int(graph_size * 1.6), + repeated_edges=False, + self_loop=False) + + self.assertEqual(len(list(graph.iterate_edges())), + int(graph_size * 1.6)) + + for edge in graph.iterate_edges(): + ufs.merge(edge.start, edge.end) + for i in range(graph_size - 1): + self.assertTrue(ufs.test_same(i + 1, i + 2)) + + def test_DAG_boundary(self): + with self.assertRaises( + Exception, + msg= + "the number of edges of connected graph must more than the number of nodes - 1" + ): + Graph.DAG(8, 6) + Graph.DAG(8, 7) + + def test_GraphMatrix(self): + g = Graph(3, True) + edge_set = [(2, 3, 3), (3, 3, 1), (2, 3, 7), (2, 3, 4), (3, 2, 1), + (1, 3, 3)] + for u, v, w in edge_set: + g.add_edge(u, v, weight=w) + self.assertEqual(str(g.to_matrix()), "-1 -1 3\n-1 -1 4\n-1 1 1") + self.assertEqual(str(g.to_matrix(default=0)), "0 0 3\n0 0 4\n0 1 1") + # lambda val, edge: edge.weight + gcd = lambda a, b: (gcd(b, a % b) if b else a) + lcm = lambda a, b: a * b // gcd(a, b) + merge1 = lambda v, e: v if v != -1 else e.weight + merge2 = lambda val, edge: max(edge.weight, val) + merge3 = lambda val, edge: min(edge.weight, val) + merge4 = lambda val, edge: gcd(val, edge.weight) + merge5 = lambda val, edge: lcm(val, edge.weight + ) if val else edge.weight + self.assertEqual(str(g.to_matrix(merge=merge1)), + "-1 -1 3\n-1 -1 3\n-1 1 1") + self.assertEqual(str(g.to_matrix(merge=merge2)), + "-1 -1 3\n-1 -1 7\n-1 1 1") + self.assertEqual(str(g.to_matrix(default=9, merge=merge3)), + "9 9 3\n9 9 3\n9 1 1") + self.assertEqual(str(g.to_matrix(default=0, merge=merge4)), + "0 0 3\n0 0 1\n0 1 1") + self.assertEqual(str(g.to_matrix(default=0, merge=merge5)), + "0 0 3\n0 0 84\n0 1 1") + + def test_forest(self): + for i in range(10): + size = randint(1, 100) + part_count = randint(1, size) + forest = Graph.forest(size, part_count) + dsu = UnionFindSet(size) + for edge in forest.iterate_edges(): + self.assertFalse(dsu.test_same(edge.start, edge.end)) + dsu.merge(edge.start, edge.end) + count = 0 + for i in range(1, size + 1): + if dsu.get_father(i) == i: + count += 1 + self.assertEqual(count, part_count) + + def test_from_undirected_degree_sequence(self): + get_deg_seq = lambda g: tuple(map(len, g.edges[1:])) + g1 = Graph.from_degree_sequence((2, 2, 1, 1, 1, 1)) + self.assertEqual(get_deg_seq(g1), (2, 2, 1, 1, 1, 1)) + for _ in range(8): + g0 = Graph.graph( + 100, 400, directed=False, self_loop=False, repeated_edges=False + ) + dsq = get_deg_seq(g0) + g1 = Graph.from_degree_sequence(dsq) + self.assertEqual(get_deg_seq(g1), dsq) + with self.assertRaises(ValueError): + Graph.from_degree_sequence((6, 6, 6)) + with self.assertRaises(ValueError): + Graph.from_degree_sequence((1, 1, 1)) + + def test_from_directed_degree_sequence(self): + def get_deg_seq(g: Graph): + cnt = len(g.edges) - 1 + indeg, outdeg = [0] * cnt, [0] * cnt + for edge in g.iterate_edges(): + indeg[edge.end - 1] += 1 + outdeg[edge.start - 1] += 1 + return tuple(zip(indeg, outdeg)) + + g1 = Graph.from_degree_sequence(((1, 2), (1, 1), (1, 0))) + self.assertEqual(get_deg_seq(g1), ((1, 2), (1, 1), (1, 0))) + + for _ in range(8): + g0 = Graph.graph( + 100, 400, directed=True, self_loop=False, repeated_edges=False + ) + dsq = get_deg_seq(g0) + g1 = Graph.from_degree_sequence(dsq) + self.assertEqual(get_deg_seq(g1), dsq) + + with self.assertRaises(ValueError): + Graph.from_degree_sequence(((2, 1), (0, 1)))