graph-matrix-analysis 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ from .core import InvalidMatrixError
2
+ from .core import Properties
3
+ from .core import Connectivity
4
+ from .core import WeightedGraph
5
+ from .core import Isomorphism
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,233 @@
1
+ import networkx as nx
2
+ import matplotlib as plt
3
+
4
+ class InvalidMatrixError(Exception):
5
+ pass
6
+
7
+ def check_matrix(A):
8
+ n = len(A)
9
+ for i in range(n):
10
+ if len(A[i]) != n:
11
+ raise InvalidMatrixError('The matrix is not square')
12
+
13
+ class Properties:
14
+ def __init__(self, A):
15
+ check_matrix(A)
16
+ self.A = A
17
+ self.n = len(A)
18
+
19
+ def count_vertex(self): # Number of vertices
20
+ return self.n
21
+
22
+ def count_edges(self): # Number of edges
23
+ edges = 0
24
+ for i in range(self.n):
25
+ for j in range(self.n):
26
+ edges += self.A[i][j]
27
+ return edges // 2
28
+
29
+ def simple(self): # Simple graph
30
+ for i in range(self.n):
31
+ if self.A[i][i] != 0:
32
+ return False
33
+
34
+ for j in range(self.n):
35
+ if self.A[i][j] > 1:
36
+ return False
37
+ return True
38
+
39
+ def complete(self): # Complete graph
40
+ for i in range(self.n):
41
+ for j in range(self.n):
42
+ if i != j and self.A[i][j] == 0:
43
+ return False
44
+ return True
45
+
46
+ def pseudograph(self): # Pseudograph
47
+ for i in range(self.n):
48
+ if self.A[i][i] > 0:
49
+ return True
50
+ return False
51
+
52
+ def multigraph(self): # Multigraph
53
+ for i in range(self.n):
54
+ for j in range(self.n):
55
+ if i != j and self.A[i][j] > 1:
56
+ return True
57
+ return False
58
+
59
+ def digraph(self): # Directed graph
60
+ for i in range(self.n):
61
+ for j in range(self.n):
62
+ if self.A[i][j] != self.A[j][i]:
63
+ return True
64
+ return False
65
+
66
+ def eulerian(self): # Eulerian graph
67
+ for i in range(self.n):
68
+ degree = sum(self.A[i])
69
+ if degree % 2 != 0:
70
+ return False
71
+ return True
72
+
73
+ def semieulerian(self): # Semi-Eulerian graph
74
+ odd = 0
75
+ for i in range(self.n):
76
+ degree = sum(self.A[i])
77
+ if degree % 2 != 0:
78
+ odd += 1
79
+ return odd == 2
80
+
81
+ class Connectivity:
82
+ def __init__(self, A):
83
+ check_matrix(A)
84
+ self.A = A
85
+ self.n = len(A)
86
+
87
+ def multiply(self, A, B):
88
+ C = []
89
+ for i in range(self.n):
90
+ row = []
91
+ for j in range(self.n):
92
+ row.append(0)
93
+ C.append(row)
94
+
95
+ for i in range(self.n):
96
+ for j in range(self.n):
97
+ for k in range(self.n):
98
+ C[i][j] += A[i][k] * B[k][j]
99
+ return C
100
+
101
+ def connectivity_matrix(self):
102
+ C = []
103
+ for i in range(self.n):
104
+ row = []
105
+ for j in range(self.n):
106
+ row.append(0)
107
+ C.append(row)
108
+
109
+ P = self.A
110
+ for k in range(1, self.n):
111
+ for i in range(self.n):
112
+ for j in range(self.n):
113
+ C[i][j] += P[i][j]
114
+ P = self.multiply(P, self.A)
115
+ return C
116
+
117
+ def connected(self):
118
+ C = self.connectivity_matrix()
119
+ for i in range(self.n):
120
+ for j in range(self.n):
121
+ if i != j and C[i][j] == 0:
122
+ return False
123
+ return True
124
+
125
+ class WeightedGraph:
126
+ def __init__(self, W):
127
+ check_matrix(W)
128
+ self.W = W
129
+ self.n = len(W)
130
+
131
+ def bellman_ford(self, start, end):
132
+ distance = []
133
+ for i in range(self.n): # Initialize distances
134
+ distance.append(float('inf'))
135
+ distance[start] = 0
136
+
137
+ predecessor = []
138
+ for i in range(self.n): # Initialize predecessors
139
+ predecessor.append(None)
140
+
141
+ # Bellman-Ford algorithm
142
+ for k in range(self.n - 1):
143
+ for i in range(self.n):
144
+ for j in range(self.n):
145
+ if self.W[i][j] != 0:
146
+ if distance[i] != float('inf'):
147
+ if distance[i] + self.W[i][j] < distance[j]:
148
+ distance[j] = distance[i] + self.W[i][j]
149
+ predecessor[j] = i
150
+
151
+ # Check negative cycles
152
+ for i in range(self.n):
153
+ for j in range(self.n):
154
+ if self.W[i][j] != 0:
155
+ if distance[i] != float('inf'):
156
+ if distance[i] + self.W[i][j] < distance[j]:
157
+ return "Negative cycle detected"
158
+
159
+ if distance[end] == float('inf'): # Check if destination is reachable
160
+ return "No path exists"
161
+
162
+ # Reconstruct shortest path
163
+ path = []
164
+ current = end
165
+
166
+ while current != None:
167
+ path.append(current)
168
+ current = predecessor[current]
169
+ path.reverse()
170
+
171
+ return path, distance[end]
172
+
173
+ def plot_graph(self):
174
+ G = nx.DiGraph()
175
+ for i in range(self.n): # Add vertices
176
+ G.add_node(i)
177
+
178
+ # Add edges and weights
179
+ for i in range(self.n):
180
+ for j in range(self.n):
181
+ if self.W[i][j] != 0:
182
+ G.add_edge(i, j, weight=self.W[i][j])
183
+
184
+ # Draw graph
185
+ pos = nx.spring_layout(G)
186
+ nx.draw(G, pos, with_labels = True, node_color = 'lightblue', node_size = 800, font_size = 12, arrows = True)
187
+
188
+ # Draw edge weights
189
+ labels = nx.get_edge_attributes(G, 'weight')
190
+
191
+ nx.draw_networkx_edge_labels(G, pos, edge_labels=labels)
192
+
193
+ plt.title("Weighted Graph")
194
+ plt.show()
195
+
196
+ class Isomorphism:
197
+ def __init__(self, A, B):
198
+ check_matrix(A)
199
+ check_matrix(B)
200
+ self.A = A
201
+ self.B = B
202
+ self.n = len(A)
203
+
204
+ def check(self, p):
205
+ for i in range(self.n):
206
+ for j in range(self.n):
207
+ if self.A[i][j] != self.B[p[i]][p[j]]:
208
+ return False
209
+ return True
210
+
211
+ def generate(self, p, used):
212
+ if len(p) == self.n:
213
+ return self.check(p)
214
+
215
+ for i in range(self.n):
216
+ if not used[i]:
217
+ p.append(i)
218
+ used[i] = True
219
+ if self.generate(p, used):
220
+ return True
221
+ p.pop()
222
+ used[i] = False
223
+ return False
224
+
225
+ def isomorphic(self):
226
+ if len(self.A) != len(self.B):
227
+ return False
228
+ p = []
229
+ used = []
230
+
231
+ for i in range(self.n):
232
+ used.append(False)
233
+ return self.generate(p, used)
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: graph_matrix_analysis
3
+ Version: 0.1.0
4
+ Summary: Library for analyzing graphs based on their adjacency matrix
5
+ Author-email: Aitor Merino <aitor.merino@alumni.mondragon.edu>, Xabier Labraga <xabier.labraga@alumni.mondragon.edu>
6
+ License-Expression: MIT
7
+ Keywords: graphs,adjacency matrix,euler,dijkstra,bellman-ford,isomorphism
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: networkx
14
+ Requires-Dist: matplotlib
15
+ Dynamic: license-file
16
+
17
+ # graph_matrix_analysis
18
+
19
+ Python library to analyze graphs from their **adjacency matrix**.
20
+
21
+ With a matrix you can:
22
+
23
+ - check whether the graph is **simple, complete, a multigraph, a pseudograph or directed**,
24
+ - check whether it is **Eulerian** or **semi-Eulerian**,
25
+ - check whether it is **connected** (using the connectivity matrix `C = A + A² + … + Aⁿ⁻¹`),
26
+ - compute the **shortest path** between two vertices with **Bellman-Ford** and draw the graph,
27
+ - check whether two matrices represent **isomorphic** graphs.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install graph_matrix_analysis
33
+ ```
34
+
35
+ It also installs `networkx` and `matplotlib`, which are used to draw the graph.
36
+
37
+ ## The matrix
38
+
39
+ The matrix is a square list of lists. `A[i][j]` is the number of edges from vertex `i` to vertex `j`, and vertices are numbered starting at 0. If the matrix is not square, `InvalidMatrixError` is raised.
40
+
41
+ ## Usage
42
+
43
+ ```python
44
+ from graph_matrix_analysis import Properties, Connectivity, WeightedGraph, Isomorphism
45
+
46
+ A = [
47
+ [0, 1, 1, 0],
48
+ [1, 0, 1, 1],
49
+ [1, 1, 0, 1],
50
+ [0, 1, 1, 0],
51
+ ]
52
+ ```
53
+
54
+ ### Properties (`Properties`)
55
+
56
+ ```python
57
+ p = Properties(A)
58
+
59
+ p.count_vertex() # 4
60
+ p.count_edges() # 5
61
+ p.simple() # True
62
+ p.complete() # False
63
+ p.pseudograph() # False
64
+ p.multigraph() # False
65
+ p.digraph() # False
66
+ p.eulerian() # False
67
+ p.semieulerian() # True
68
+ ```
69
+
70
+ | Method | Returns `True` if... |
71
+ |---|---|
72
+ | `simple()` | it has no loops or multiple edges |
73
+ | `complete()` | there is an edge between every pair of vertices |
74
+ | `pseudograph()` | it has at least one loop |
75
+ | `multigraph()` | it has multiple edges between distinct vertices |
76
+ | `digraph()` | the matrix is not symmetric (directed graph) |
77
+ | `eulerian()` | all vertices have even degree |
78
+ | `semieulerian()` | exactly two vertices have odd degree |
79
+
80
+ ### Connectivity (`Connectivity`)
81
+
82
+ ```python
83
+ c = Connectivity(A)
84
+
85
+ c.connected() # True
86
+ c.connectivity_matrix() # [[4, 7, 7, 4], [7, 7, 8, 7], [7, 8, 7, 7], [4, 7, 7, 4]]
87
+ ```
88
+
89
+ `connectivity_matrix()` computes `C = A + A² + … + Aⁿ⁻¹`. The graph is connected if all off-diagonal entries of `C` are non-zero.
90
+
91
+ ### Shortest path (`WeightedGraph`)
92
+
93
+ ```python
94
+ W = [
95
+ [0, 4, 5, 0],
96
+ [0, 0, -3, 6],
97
+ [0, 0, 0, 2],
98
+ [0, 0, 0, 0],
99
+ ]
100
+ g = WeightedGraph(W)
101
+
102
+ g.bellman_ford(0, 3) # ([0, 1, 2, 3], 3) -> (path, distance)
103
+ g.bellman_ford(3, 0) # 'No path exists'
104
+ g.plot_graph() # draws the graph with its weights
105
+ ```
106
+
107
+ Here the matrix is a **weight** matrix: `W[i][j]` is the weight of the edge from `i` to `j`, and 0 means there is no edge. Negative weights are allowed; if there is a negative cycle, it returns `'Negative cycle detected'`.
108
+
109
+ ### Isomorphism (`Isomorphism`)
110
+
111
+ ```python
112
+ B = [
113
+ [0, 1, 1, 1],
114
+ [1, 0, 1, 0],
115
+ [1, 1, 0, 1],
116
+ [1, 0, 1, 0],
117
+ ]
118
+
119
+ Isomorphism(A, B).isomorphic() # True
120
+ ```
121
+
122
+ It tries every permutation of the vertices, so it is only practical for small graphs.
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ git clone https://github.com/tuusuario/graph_matrix_analysis
128
+ cd grafomatriz
129
+ pip install -e .
130
+ pip install pytest
131
+ pytest
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,7 @@
1
+ graph_matrix_analysis/__init__.py,sha256=PNsS3KdBZhPEQ476pRCiglgStTh6v6dPwzMEW4fsKfg,189
2
+ graph_matrix_analysis/core.py,sha256=9VMRNv4VM9aHbYnltGb8l7kckifm5hiiyIPBxBqVaz8,6670
3
+ graph_matrix_analysis-0.1.0.dist-info/licenses/LICENSE,sha256=ria4zU4bfCH-lgPtsZmWSRj6edwMtosV_aM0Qx0rgHM,1085
4
+ graph_matrix_analysis-0.1.0.dist-info/METADATA,sha256=RbPUYxXLGqsg9z9ZljZwt7ytwTUsd71vRSgLvHFtxOM,3859
5
+ graph_matrix_analysis-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
6
+ graph_matrix_analysis-0.1.0.dist-info/top_level.txt,sha256=ylWefJRSHY7dS-K1oWwDU7TVZdWE8GJT1Kff9YthENk,22
7
+ graph_matrix_analysis-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aitor Merino, Xabier Labraga
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ graph_matrix_analysis