Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to convert the graph to adjacency matrix. #134

Merged
merged 7 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ docs/_build/
target/

# Pycharm
venv
venv/

*.DS_Store

Expand Down
153 changes: 111 additions & 42 deletions cyaron/graph.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from .utils import *
import random
from typing import TypeVar, Callable


__all__ = ["Edge", "Graph"]


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.
Expand All @@ -26,11 +31,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.
Expand All @@ -49,6 +56,17 @@ def edge_count(self):
cnt //= 2
return cnt

def to_matrix(self, **kwargs):
"""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.
"""
return GraphMatrix(self, **kwargs)

def to_str(self, **kwargs):
"""to_str(self, **kwargs) -> str
Convert the graph to string with format. Splits with "\n"
Expand All @@ -66,7 +84,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:
Expand Down Expand Up @@ -164,9 +183,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")
Expand Down Expand Up @@ -213,33 +233,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):
Expand Down Expand Up @@ -278,16 +300,17 @@ 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
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

Expand Down Expand Up @@ -318,9 +341,11 @@ 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)
if not repeated_edges:
Expand All @@ -332,21 +357,22 @@ def DAG(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()
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)
Expand All @@ -355,7 +381,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

Expand Down Expand Up @@ -383,8 +410,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)
Expand All @@ -397,23 +426,24 @@ 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)

for edge in graph.iterate_edges():
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

Expand Down Expand Up @@ -459,8 +489,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)
Expand All @@ -470,15 +500,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)
Expand All @@ -495,3 +528,39 @@ def _calc_max_edge(point_count, directed, self_loop):
if self_loop:
max_edge += point_count
return max_edge


class GraphMatrix:
"""
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')

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[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__()
21 changes: 21 additions & 0 deletions cyaron/tests/graph_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")