Skip to content

Commit

Permalink
feat: graphs, adjacency matrix
Browse files Browse the repository at this point in the history
Add base class Graph and AdjacencyMatrix implementation and tests
  • Loading branch information
rubenperezm committed Oct 6, 2024
1 parent b916ff4 commit 6ae739d
Show file tree
Hide file tree
Showing 5 changed files with 590 additions and 2 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ A Python library for data structures. This library provides more than 20 advance
- [ ] Skip List
- [x] Trie
- [x] Max Heap
- [ ] Graph (Adjacency Matrix)
- [ ] Graph (Edge List)
- [x] Graph (Adjacency Matrix)
- [ ] Graph (Adjacency List)
5 changes: 5 additions & 0 deletions pystrukts/graph/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# pylint: skip-file

from .graph import Graph
# from .adjacency_list import AdjacencyList
from .adjacency_matrix import AdjacencyMatrix
248 changes: 248 additions & 0 deletions pystrukts/graph/adjacency_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
'''
Adjacency matrix representation of a graph.
This module provides the AdjacencyMatrix class, which represents a graph using an adjacency matrix.
'''

from warnings import warn
from pystrukts.graph import Graph

class AdjacencyMatrix(Graph):
'''
Graph representation using adjacency matrix.
Attributes:
num_vertices (int): The number of vertices in the graph.
directed (bool): True if the graph is directed, False otherwise.
matrix (list): The adjacency matrix of the graph.
no_edge_value (float): The value used to represent the absence of an edge.
Methods:
copy() -> AdjacencyMatrix: Create a copy of the graph.
_copy_matrix(matrix: list, directed: bool) -> list: Copy the adjacency matrix.
_raise_error_if_invalid_vertex(x: int) -> None: Raise an error if the vertex does not exist.
has_edge(x: int, y: int) -> bool: Check if there is an edge between two vertices.
neighbors(x: int) -> list: Get the neighbors of a vertex.
add_edge(x: int, y: int, w: float = 1.0) -> None: Add an edge between two vertices.
If the edge already exists, the weight is updated.
remove_edge(x: int, y: int) -> None: Remove an edge between two vertices.
add_vertex() -> None: Add an isolated vertex to the graph.
remove_vertex(x: int) -> None: Remove a vertex from the graph.
get_edge_weight(x: int, y: int) -> float: Get the weight of an edge.
set_edge_weight(x: int, y: int, w: float) -> None: Set the weight of an edge.
'''

def __init__(self, num_vertices: int = None, directed: bool = False,
matrix: list = None, no_edge_value: float = float('inf')) -> None:
'''
Initialize the adjacency matrix.
Args:
num_vertices (int): The number of vertices in the graph.
directed (bool): True if the graph is directed, False otherwise.
matrix (list): The adjacency matrix of the graph.
no_edge_value (float): The value used to represent the absence of an edge.
Raises:
ValueError: No adjacency matrix or number of vertices provided.
'''

if not matrix and not num_vertices:
raise ValueError('You must specify either the number of vertices \
or the adjacency matrix.')

if matrix and num_vertices:
warn('The number of vertices will be ignored, as an adjacency matrix was provided.')

self.num_vertices = len(matrix) if matrix is not None else num_vertices
self.directed = directed
self.no_edge_value = no_edge_value

if matrix is not None:
for row in matrix:
if len(row) != len(matrix):
raise ValueError('The number of columns in the matrix must be \
equal to the number of vertices.')

self.matrix = self._copy_matrix(matrix, directed)
elif num_vertices < 0:
raise ValueError('The number of vertices must be a non-negative integer.')
else:
self.matrix = [[no_edge_value for _ in range(num_vertices)]
for _ in range(num_vertices)]

def copy(self) -> 'AdjacencyMatrix':
'''
Create a copy of the graph.
Returns:
out (AdjacencyMatrix): The copy of the graph.
'''

return AdjacencyMatrix(directed = self.directed, matrix = self.matrix)

def _copy_matrix(self, matrix: list, directed: bool) -> list:
'''
Copy the adjacency matrix with floating point values.
If the graph is undirected, the matrix is made symmetric by copying the upper triangle.
Args:
matrix (list): The matrix to be copied.
directed (bool): True if the graph is directed, False otherwise.
Returns:
out (list): The symmetric matrix.
'''

if directed:
return [[matrix[i][j] if i <= j else matrix[j][i]
for j in range(self.num_vertices)] for i in range(self.num_vertices)]
return [[float(matrix[i][j]) for j in range(self.num_vertices)]
for i in range(self.num_vertices)]

def _raise_error_if_invalid_vertex(self, x: int) -> None:
'''
Raise an error if the vertex does not exist.
Args:
x (int): The vertex.
Raises:
ValueError: If the vertex does not exist.
'''
if x >= self.num_vertices or x < 0:
raise ValueError(f'The vertex {x} does not exist.')

def has_edge(self, x: int, y: int) -> bool:
'''
Check if there is an edge between two vertices.
If the vertices are the same, the function checks if the vertex has a self-loop.
Args:
x (int): The source vertex.
y (int): The destination vertex.
Returns:
out (bool): True if there is an edge between the vertices, False otherwise.
Raises:
ValueError: If x or y are not valid vertices.
'''

