Skip to content

Commit 7a5ae57

Browse files
weilycoderMr-Python-in-China
authored andcommitted
Support for generating graphs randomly based on a degree sequence
1 parent ffb3739 commit 7a5ae57

File tree

1 file changed

+114
-26
lines changed

1 file changed

+114
-26
lines changed

cyaron/graph.py

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from .utils import *
22
from .vector import Vector
3+
import math
34
import random
45
import itertools
5-
from typing import Callable, Dict, Iterable, List, Sequence, Tuple, TypeVar, Union, cast
6+
from typing import * # type: ignore
67

78
__all__ = ["Edge", "Graph", "SwitchGraph"]
89

@@ -36,8 +37,8 @@ def unweighted_edge(edge):
3637

3738

3839
class SwitchGraph:
39-
"""A graph which can switch edges quickly
40-
"""
40+
"""A graph which can switch edges quickly"""
41+
4142
directed: bool
4243
__edges: Dict[Tuple[int, int], int]
4344

@@ -48,6 +49,13 @@ def get_edges(self):
4849
ret.extend(itertools.repeat(k, self.__edges[k]))
4950
return sorted(ret)
5051

52+
def edge_count(self):
53+
val = 0
54+
for k in self.__edges:
55+
if k[0] <= k[1]:
56+
val += self.__edges[k]
57+
return val
58+
5159
def __insert(self, u: int, v: int):
5260
if (u, v) not in self.__edges:
5361
self.__edges[(u, v)] = 0
@@ -59,22 +67,20 @@ def __remove(self, u: int, v: int):
5967
self.__edges.pop((u, v))
6068

6169
def insert(self, u: int, v: int):
62-
"""Add edge (u, v)
63-
"""
70+
"""Add edge (u, v)"""
6471
self.__insert(u, v)
6572
if not self.directed and u != v:
6673
self.__insert(v, u)
6774

6875
def remove(self, u: int, v: int):
69-
"""Remove edge (u, v)
70-
"""
76+
"""Remove edge (u, v)"""
7177
self.__remove(u, v)
7278
if not self.directed and u != v:
7379
self.__remove(v, u)
7480

