clabtoolkit 0.3.3__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,351 @@
1
+ import numpy as np
2
+ from scipy.sparse import csr_matrix
3
+ from typing import Tuple, Union, Optional
4
+ import warnings
5
+
6
+
7
+ ####################################################################################################
8
+ ####################################################################################################
9
+ ############ ############
10
+ ############ ############
11
+ ############ Section 1: Methods dedicated create CSR graphs from different sources ############
12
+ ############ CSR (Compressed Sparse Row) format is efficient for graph representation ############
13
+ ############ and is widely used in scientific computing and machine learning. ############
14
+ ############ ############
15
+ ############ ############
16
+ ####################################################################################################
17
+ ####################################################################################################
18
+ def adjacency_matrix_to_csr(adj_matrix: np.ndarray) -> csr_matrix:
19
+ """
20
+ Convert a dense adjacency matrix to CSR (Compressed Sparse Row) format.
21
+
22
+ This method takes a square adjacency matrix where non-zero entries represent
23
+ connections between vertices and converts it to an efficient sparse representation.
24
+
25
+ Parameters
26
+ ----------
27
+ adj_matrix : np.ndarray
28
+ A square 2D numpy array representing the adjacency matrix.
29
+ Shape should be (n_vertices, n_vertices) where n_vertices is the number
30
+ of vertices in the graph. Non-zero values represent edge weights.
31
+
32
+ Returns
33
+ -------
34
+ csr_matrix
35
+ A scipy sparse CSR matrix representing the same graph connectivity.
36
+
37
+ Raises
38
+ ------
39
+ ValueError
40
+ If the input matrix is not 2D or not square.
41
+ TypeError
42
+ If the input is not a numpy array.
43
+
44
+ Examples
45
+ --------
46
+ >>> import numpy as np
47
+ >>> # Create a simple 4-vertex graph
48
+ >>> adj = np.array([[0, 1, 1, 0],
49
+ ... [1, 0, 1, 1],
50
+ ... [1, 1, 0, 1],
51
+ ... [0, 1, 1, 0]])
52
+ >>> csr_graph = adjacency_matrix_to_csr(adj)
53
+ >>> print(csr_graph.toarray())
54
+ [[0 1 1 0]
55
+ [1 0 1 1]
56
+ [1 1 0 1]
57
+ [0 1 1 0]]
58
+
59
+ >>> # With weighted edges
60
+ >>> adj_weighted = np.array([[0, 2.5, 1.0, 0],
61
+ ... [2.5, 0, 0, 3.2],
62
+ ... [1.0, 0, 0, 1.8],
63
+ ... [0, 3.2, 1.8, 0]])
64
+ >>> csr_weighted = adjacency_matrix_to_csr(adj_weighted)
65
+ >>> print(f"Non-zero values: {csr_weighted.data}")
66
+ Non-zero values: [2.5 1. 2.5 3.2 1. 1.8 3.2 1.8]
67
+ """
68
+ if not isinstance(adj_matrix, np.ndarray):
69
+ raise TypeError("Input must be a numpy array")
70
+
71
+ if adj_matrix.ndim != 2:
72
+ raise ValueError("Input must be a 2D array")
73
+
74
+ if adj_matrix.shape[0] != adj_matrix.shape[1]:
75
+ raise ValueError("Adjacency matrix must be square")
76
+
77
+ return csr_matrix(adj_matrix)
78
+
79
+
80
+ ####################################################################################################
81
+ def triangulated_mesh_to_csr(
82
+ faces: np.ndarray, n_vertices: Optional[int] = None
83
+ ) -> csr_matrix:
84
+ """
85
+ Convert triangulated mesh faces to a CSR graph representation.
86
+
87
+ This method constructs a graph where vertices are connected if they share
88
+ an edge in any triangle face. All edge weights are set to 1. The resulting
89
+ graph represents the 1-ring neighborhood connectivity of the mesh.
90
+
91
+ Parameters
92
+ ----------
93
+ faces : np.ndarray
94
+ A 2D numpy array of shape (n_faces, 3) where each row contains the
95
+ indices of three vertices forming a triangle. Vertex indices should
96
+ be non-negative integers.
97
+
98
+ n_vertices : int, optional
99
+ Total number of vertices in the mesh. If None, it will be inferred
100
+ as the maximum vertex index + 1. Providing this parameter is recommended
101
+ for meshes with isolated vertices.
102
+
103
+ Returns
104
+ -------
105
+ csr_matrix
106
+ A scipy sparse CSR matrix of shape (n_vertices, n_vertices) where
107
+ entry (i,j) is 1 if vertices i and j are connected by an edge in the mesh,
108
+ and 0 otherwise. The matrix is symmetric for undirected graphs.
109
+
110
+ Raises
111
+ ------
112
+ ValueError
113
+ If faces array is not 2D, doesn't have 3 columns, contains negative
114
+ indices, or if n_vertices is less than the maximum vertex index.
115
+
116
+ TypeError
117
+ If faces is not a numpy array or contains non-integer values.
118
+
119
+ Examples
120
+ --------
121
+ >>> import numpy as np
122
+ >>> # Define a simple tetrahedron (4 faces, 4 vertices)
123
+ >>> faces = np.array([[0, 1, 2],
124
+ ... [0, 1, 3],
125
+ ... [0, 2, 3],
126
+ ... [1, 2, 3]])
127
+ >>> csr_graph = triangulated_mesh_to_csr(faces)
128
+ >>> print("Adjacency matrix:")
129
+ >>> print(csr_graph.toarray())
130
+ Adjacency matrix:
131
+ [[0 1 1 1]
132
+ [1 0 1 1]
133
+ [1 1 0 1]
134
+ [1 1 1 0]]
135
+
136
+ >>> # Triangle mesh with explicit vertex count
137
+ >>> faces_triangle = np.array([[0, 1, 2]])
138
+ >>> csr_triangle = triangulated_mesh_to_csr(faces_triangle, n_vertices=5)
139
+ >>> print(f"Shape: {csr_triangle.shape}")
140
+ >>> print("Connections for triangle [0,1,2]:")
141
+ >>> print(csr_triangle.toarray())
142
+ Shape: (5, 5)
143
+ Connections for triangle [0,1,2]:
144
+ [[0 1 1 0 0]
145
+ [1 0 1 0 0]
146
+ [1 1 0 0 0]
147
+ [0 0 0 0 0]
148
+ [0 0 0 0 0]]
149
+ """
150
+ if not isinstance(faces, np.ndarray):
151
+ raise TypeError("Faces must be a numpy array")
152
+
153
+ if faces.ndim != 2:
154
+ raise ValueError("Faces array must be 2D")
155
+
156
+ if faces.shape[1] != 3:
157
+ raise ValueError(
158
+ "Faces array must have exactly 3 columns for triangulated mesh"
159
+ )
160
+
161
+ if not np.issubdtype(faces.dtype, np.integer):
162
+ raise TypeError("Faces array must contain integer vertex indices")
163
+
164
+ if np.any(faces < 0):
165
+ raise ValueError("Vertex indices must be non-negative")
166
+
167
+ max_vertex_idx = np.max(faces)
168
+
169
+ if n_vertices is None:
170
+ n_vertices = max_vertex_idx + 1
171
+ elif n_vertices <= max_vertex_idx:
172
+ raise ValueError(
173
+ f"n_vertices ({n_vertices}) must be greater than maximum vertex index ({max_vertex_idx})"
174
+ )
175
+
176
+ # Extract all edges from triangular faces
177
+ # Each triangle (v0, v1, v2) generates edges: (v0,v1), (v1,v2), (v0,v2)
178
+ edges = []
179
+ for face in faces:
180
+ v0, v1, v2 = face
181
+ edges.extend([(v0, v1), (v1, v2), (v0, v2)])
182
+
183
+ edges = np.array(edges)
184
+
185
+ # Create symmetric edges (undirected graph)
186
+ edges_symmetric = np.vstack([edges, edges[:, [1, 0]]])
187
+
188
+ # Remove duplicate edges and create CSR matrix
189
+ row_indices = edges_symmetric[:, 0]
190
+ col_indices = edges_symmetric[:, 1]
191
+ data = np.ones(len(edges_symmetric), dtype=int)
192
+
193
+ # Create sparse matrix and eliminate duplicates by summing
194
+ csr_graph = csr_matrix(
195
+ (data, (row_indices, col_indices)), shape=(n_vertices, n_vertices)
196
+ )
197
+
198
+ # Convert to binary (in case of duplicate edges)
199
+ csr_graph.data = (csr_graph.data > 0).astype(int)
200
+
201
+ return csr_graph
202
+
203
+
204
+ ####################################################################################################
205
+ def edges_to_csr(
206
+ edges: np.ndarray,
207
+ edge_values: np.ndarray,
208
+ n_vertices: Optional[int] = None,
209
+ symmetric: bool = True,
210
+ ) -> csr_matrix:
211
+ """
212
+ Convert an edge list with values to CSR graph representation.
213
+
214
+ This method constructs a graph from a list of edges and their corresponding
215
+ weights/values. Useful for creating graphs from pre-computed edge lists.
216
+
217
+ Parameters
218
+ ----------
219
+ edges : np.ndarray
220
+ A 2D numpy array of shape (n_edges, 2) where each row contains the
221
+ indices of two connected vertices. Vertex indices should be non-negative integers.
222
+
223
+ edge_values : np.ndarray
224
+ A 1D numpy array of length n_edges containing the weight/value for each edge.
225
+ Values can be any numeric type (int, float, etc.).
226
+
227
+ n_vertices : int, optional
228
+ Total number of vertices in the graph. If None, it will be inferred
229
+ as the maximum vertex index + 1. Providing this parameter is recommended
230
+ for graphs with isolated vertices.
231
+
232
+ symmetric : bool, default=True
233
+ If True, creates an undirected graph by adding reverse edges with the same values.
234
+ If False, creates a directed graph using only the provided edges.
235
+
236
+ Returns
237
+ -------
238
+ csr_matrix
239
+ A scipy sparse CSR matrix of shape (n_vertices, n_vertices) where
240
+ entry (i,j) contains the weight of the edge from vertex i to vertex j.
241
+ For undirected graphs (symmetric=True), the matrix is symmetric.
242
+
243
+ Raises
244
+ ------
245
+ ValueError
246
+ If edges array is not 2D, doesn't have 2 columns, contains negative indices,
247
+ edge_values length doesn't match number of edges, or if n_vertices is less
248
+ than the maximum vertex index.
249
+
250
+ TypeError
251
+ If edges contains non-integer values or edge_values is not numeric.
252
+
253
+ Examples
254
+ --------
255
+ >>> import numpy as np
256
+ >>> # Create a simple weighted graph
257
+ >>> edges = np.array([[0, 1],
258
+ ... [1, 2],
259
+ ... [0, 2]])
260
+ >>> values = np.array([2.5, 1.0, 3.2])
261
+ >>> csr_graph = edges_to_csr(edges, values)
262
+ >>> print("Symmetric weighted graph:")
263
+ >>> print(csr_graph.toarray())
264
+ Symmetric weighted graph:
265
+ [[0. 2.5 3.2]
266
+ [2.5 0. 1. ]
267
+ [3.2 1. 0. ]]
268
+
269
+ >>> # Directed graph example
270
+ >>> edges_directed = np.array([[0, 1], [1, 2]])
271
+ >>> values_directed = np.array([0.8, 1.5])
272
+ >>> csr_directed = edges_to_csr(edges_directed, values_directed, symmetric=False)
273
+ >>> print("Directed graph:")
274
+ >>> print(csr_directed.toarray())
275
+ Directed graph:
276
+ [[0. 0.8 0. ]
277
+ [0. 0. 1.5]
278
+ [0. 0. 0. ]]
279
+
280
+ >>> # Handle duplicate edges (values are summed)
281
+ >>> edges_dup = np.array([[0, 1], [0, 1], [1, 0]])
282
+ >>> values_dup = np.array([1.0, 2.0, 0.5])
283
+ >>> csr_dup = edges_to_csr(edges_dup, values_dup)
284
+ >>> print("Duplicate edges (summed):")
285
+ >>> print(csr_dup.toarray())
286
+ Duplicate edges (summed):
287
+ [[0. 3.5]
288
+ [3.5 0. ]]
289
+ """
290
+ if not isinstance(edges, np.ndarray):
291
+ raise TypeError("Edges must be a numpy array")
292
+
293
+ if not isinstance(edge_values, np.ndarray):
294
+ raise TypeError("Edge values must be a numpy array")
295
+
296
+ if edges.ndim != 2:
297
+ raise ValueError("Edges array must be 2D")
298
+
299
+ if edges.shape[1] != 2:
300
+ raise ValueError("Edges array must have exactly 2 columns")
301
+
302
+ if edge_values.ndim != 1:
303
+ raise ValueError("Edge values must be a 1D array")
304
+
305
+ if len(edges) != len(edge_values):
306
+ raise ValueError(
307
+ f"Number of edges ({len(edges)}) must match number of edge values ({len(edge_values)})"
308
+ )
309
+
310
+ if not np.issubdtype(edges.dtype, np.integer):
311
+ raise TypeError("Edges array must contain integer vertex indices")
312
+
313
+ if not np.issubdtype(edge_values.dtype, np.number):
314
+ raise TypeError("Edge values must be numeric")
315
+
316
+ if np.any(edges < 0):
317
+ raise ValueError("Vertex indices must be non-negative")
318
+
319
+ if len(edges) == 0:
320
+ warnings.warn("Empty edge list provided", UserWarning)
321
+ if n_vertices is None:
322
+ n_vertices = 0
323
+ return csr_matrix((n_vertices, n_vertices))
324
+
325
+ max_vertex_idx = np.max(edges)
326
+
327
+ if n_vertices is None:
328
+ n_vertices = max_vertex_idx + 1
329
+ elif n_vertices <= max_vertex_idx:
330
+ raise ValueError(
331
+ f"n_vertices ({n_vertices}) must be greater than maximum vertex index ({max_vertex_idx})"
332
+ )
333
+
334
+ # Prepare edge data
335
+ if symmetric:
336
+ # Add reverse edges for undirected graph
337
+ all_edges = np.vstack([edges, edges[:, [1, 0]]])
338
+ all_values = np.concatenate([edge_values, edge_values])
339
+ else:
340
+ all_edges = edges
341
+ all_values = edge_values
342
+
343
+ row_indices = all_edges[:, 0]
344
+ col_indices = all_edges[:, 1]
345
+
346
+ # Create CSR matrix (duplicate edges will be summed automatically)
347
+ csr_graph = csr_matrix(
348
+ (all_values, (row_indices, col_indices)), shape=(n_vertices, n_vertices)
349
+ )
350
+
351
+ return csr_graph