self._raise_error_if_invalid_vertex(x)
self._raise_error_if_invalid_vertex(y)

return self.matrix[x][y] != self.no_edge_value

def neighbors(self, x: int) -> list:
'''
Get the neighbors of a vertex.
If x has a self-loop, the output contains the vertex itself.
Args:
x (int): The vertex.
Returns:
out (list): The list of neighbors of the vertex.
Raises:
ValueError: If x is not a valid vertex.
'''

self._raise_error_if_invalid_vertex(x)

return [i for i in range(self.num_vertices) if self.matrix[x][i] != self.no_edge_value]

def add_edge(self, x: int, y: int, w: float = 1.0) -> None:
'''
Add an edge between two vertices. If the edge already exists, the weight is updated.
Args:
x (int): The source vertex.
y (int): The destination vertex.
w (float): The weight of the edge.
Raises:
ValueError: If x or y are not valid vertices.
'''

self._raise_error_if_invalid_vertex(x)
self._raise_error_if_invalid_vertex(y)

self.matrix[x][y] = w

if self.directed:
self.matrix[y][x] = w

def remove_edge(self, x: int, y: int) -> None:
'''
Remove an edge between two vertices.
Args:
x (int): The source vertex.
y (int): The destination vertex.
Raises:
ValueError: If x or y are not valid vertices.
'''

self._raise_error_if_invalid_vertex(x)
self._raise_error_if_invalid_vertex(y)

self.matrix[x][y] = self.no_edge_value

if self.directed:
self.matrix[y][x] = self.no_edge_value

def add_vertex(self) -> None:
'''
Add an isolated vertex to the graph.
'''

self.num_vertices += 1

for row in self.matrix:
row.append(self.no_edge_value)

self.matrix.append([self.no_edge_value for _ in range(self.num_vertices)])

def remove_vertex(self, x: int) -> None:
'''
Remove a vertex from the graph.
Args:
x (int): The vertex to be removed.
Raises:
ValueError: If the vertex does not exist.
'''

self._raise_error_if_invalid_vertex(x)

self.num_vertices -= 1

for row in self.matrix:
del row[x]

del self.matrix[x]

def get_edge_weight(self, x: int, y: int) -> float:
'''
Get the weight of an edge.
Args:
x (int): The source vertex.
y (int): The destination vertex.
Returns:
out (float): The weight of the edge.
Raises:
ValueError: If x or y are not valid vertices.
'''

self._raise_error_if_invalid_vertex(x)
self._raise_error_if_invalid_vertex(y)

return self.matrix[x][y]
104 changes: 104 additions & 0 deletions pystrukts/graph/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'''
Module for the Graph abstract class.
'''

from abc import ABCMeta, abstractmethod

class Graph(metaclass=ABCMeta):
'''
Abstract class for graph representations.
Methods:
copy() -> Graph: Create a copy of the graph.
has_edge(x: int, y: int) -> bool: Check if there is an edge between two vertices.
neighbors(x: int) -> list: Get the neighbors of a vertex.
add_edge(x: int, y: int, w: float = 1.0) -> None: Add an edge between two vertices.
If the edge already exists, the weight is updated.
remove_edge(x: int, y: int) -> None: Remove an edge between two vertices.
add_vertex() -> None: Add an isolated vertex to the graph.
remove_vertex(x: int) -> None: Remove a vertex from the graph.
get_edge_weight(x: int, y: int) -> float: Get the weight of an edge.
'''

@abstractmethod
def copy(self):
'''
Create a copy of the graph.
Returns:
Graph: A copy of the graph.
'''

@abstractmethod
def has_edge(self, x: int, y: int) -> bool:
'''
Check if there is an edge between two vertices.
Args:
x (int): The source vertex.
y (int): The destination vertex.
Returns:
out (bool): True if there is an edge between the vertices, False otherwise.
'''

@abstractmethod
def neighbors(self, x: int) -> list:
'''
Get the neighbors of a vertex.
Args:
x (int): The vertex.
Returns:
out (list): The neighbors of the vertex.
'''

@abstractmethod
def add_edge(self, x: int, y: int, w: float = 1.0):
'''
Add an edge between two vertices.
Args:
x (int): The source vertex.
y (int): The destination vertex.
w (float): The weight of the edge.
'''

@abstractmethod
def remove_edge(self, x: int, y: int):
'''
Remove an edge between two vertices.
Args:
x (int): The source vertex.
y (int): The destination vertex.
'''

@abstractmethod
def add_vertex(self):
'''
Add an isolated vertex to the graph.
'''

@abstractmethod
def remove_vertex(self, x: int):
'''
Remove a vertex from the graph.
Args:
x (int): The vertex.
'''

@abstractmethod
def get_edge_weight(self, x: int, y: int) -> float:
'''
Get the weight of an edge.
Args:
x (int): The source vertex.
y (int): The destination vertex.
Returns:
out (float): The weight of the edge.
'''
Loading

0 comments on commit 6ae739d

Please sign in to comment.