75-
def __init__(self,
76-
E: Iterable[Union[Edge, Tuple[int, int]]],
77-
directed: bool = True):
81+
def __init__(
82+
self, E: Iterable[Union[Edge, Tuple[int, int]]], directed: bool = True
83+
):
7884
self.directed = directed
7985
self.__edges = {}
8086
for e in E:
@@ -92,9 +98,9 @@ def switch(self, *, self_loop: bool = False, repeated_edges: bool = False):
9298
Returns:
9399
If a switch was performed, then return True. If the switch was rejected, then return False.
94100
"""
95-
first, second = random.choices(list(self.__edges.keys()),
96-
list(self.__edges.values()),
97-
k=2)
101+
first, second = random.choices(
102+
list(self.__edges.keys()), list(self.__edges.values()), k=2
103+
)
98104
x1, y1 = first if self.directed else sorted(first)
99105
x2, y2 = second if self.directed else sorted(second)
100106

@@ -105,7 +111,7 @@ def switch(self, *, self_loop: bool = False, repeated_edges: bool = False):
105111
if {x1, y1} & {x2, y2} != set():
106112
return False
107113

108-
if repeated_edges:
114+
if not repeated_edges:
109115
if (x1, y2) in self.__edges or (x2, y1) in self.__edges:
110116
return False
111117

@@ -118,11 +124,13 @@ def switch(self, *, self_loop: bool = False, repeated_edges: bool = False):
118124

119125
@staticmethod
120126
def from_directed_degree_sequence(
121-
degree_sequence: Sequence[Tuple[int, int]],
122-
start_id: int = 1,
123-
*,
124-
self_loop: bool = False,
125-
repeated_edges: bool = False) -> "SwitchGraph":
127+
degree_sequence: Sequence[Tuple[int, int]],
128+
start_id: int = 1,
129+
*,
130+
self_loop: bool = False,
131+
repeated_edges: bool = False
132+
):
133+
"""Generate a directed graph greedily based on the degree sequence."""
126134
if any(x < 0 or y < 0 for (x, y) in degree_sequence):
127135
raise ValueError("Degree sequence is not graphical.")
128136

@@ -135,8 +143,9 @@ def from_directed_degree_sequence(
135143
if len(degree_sequence) == 0:
136144
return ret
137145

138-
degseq = [[sout, sin, vn]
139-
for vn, (sin, sout) in enumerate(degree_sequence, start_id)]
146+
degseq = [
147+
[sout, sin, vn] for vn, (sin, sout) in enumerate(degree_sequence, start_id)
148+
]
140149
degseq.sort(reverse=True)
141150

142151
try:
@@ -165,11 +174,13 @@ def from_directed_degree_sequence(
165174

166175
@staticmethod
167176
def from_undirected_degree_sequence(
168-
degree_sequence: Sequence[int],
169-
start_id: int = 1,
170-
*,
171-
self_loop: bool = False,
172-
repeated_edges: bool = False) -> "SwitchGraph":
177+
degree_sequence: Sequence[int],
178+
start_id: int = 1,
179+
*,
180+
self_loop: bool = False,
181+
repeated_edges: bool = False
182+
):
183+
"""Generate an undirected graph greedily based on the degree sequence."""
173184
if any(x < 0 for x in degree_sequence):
174185
raise ValueError("Degree sequence is not graphical.")
175186

@@ -504,6 +515,58 @@ def graph(point_count, edge_count, **kwargs):
504515
i += 1
505516
return graph
506517

518+
@staticmethod
519+
def from_degree_sequence(
520+
degree_sequence: Union[Sequence[Tuple[int, int]], Sequence[int]],
521+
n_iter: Optional[int] = None,
522+
*,
523+
self_loop: bool = False,
524+
repeated_edges: bool = False,
525+
weight_limit: Union[int, Tuple[int, int]] = (1, 1),
526+
weight_gen: Optional[Callable[[], int]] = None,
527+
iter_limit: int = int(1e6)
528+
):
529+
if len(degree_sequence) == 0:
530+
return Graph(0)
531+
if isinstance(weight_limit, int):
532+
weight_limit = (1, weight_limit)
533+
if weight_gen is None:
534+
weight_gen = lambda: random.randint(*weight_limit)
535+
if isinstance(degree_sequence[0], int):
536+
directed = False
537+
sg = SwitchGraph.from_undirected_degree_sequence(
538+
cast(Sequence[int], degree_sequence),
539+
self_loop=self_loop,
540+
repeated_edges=repeated_edges,
541+
)
542+
else:
543+
directed = True
544+
sg = SwitchGraph.from_directed_degree_sequence(
545+
cast(Sequence[Tuple[int, int]], degree_sequence),
546+
self_loop=self_loop,
547+
repeated_edges=repeated_edges,
548+
)
549+
point_cnt = len(degree_sequence)
550+
edge_cnt = sg.edge_count()
551+
if n_iter is None:
552+
n_iter = int(
553+
Graph._estimate_upperbound(
554+
point_cnt,
555+
edge_cnt,
556+
directed,
557+
self_loop,
558+
repeated_edges,
559+
)
560+
/ math.log(edge_cnt)
561+
)
562+
n_iter = min(n_iter, iter_limit)
563+
for _ in range(n_iter):
564+
sg.switch(self_loop=self_loop, repeated_edges=repeated_edges)
565+
g = Graph(len(degree_sequence), directed)
566+
for edge in sg.get_edges():
567+
g.add_edge(*edge, weight=weight_gen())
568+
return g
569+
507570
@staticmethod
508571
def DAG(point_count, edge_count, **kwargs):
509572
"""DAG(point_count, edge_count, **kwargs) -> Graph
@@ -713,6 +776,31 @@ def _calc_max_edge(point_count, directed, self_loop):
713776
max_edge += point_count
714777
return max_edge
715778

779+
@staticmethod
780+
def _estimate_comb(n: int, k: int):
781+
try:
782+
return float(sum(math.log(n - i) - math.log(i + 1) for i in range(k)))
783+
except ValueError:
784+
return 0.0
785+
786+
@staticmethod
787+
def _estimate_upperbound(
788+
point_count: int,
789+
edge_count: int,
790+
directed: bool,
791+
self_loop: bool,
792+
repeated_edges: bool,
793+
):
794+
tot_edge = point_count * (point_count - 1)
795+
if not directed:
796+
tot_edge //= 2
797+
if self_loop:
798+
tot_edge += point_count
799+
if repeated_edges:
800+
return Graph._estimate_comb(edge_count + tot_edge - 1, edge_count)
801+
else:
802+
return Graph._estimate_comb(tot_edge, edge_count)
803+
716804
@staticmethod
717805
def forest(point_count, tree_count, **kwargs):
718806
"""

0 commit comments

Comments
 (0)