From cb603fa51cbf242987566e081b5cb6c5331b15df Mon Sep 17 00:00:00 2001 From: weilycoder Date: Tue, 1 Oct 2024 10:41:29 +0800 Subject: [PATCH 1/6] Allow users to convert the graph to adjacency matrix. --- cyaron/graph.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cyaron/graph.py b/cyaron/graph.py index ee6b9ff..0a8ba82 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -40,6 +40,24 @@ def __init__(self, point_count, directed=False): self.directed = directed self.edges = [[] for i in range(point_count + 1)] + def to_matrix(self, **kwargs): + """to_matrix(self, **kwargs) -> list[list[Any]] + Convert the graph to adjacency matrix. + **kwargs(Keyword args): + int default = -1 -> the default value when the edge does not exist. + Any output(Edge) + = lambda edge: edge.weight + -> the mapping from edges to values in matrix. + Note that the index start from 0 and the values in the Column 0 or the Row 0 are always the default. + """ + default = kwargs.get("default", -1) + output = kwargs.get("output", lambda edge: edge.weight) + n = len(self.edges) + matrix = [[default for _ in range(n)] for _ in range(n)] + for edge in self.iterate_edges(): + matrix[edge.start][edge.end] = output(edge) + return matrix + def to_str(self, **kwargs): """to_str(self, **kwargs) -> str Convert the graph to string with format. Splits with "\n" From a1f1b5ce9d4fdaf7998ae7ddea7feeeab3b80ea4 Mon Sep 17 00:00:00 2001 From: weilycoder Date: Tue, 1 Oct 2024 14:34:38 +0800 Subject: [PATCH 2/6] allowing repeated edges --- cyaron/graph.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cyaron/graph.py b/cyaron/graph.py index 0a8ba82..49db3f2 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -45,17 +45,17 @@ def to_matrix(self, **kwargs): Convert the graph to adjacency matrix. **kwargs(Keyword args): int default = -1 -> the default value when the edge does not exist. - Any output(Edge) - = lambda edge: edge.weight - -> the mapping from edges to values in matrix. + Any merge(Any, Edge) + = lambda val, edge: edge.weight + -> the mapping from the old values in matrix and the edges to the new values in matrix. Note that the index start from 0 and the values in the Column 0 or the Row 0 are always the default. """ default = kwargs.get("default", -1) - output = kwargs.get("output", lambda edge: edge.weight) + merge = kwargs.get("merge", lambda val, edge: edge.weight) n = len(self.edges) matrix = [[default for _ in range(n)] for _ in range(n)] for edge in self.iterate_edges(): - matrix[edge.start][edge.end] = output(edge) + matrix[edge.start][edge.end] = merge(matrix[edge.start][edge.end], edge) return matrix def to_str(self, **kwargs): From c696bdc5d6300522e6f0e0a1e1854c82a05c47ca Mon Sep 17 00:00:00 2001 From: "Mr. Python" <2789762371@qq.com> Date: Thu, 3 Oct 2024 02:35:57 +0800 Subject: [PATCH 3/6] Add a matrix class to enable str() --- .gitignore | 5 +- cyaron/graph.py | 138 +++++++++++++++++++++++++++++++----------------- 2 files changed, 93 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 94e2e44..12e97f7 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,7 @@ docs/_build/ target/ # Pycharm -venv \ No newline at end of file +venv/ + +# VS Code +.vscode/ diff --git a/cyaron/graph.py b/cyaron/graph.py index 49db3f2..0b77d64 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -1,9 +1,11 @@ from .utils import * import random +from typing import TypeVar, Callable class Edge: """Class Edge: A class of the edge in the graph""" + def __init__(self, u, v, w): """__init__(self, u, v, w) -> None Initialize a edge. @@ -26,11 +28,13 @@ def unweighted_edge(edge): """unweighted_edge(edge) -> str Return a string to output the edge without weight. The string contains the start vertex, end vertex(u,v) and splits with space. """ - return '%d %d'%(edge.start,edge.end) + return '%d %d' % (edge.start, edge.end) + class Graph: """Class Graph: A class of the graph """ + def __init__(self, point_count, directed=False): """__init__(self, point_count) -> None Initialize a graph. @@ -50,13 +54,7 @@ def to_matrix(self, **kwargs): -> the mapping from the old values in matrix and the edges to the new values in matrix. Note that the index start from 0 and the values in the Column 0 or the Row 0 are always the default. """ - default = kwargs.get("default", -1) - merge = kwargs.get("merge", lambda val, edge: edge.weight) - n = len(self.edges) - matrix = [[default for _ in range(n)] for _ in range(n)] - for edge in self.iterate_edges(): - matrix[edge.start][edge.end] = merge(matrix[edge.start][edge.end], edge) - return matrix + return GraphMatrix(self) def to_str(self, **kwargs): """to_str(self, **kwargs) -> str @@ -75,7 +73,8 @@ def to_str(self, **kwargs): edge_buf = [] for edge in self.iterate_edges(): edge_buf.append( - Edge(new_node_id[edge.start], new_node_id[edge.end], edge.weight)) + Edge(new_node_id[edge.start], new_node_id[edge.end], + edge.weight)) random.shuffle(edge_buf) for edge in edge_buf: if not self.directed and random.randint(0, 1) == 0: @@ -173,9 +172,10 @@ def tree(point_count, chain=0, flower=0, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) - father_gen = kwargs.get("father_gen", lambda cur: random.randrange(1, cur)) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) + father_gen = kwargs.get("father_gen", + lambda cur: random.randrange(1, cur)) if not 0 <= chain <= 1 or not 0 <= flower <= 1: raise Exception("chain and flower must be between 0 and 1") @@ -222,33 +222,35 @@ def binary_tree(point_count, left=0, right=0, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) if not 0 <= left <= 1 or not 0 <= right <= 1: raise Exception("left and right must be between 0 and 1") if left + right > 1: raise Exception("left plus right must be smaller than 1") - - can_left=[1] - can_right=[1] + + can_left = [1] + can_right = [1] graph = Graph(point_count, directed) for i in range(2, point_count + 1): edge_pos = random.random() node = 0 # Left - if edge_pos < left or left + right < edge_pos <= (1.0 - left - right) / 2: - point_index = random.randint(0,len(can_left)-1) + if edge_pos < left or left + right < edge_pos <= (1.0 - left - + right) / 2: + point_index = random.randint(0, len(can_left) - 1) node = can_left[point_index] - del_last_node = can_left.pop() # Save a copy of the last element + del_last_node = can_left.pop( + ) # Save a copy of the last element if point_index < len(can_left): # If the chosen element isn't the last one, # Copy the last one to the position of the chosen one can_left[point_index] = del_last_node # Right else: - # elif left <= edge_pos <= left + right or (1.0 - left - right) / 2 < edge_pos < 1: - point_index = random.randint(0,len(can_right)-1) + # elif left <= edge_pos <= left + right or (1.0 - left - right) / 2 < edge_pos < 1: + point_index = random.randint(0, len(can_right) - 1) node = can_right[point_index] del_last_node = can_right.pop() if point_index < len(can_right): @@ -282,8 +284,8 @@ def graph(point_count, edge_count, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) graph = Graph(point_count, directed) used_edges = set() i = 0 @@ -291,7 +293,8 @@ def graph(point_count, edge_count, **kwargs): u = random.randint(1, point_count) v = random.randint(1, point_count) - if (not self_loop and u == v) or (not repeated_edges and (u, v) in used_edges): + if (not self_loop and u == v) or (not repeated_edges and + (u, v) in used_edges): # Then we generate a new pair of nodes continue @@ -322,30 +325,33 @@ def DAG(point_count, edge_count, **kwargs): -> the generator of the weights. It should return the weight. The default way is to use the random.randint() """ if edge_count < point_count - 1: - raise Exception("the number of edges of connected graph must more than the number of nodes - 1") + raise Exception( + "the number of edges of connected graph must more than the number of nodes - 1" + ) - self_loop = kwargs.get("self_loop", False) # DAG default has no loop + self_loop = kwargs.get("self_loop", False) # DAG default has no loop repeated_edges = kwargs.get("repeated_edges", True) loop = kwargs.get("loop", False) weight_limit = kwargs.get("weight_limit", (1, 1)) if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) - + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) + used_edges = set() - edge_buf = list(Graph.tree(point_count, weight_gen=weight_gen).iterate_edges()) + edge_buf = list( + Graph.tree(point_count, weight_gen=weight_gen).iterate_edges()) graph = Graph(point_count, directed=True) for edge in edge_buf: if loop and random.randint(1, 2) == 1: edge.start, edge.end = edge.end, edge.start graph.add_edge(edge.start, edge.end, weight=edge.weight) - + if not repeated_edges: used_edges.add((edge.start, edge.end)) - + i = point_count - 1 while i < edge_count: u = random.randint(1, point_count) @@ -354,7 +360,8 @@ def DAG(point_count, edge_count, **kwargs): if not loop and u > v: u, v = v, u - if (not self_loop and u == v) or (not repeated_edges and (u, v) in used_edges): + if (not self_loop and u == v) or (not repeated_edges and + (u, v) in used_edges): # Then we generate a new pair of nodes continue @@ -382,8 +389,10 @@ def UDAG(point_count, edge_count, **kwargs): = lambda: random.randint(weight_limit[0], weight_limit[1]) -> the generator of the weights. It should return the weight. The default way is to use the random.randint() """ - if edge_count < point_count - 1: - raise Exception("the number of edges of connected graph must more than the number of nodes - 1") + if edge_count < point_count - 1: + raise Exception( + "the number of edges of connected graph must more than the number of nodes - 1" + ) self_loop = kwargs.get("self_loop", True) repeated_edges = kwargs.get("repeated_edges", True) @@ -391,9 +400,9 @@ def UDAG(point_count, edge_count, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) - + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) + used_edges = set() graph = Graph.tree(point_count, weight_gen=weight_gen, directed=False) @@ -401,13 +410,14 @@ def UDAG(point_count, edge_count, **kwargs): if not repeated_edges: used_edges.add((edge.start, edge.end)) used_edges.add((edge.end, edge.start)) - + i = point_count - 1 while i < edge_count: u = random.randint(1, point_count) v = random.randint(1, point_count) - if (not self_loop and u == v) or (not repeated_edges and (u, v) in used_edges): + if (not self_loop and u == v) or (not repeated_edges and + (u, v) in used_edges): # Then we generate a new pair of nodes continue @@ -441,8 +451,8 @@ def hack_spfa(point_count, **kwargs): if not list_like(weight_limit): weight_limit = (1, weight_limit) weight_gen = kwargs.get( - "weight_gen", lambda: random.randint( - weight_limit[0], weight_limit[1])) + "weight_gen", + lambda: random.randint(weight_limit[0], weight_limit[1])) point_to_skip = point_count + 3 graph = Graph(point_count, directed) @@ -452,15 +462,18 @@ def hack_spfa(point_count, **kwargs): for i in range(1, half): (x, y) = (i, i + 1) - graph.add_edge(x + (x >= point_to_skip), y + - (y >= point_to_skip), weight=weight_gen()) + graph.add_edge(x + (x >= point_to_skip), + y + (y >= point_to_skip), + weight=weight_gen()) (x, y) = (i + half, i + half + 1) - graph.add_edge(x + (x >= point_to_skip), y + - (y >= point_to_skip), weight=weight_gen()) + graph.add_edge(x + (x >= point_to_skip), + y + (y >= point_to_skip), + weight=weight_gen()) for i in range(1, half + 1): (x, y) = (i, i + half) - graph.add_edge(x + (x >= point_to_skip), y + - (y >= point_to_skip), weight=weight_gen()) + graph.add_edge(x + (x >= point_to_skip), + y + (y >= point_to_skip), + weight=weight_gen()) for i in range(extraedg): u = random.randint(1, point_count) @@ -468,3 +481,30 @@ def hack_spfa(point_count, **kwargs): graph.add_edge(u, v, weight=weight_gen()) return graph + + +T = TypeVar('T') + + +class GraphMatrix: + """Class GraphMatrix: A class of the graph represented by adjacency matrix""" + + def __init__(self, + graph: Graph, + default: T = -1, + merge: Callable[[T, Edge], + T] = lambda val, edge: edge.weight): + """ + Args: + graph: the graph to convert, + default: the default value when the edge does not exist, + merge: the mapping from the old values in matrix and the edges to the new values in matrix. + """ + n = len(graph.edges) + self.matrix = [[default for _ in range(n)] for _ in range(n)] + for edge in graph.iterate_edges(): + self.matrix[edge.start][edge.end] = merge( + self.matrix[edge.start][edge.end], edge) + + def __str__(self): + return '\n'.join([' '.join(map(str, row)) for row in self.matrix]) From 313659903f2b8983f861d7bcbc538b2d3efe0622 Mon Sep 17 00:00:00 2001 From: weilycoder Date: Thu, 3 Oct 2024 18:02:48 +0800 Subject: [PATCH 4/6] move TypeVar T --- cyaron/graph.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cyaron/graph.py b/cyaron/graph.py index 01c5132..6c8dbfc 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -528,12 +528,11 @@ def _calc_max_edge(point_count, directed, self_loop): return max_edge -T = TypeVar('T') - - class GraphMatrix: """Class GraphMatrix: A class of the graph represented by adjacency matrix""" + T = TypeVar('T') + def __init__(self, graph: Graph, default: T = -1, From c35e622aef5c51ceff5f78c126c34e4ec39ec55f Mon Sep 17 00:00:00 2001 From: weilycoder Date: Thu, 3 Oct 2024 19:03:38 +0800 Subject: [PATCH 5/6] fix bugs --- cyaron/graph.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cyaron/graph.py b/cyaron/graph.py index 6c8dbfc..aa921d0 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -1,6 +1,6 @@ from .utils import * import random -from typing import TypeVar, Callable +from typing import TypeVar, Callable, List class Edge: @@ -54,16 +54,15 @@ def edge_count(self): return cnt def to_matrix(self, **kwargs): - """to_matrix(self, **kwargs) -> list[list[Any]] + """to_matrix(self, **kwargs) -> GraphMatrix Convert the graph to adjacency matrix. **kwargs(Keyword args): int default = -1 -> the default value when the edge does not exist. Any merge(Any, Edge) = lambda val, edge: edge.weight -> the mapping from the old values in matrix and the edges to the new values in matrix. - Note that the index start from 0 and the values in the Column 0 or the Row 0 are always the default. """ - return GraphMatrix(self) + return GraphMatrix(self, **kwargs) def to_str(self, **kwargs): """to_str(self, **kwargs) -> str @@ -551,4 +550,10 @@ def __init__(self, self.matrix[edge.start][edge.end], edge) def __str__(self): - return '\n'.join([' '.join(map(str, row)) for row in self.matrix]) + return '\n'.join([' '.join(map(str, row[1:])) for row in self.matrix[1:]]) + + def __call__(self, u: int, v: int): + return self.matrix[u][v] + + def __iter__(self): + return self.matrix.__iter__() From 011d6ccd8f7968ef405a6b576019c99c03eae693 Mon Sep 17 00:00:00 2001 From: weilycoder Date: Thu, 3 Oct 2024 21:53:18 +0800 Subject: [PATCH 6/6] add test_GraphMatrix --- cyaron/graph.py | 11 +++++++++-- cyaron/tests/graph_test.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/cyaron/graph.py b/cyaron/graph.py index aa921d0..c5965ed 100644 --- a/cyaron/graph.py +++ b/cyaron/graph.py @@ -1,6 +1,9 @@ from .utils import * import random -from typing import TypeVar, Callable, List +from typing import TypeVar, Callable + + +__all__ = ["Edge", "Graph"] class Edge: @@ -528,7 +531,11 @@ def _calc_max_edge(point_count, directed, self_loop): class GraphMatrix: - """Class GraphMatrix: A class of the graph represented by adjacency matrix""" + """ + Class GraphMatrix: A class of the graph represented by adjacency matrix. + + *Deprecation warning: This class may be removed after a generic matrix class is implemented in the project.* + """ T = TypeVar('T') diff --git a/cyaron/tests/graph_test.py b/cyaron/tests/graph_test.py index 976930a..1e3f1db 100644 --- a/cyaron/tests/graph_test.py +++ b/cyaron/tests/graph_test.py @@ -151,3 +151,24 @@ 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")