topologicpy 0.7.96__py3-none-any.whl → 0.7.98__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.
topologicpy/Graph.py CHANGED
@@ -750,102 +750,6 @@ class Graph:
750
750
  _ = graph.AllPaths(vertexA, vertexB, True, timeLimit, paths) # Hook to Core
751
751
  return paths
752
752
 
753
- @staticmethod
754
- def IsIsomorphic(graphA, graphB, maxIterations=10, silent=False):
755
- """
756
- Tests if the two input graphs are isomorphic according to the Weisfeiler Lehman graph isomorphism test. See https://en.wikipedia.org/wiki/Weisfeiler_Leman_graph_isomorphism_test
757
-
758
- Parameters
759
- ----------
760
- graphA : topologic_core.Graph
761
- The first input graph.
762
- graphB : topologic_core.Graph
763
- The second input graph.
764
- maxIterations : int , optional
765
- This number limits the number of iterations to prevent the function from running indefinitely, particularly for very large or complex graphs.
766
- silent : bool , optional
767
- If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
768
-
769
- Returns
770
- -------
771
- bool
772
- True if the two input graphs are isomorphic. False otherwise
773
-
774
- """
775
-
776
- from topologicpy.Topology import Topology
777
-
778
- def weisfeiler_lehman_test(graph1, graph2, max_iterations=10):
779
- """
780
- Test if two graphs are isomorphic using the Weisfeiler-Leman (WL) algorithm with early stopping.
781
-
782
- Parameters:
783
- graph1 (dict): Adjacency list representation of the first graph.
784
- graph2 (dict): Adjacency list representation of the second graph.
785
- max_iterations (int): Maximum WL iterations allowed (default is 10).
786
-
787
- Returns:
788
- bool: True if the graphs are WL-isomorphic, False otherwise.
789
- """
790
-
791
- def wl_iteration(labels, graph):
792
- """Perform one WL iteration and return updated labels."""
793
- new_labels = {}
794
- for node in graph:
795
- neighborhood_labels = sorted([labels[neighbor] for neighbor in graph[node]])
796
- new_labels[node] = (labels[node], tuple(neighborhood_labels))
797
- unique_labels = {}
798
- count = 0
799
- for node in sorted(new_labels):
800
- if new_labels[node] not in unique_labels:
801
- unique_labels[new_labels[node]] = count
802
- count += 1
803
- new_labels[node] = unique_labels[new_labels[node]]
804
- return new_labels
805
-
806
- # Initialize labels
807
- labels1 = {node: 1 for node in graph1}
808
- labels2 = {node: 1 for node in graph2}
809
-
810
- for i in range(max_iterations):
811
- # Perform WL iteration for both graphs
812
- new_labels1 = wl_iteration(labels1, graph1)
813
- new_labels2 = wl_iteration(labels2, graph2)
814
-
815
- # Check if the label distributions match
816
- if sorted(new_labels1.values()) != sorted(new_labels2.values()):
817
- return False
818
-
819
- # Check for stability (early stopping)
820
- if new_labels1 == labels1 and new_labels2 == labels2:
821
- break
822
-
823
- # Update labels for next iteration
824
- labels1, labels2 = new_labels1, new_labels2
825
-
826
- return True
827
-
828
- if not Topology.IsInstance(graphA, "Graph") and not Topology.IsInstance(graphB, "Graph"):
829
- if not silent:
830
- print("Graph.IsIsomorphic - Error: The input graph parameters are not valid graphs. Returning None.")
831
- return None
832
- if not Topology.IsInstance(graphA, "Graph"):
833
- if not silent:
834
- print("Graph.IsIsomorphic - Error: The input graphA parameter is not a valid graph. Returning None.")
835
- return None
836
- if not Topology.IsInstance(graphB, "Graph"):
837
- if not silent:
838
- print("Graph.IsIsomorphic - Error: The input graphB parameter is not a valid graph. Returning None.")
839
- return None
840
- if maxIterations <= 0:
841
- if not silent:
842
- print("Graph.IsIsomorphic - Error: The input maxIterations parameter is not within a valid range. Returning None.")
843
- return None
844
-
845
- g1 = Graph.AdjacencyDictionary(graphA)
846
- g2 = Graph.AdjacencyDictionary(graphB)
847
- return weisfeiler_lehman_test(g1, g2, max_iterations=maxIterations)
848
-
849
753
  @staticmethod
850
754
  def AverageClusteringCoefficient(graph, mantissa: int = 6, silent: bool = False):
851
755
  """
@@ -1336,16 +1240,18 @@ class Graph:
1336
1240
  return bot_graph.serialize(format=format)
1337
1241
 
1338
1242
  @staticmethod
1339
- def BetweennessCentrality(graph, key: str = "betweenness_centrality", mantissa: int = 6, tolerance: float = 0.001, silent: bool = False):
1243
+ def BetweennessCentrality(graph, key: str = "betweenness_centrality", method="vertex", mantissa: int = 6, tolerance: float = 0.001, silent: bool = False):
1340
1244
  """
1341
- Returns the betweenness centrality measure of the input list of vertices within the input graph. The order of the returned list is the same as the order of the input list of vertices. If no vertices are specified, the betweeness centrality of all the vertices in the input graph is computed. See https://en.wikipedia.org/wiki/Betweenness_centrality.
1245
+ Returns the betweenness centrality of the input graph. The order of the returned list is the same as the order of vertices/edges. See https://en.wikipedia.org/wiki/Betweenness_centrality.
1342
1246
 
1343
1247
  Parameters
1344
1248
  ----------
1345
1249
  graph : topologic_core.Graph
1346
1250
  The input graph.
1347
1251
  key : str , optional
1348
- The dictionary key under which to save the betweeness centrality score. The default is "betweenness_centrality".
1252
+ The dictionary key under which to store the betweeness centrality score. The default is "betweenness_centrality".
1253
+ method : str , optional
1254
+ The method of computing the betweenness centrality. The options are "vertex" or "edge". The default is "vertex".
1349
1255
  mantissa : int , optional
1350
1256
  The desired length of the mantissa. The default is 6.
1351
1257
  tolerance : float , optional
@@ -1372,7 +1278,7 @@ class Graph:
1372
1278
  shortest_paths[path[-1]].append(path)
1373
1279
  return shortest_paths
1374
1280
 
1375
- def calculate_betweenness():
1281
+ def calculate_vertex_betweenness():
1376
1282
  betweenness = {v: 0.0 for v in py_graph}
1377
1283
  for s in py_graph:
1378
1284
  shortest_paths = shortest_paths_count(s)
@@ -1387,6 +1293,56 @@ class Graph:
1387
1293
  betweenness[v] += dependency[v]
1388
1294
  return betweenness
1389
1295
 
1296
+ def calculate_edge_betweenness(graph_adj_matrix):
1297
+ n = len(graph_adj_matrix)
1298
+ edge_betweenness_scores = {}
1299
+
1300
+ # Iterate over all node pairs as source and target nodes
1301
+ for source in range(n):
1302
+ # Initialize the 'distance' and 'predecessors' for each node
1303
+ distance = [-1] * n
1304
+ predecessors = [[] for _ in range(n)]
1305
+ distance[source] = 0
1306
+ stack = []
1307
+ queue = [source]
1308
+
1309
+ # Breadth-first search to find shortest paths
1310
+ while queue:
1311
+ current_node = queue.pop(0)
1312
+ stack.append(current_node)
1313
+ for neighbor in range(n):
1314
+ if graph_adj_matrix[current_node][neighbor] == 1:
1315
+ if distance[neighbor] == -1: # First time visiting neighbor
1316
+ distance[neighbor] = distance[current_node] + 1
1317
+ queue.append(neighbor)
1318
+ if distance[neighbor] == distance[current_node] + 1: # Shortest path
1319
+ predecessors[neighbor].append(current_node)
1320
+
1321
+ # Initialize the dependency values for each node
1322
+ dependency = [0] * n
1323
+
1324
+ # Process the nodes in reverse order of discovery
1325
+ while stack:
1326
+ current_node = stack.pop()
1327
+ for pred in predecessors[current_node]:
1328
+ dependency[pred] += (1 + dependency[current_node]) / len(predecessors[current_node])
1329
+
1330
+ # Update edge betweenness scores
1331
+ if pred < current_node:
1332
+ edge = (pred, current_node)
1333
+ else:
1334
+ edge = (current_node, pred)
1335
+
1336
+ if edge not in edge_betweenness_scores:
1337
+ edge_betweenness_scores[edge] = 0
1338
+ edge_betweenness_scores[edge] += dependency[current_node]
1339
+
1340
+ # Normalize edge betweenness scores by dividing by 2 (since each edge is counted twice)
1341
+ for edge in edge_betweenness_scores:
1342
+ edge_betweenness_scores[edge] /= 2
1343
+
1344
+ return edge_betweenness_scores
1345
+
1390
1346
  from topologicpy.Topology import Topology
1391
1347
  from topologicpy.Dictionary import Dictionary
1392
1348
 
@@ -1395,39 +1351,176 @@ class Graph:
1395
1351
  print("Graph.BetweenessCentrality - Error: The input graph is not a valid graph. Returning None.")
1396
1352
  return None
1397
1353
 
1398
- vertices = Graph.Vertices(graph)
1354
+ if "v" in method.lower():
1355
+ vertices = Graph.Vertices(graph)
1399
1356
 
1400
- if len(vertices) < 1:
1357
+ if len(vertices) < 1:
1358
+ if not silent:
1359
+ print("Graph.BetweenessCentrality - Error: The input graph does not contain valid vertices. Returning None.")
1360
+ return None
1361
+ if len(vertices) == 1:
1362
+ d = Topology.Dictionary(vertices[0])
1363
+ d = Dictionary.SetValueAtKey(d, key, 1.0)
1364
+ vertices[0] = Topology.SetDictionary(vertices[0], d)
1365
+ return [1.0]
1366
+
1367
+ py_graph = Graph.AdjacencyDictionary(graph)
1368
+ vertex_betweenness = calculate_vertex_betweenness()
1369
+ for v in vertex_betweenness:
1370
+ vertex_betweenness[v] /= 2.0 # Each shortest path is counted twice
1371
+
1372
+ min_betweenness = min(vertex_betweenness.values())
1373
+ max_betweenness = max(vertex_betweenness.values())
1374
+ if (max_betweenness - min_betweenness) > 0:
1375
+ for v in vertex_betweenness:
1376
+ vertex_betweenness[v] = (vertex_betweenness[v] - min_betweenness)/ (max_betweenness - min_betweenness) # Normalize to [0, 1]
1377
+
1378
+
1379
+ vertex_betweenness_scores = [0]*len(vertices)
1380
+ for i, score in vertex_betweenness.items():
1381
+ vertex = vertices[int(i)]
1382
+ d = Topology.Dictionary(vertex)
1383
+ d = Dictionary.SetValueAtKey(d, key, round(score, mantissa))
1384
+ vertex = Topology.SetDictionary(vertex, d)
1385
+ vertex_betweenness_scores[int(i)] = round(score, mantissa)
1386
+
1387
+ return vertex_betweenness_scores
1388
+ else:
1389
+ graph_edges = Graph.Edges(graph)
1390
+ adj_matrix = Graph.AdjacencyMatrix(graph)
1391
+ meshData = Graph.MeshData(graph)
1392
+ edges = meshData["edges"]
1393
+ if len(graph_edges) < 1:
1394
+ if not silent:
1395
+ print("Graph.BetweenessCentrality - Error: The input graph does not contain any edges. Returning None.")
1396
+ return None
1397
+ if len(graph_edges) == 1:
1398
+ d = Topology.Dictionary(graph_edges[0])
1399
+ d = Dictionary.SetValueAtKey(d, key, 1.0)
1400
+ graph_edges[0] = Topology.SetDictionary(graph_edges[0], d)
1401
+ return [1.0]
1402
+
1403
+ edge_betweenness = calculate_edge_betweenness(adj_matrix)
1404
+ keys = list(edge_betweenness.keys())
1405
+ values = list(edge_betweenness.values())
1406
+ min_value = min(values)
1407
+ max_value = max(values)
1408
+ edge_betweenness_scores = []
1409
+ for i, edge in enumerate(edges):
1410
+ u,v = edge
1411
+ if (u,v) in keys:
1412
+ score = edge_betweenness[(u,v)]
1413
+ elif (v,u) in keys:
1414
+ score = edge_betweenness[(v,u)]
1415
+ else:
1416
+ score = 0
1417
+ score = (score - min_value)/(max_value - min_value)
1418
+ edge_betweenness_scores.append(round(score, mantissa))
1419
+ d = Topology.Dictionary(graph_edges[i])
1420
+ d = Dictionary.SetValueAtKey(d, key, round(score, mantissa))
1421
+ graph_edges[i] = Topology.SetDictionary(graph_edges[i], d)
1422
+ return edge_betweenness_scores
1423
+
1424
+ @staticmethod
1425
+ def Bridges(graph, key: str = "bridge", silent: bool = False):
1426
+ """
1427
+ Returns the list of bridge edges in the input graph. See: https://en.wikipedia.org/wiki/Bridge_(graph_theory)
1428
+
1429
+ Parameters
1430
+ ----------
1431
+ graph : topologic_core.Graph
1432
+ The input graph.
1433
+ key : str , optional
1434
+ The edge dictionary key under which to store the bridge status. 0 means the edge is NOT a bridge. 1 means that the edge IS a bridge. The default is "bridge".
1435
+ silent : bool , optional
1436
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
1437
+
1438
+ Returns
1439
+ -------
1440
+ list
1441
+ The list of bridge edges in the input graph.
1442
+ """
1443
+ from topologicpy.Topology import Topology
1444
+ from topologicpy.Dictionary import Dictionary
1445
+ from topologicpy.Graph import Graph
1446
+
1447
+ if not Topology.IsInstance(graph, "graph"):
1401
1448
  if not silent:
1402
- print("Graph.BetweenessCentrality - Error: The input graph does not contain valid vertices. Returning None.")
1449
+ print("Graph.Bridges - Error: The input graph parameter is not a valid topologic graph. Returning None")
1403
1450
  return None
1404
- if len(vertices) == 1:
1405
- d = Topology.Dictionary(vertices[0])
1406
- d = Dictionary.SetValueAtKey(d, key, 1.0)
1407
- vertices[0] = Topology.SetDictionary(vertices[0], d)
1408
- return [1.0]
1409
-
1410
- py_graph = Graph.AdjacencyDictionary(graph)
1411
- betweenness = calculate_betweenness()
1412
- for v in betweenness:
1413
- betweenness[v] /= 2.0 # Each shortest path is counted twice
1414
1451
 
1415
- max_betweenness = max(betweenness.values())
1416
- if max_betweenness > 0:
1417
- for v in betweenness:
1418
- betweenness[v] /= max_betweenness # Normalize to [0, 1]
1419
-
1420
-
1421
- return_betweenness = [0]*len(vertices)
1422
- for i, v in betweenness.items():
1423
- vertex = vertices[int(i)]
1424
- d = Topology.Dictionary(vertex)
1425
- d = Dictionary.SetValueAtKey(d, key, round(v, mantissa))
1426
- vertex = Topology.SetDictionary(vertex, d)
1427
- return_betweenness[int(i)] = v
1428
-
1429
- return return_betweenness
1430
-
1452
+ graph_edges = Graph.Edges(graph)
1453
+ for edge in graph_edges:
1454
+ d = Topology.Dictionary(edge)
1455
+ d = Dictionary.SetValueAtKey(d, key, 0)
1456
+ edge = Topology.SetDictionary(edge, d)
1457
+ mesh_data = Graph.MeshData(graph)
1458
+ mesh_edges = mesh_data['edges']
1459
+
1460
+ # Get adjacency dictionary
1461
+ adjacency_dict = Graph.AdjacencyDictionary(graph)
1462
+ if not adjacency_dict:
1463
+ if not silent:
1464
+ print("Graph.Bridges - Error: Failed to retrieve adjacency dictionary. Returning None")
1465
+ return None
1466
+
1467
+ # Helper function to perform DFS and find bridges
1468
+ def dfs(vertex, parent, time, low, disc, visited, adjacency_dict, bridges, edge_map):
1469
+ visited[int(vertex)] = True
1470
+ disc[int(vertex)] = low[int(vertex)] = time[0]
1471
+ time[0] += 1
1472
+ for neighbor in adjacency_dict[vertex]:
1473
+ if not visited[int(neighbor)]:
1474
+ dfs(neighbor, vertex, time, low, disc, visited, adjacency_dict, bridges, edge_map)
1475
+ low[int(vertex)] = min(low[int(vertex)], low[int(neighbor)])
1476
+
1477
+ # Check if edge is a bridge
1478
+ if low[int(neighbor)] > disc[int(vertex)]:
1479
+ bridges.add((vertex, neighbor))
1480
+ elif neighbor != parent:
1481
+ low[int(vertex)] = min(low[int(vertex)], disc[int(neighbor)])
1482
+
1483
+ # Prepare adjacency list and edge mapping
1484
+ vertices = list(adjacency_dict.keys())
1485
+ num_vertices = len(vertices)
1486
+ visited = [False] * num_vertices
1487
+ disc = [-1] * num_vertices
1488
+ low = [-1] * num_vertices
1489
+ time = [0]
1490
+ bridges = set()
1491
+
1492
+ # Map edges to indices
1493
+ edge_map = {}
1494
+ index = 0
1495
+ for vertex, neighbors in adjacency_dict.items():
1496
+ for neighbor in neighbors:
1497
+ if (neighbor, vertex) not in edge_map: # Avoid duplicating edges in undirected graphs
1498
+ edge_map[(vertex, neighbor)] = index
1499
+ index += 1
1500
+
1501
+ # Run DFS from all unvisited vertices
1502
+ for i, vertex in enumerate(vertices):
1503
+ if not visited[i]:
1504
+ dfs(vertex, -1, time, low, disc, visited, adjacency_dict, bridges, edge_map)
1505
+
1506
+ # Mark bridges in the edges' dictionaries
1507
+ bridge_edges = []
1508
+ for edge in bridges:
1509
+ i, j = edge
1510
+ i = int(i)
1511
+ j = int(j)
1512
+ try:
1513
+ edge_index = mesh_edges.index([i,j])
1514
+ except:
1515
+ edge_index = mesh_edges.index([j,i])
1516
+ bridge_edges.append(graph_edges[edge_index])
1517
+ for edge in bridge_edges:
1518
+ d = Topology.Dictionary(edge)
1519
+ d = Dictionary.SetValueAtKey(d, key, 1)
1520
+ edge = Topology.SetDictionary(edge, d)
1521
+
1522
+ return bridge_edges
1523
+
1431
1524
  @staticmethod
1432
1525
  def ByAdjacencyMatrixCSVPath(path: str, dictionaries: list = None, silent: bool = False):
1433
1526
  """
@@ -1514,7 +1607,8 @@ class Graph:
1514
1607
  for i in range(len(adjacencyMatrix)):
1515
1608
  x, y, z = random.uniform(xMin,xMax), random.uniform(yMin,yMax), random.uniform(zMin,zMax)
1516
1609
  v = Vertex.ByCoordinates(x, y, z)
1517
- v = Topology.SetDictionary(v, dictionaries[i])
1610
+ if isinstance(dictionaries, list):
1611
+ v = Topology.SetDictionary(v, dictionaries[i])
1518
1612
  vertices.append(v)
1519
1613
 
1520
1614
  # Create the graph using vertices and edges
@@ -4181,6 +4275,82 @@ class Graph:
4181
4275
  v = Topology.SetDictionary(v, d)
4182
4276
  return graph
4183
4277
 
4278
+ @staticmethod
4279
+ def Complement(graph, tolerance=0.0001, silent=False):
4280
+ """
4281
+ Creates the complement graph of the input graph. See https://en.wikipedia.org/wiki/Complement_graph
4282
+
4283
+ Parameters
4284
+ ----------
4285
+ graph : topologicpy.Graph
4286
+ The input topologic graph.
4287
+ tolerance : float , optional
4288
+ The desired tolerance. The default is 0.0001.
4289
+ silent : bool , optional
4290
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4291
+
4292
+ Returns
4293
+ -------
4294
+ topologicpy.Graph
4295
+ The created complement topologic graph.
4296
+
4297
+ """
4298
+ def complement_graph(adj_dict):
4299
+ """
4300
+ Creates the complement graph from an input adjacency dictionary.
4301
+
4302
+ Parameters:
4303
+ adj_dict (dict): The adjacency dictionary where keys are nodes and
4304
+ values are lists of connected nodes.
4305
+
4306
+ Returns:
4307
+ list of tuples: A list of edge index tuples representing the complement graph.
4308
+ """
4309
+ # Get all nodes in the graph
4310
+ nodes = list(adj_dict.keys())
4311
+ # Initialize a set to store edges of the complement graph
4312
+ complement_edges = set()
4313
+ # Convert adjacency dictionary to a set of existing edges
4314
+ existing_edges = set()
4315
+ for node, neighbors in adj_dict.items():
4316
+ for neighbor in neighbors:
4317
+ # Add the edge as an ordered tuple to ensure no duplicates
4318
+ existing_edges.add(tuple(sorted((node, neighbor))))
4319
+ # Generate all possible edges and check if they exist in the original graph
4320
+ for i, node1 in enumerate(nodes):
4321
+ for j in range(i + 1, len(nodes)):
4322
+ node2 = nodes[j]
4323
+ edge = tuple(sorted((node1, node2)))
4324
+ # Add the edge if it's not in the original graph
4325
+ if edge not in existing_edges:
4326
+ complement_edges.add(edge)
4327
+ # Return the complement edges as a sorted list of tuples
4328
+ return sorted(complement_edges)
4329
+
4330
+ from topologicpy.Graph import Graph
4331
+ from topologicpy.Edge import Edge
4332
+ from topologicpy.Vertex import Vertex
4333
+ from topologicpy.Topology import Topology
4334
+
4335
+ if not Topology.IsInstance(graph, "graph"):
4336
+ if not silent:
4337
+ print("Graph.Complement - Error: The input graph parameter is not a valid topologic graph. Returning None.")
4338
+ return None
4339
+ adj_dict = Graph.AdjacencyDictionary(graph)
4340
+ py_edges = complement_graph(adj_dict)
4341
+ vertices = Graph.Vertices(graph)
4342
+ adjusted_vertices = Vertex.Separate(vertices, minDistance=tolerance)
4343
+ edges = []
4344
+ for py_edge in py_edges:
4345
+ start, end = py_edge
4346
+ sv = adjusted_vertices[int(start)]
4347
+ ev = adjusted_vertices[int(end)]
4348
+ edge = Edge.ByVertices(sv, ev, tolerance=tolerance, silent=silent)
4349
+ if Topology.IsInstance(edge, "edge"):
4350
+ edges.append(edge)
4351
+ return_graph = Graph.ByVerticesEdges(adjusted_vertices, edges)
4352
+ return return_graph
4353
+
4184
4354
  @staticmethod
4185
4355
  def Complete(graph, silent: bool = False):
4186
4356
  """
@@ -4410,7 +4580,7 @@ class Graph:
4410
4580
  vertices : list , optional
4411
4581
  The input list of vertices. The default is None.
4412
4582
  key : str , optional
4413
- The dictionary key under which to save the closeness centrality score. The default is "closeness_centrality".
4583
+ The dictionary key under which to store the closeness centrality score. The default is "closeness_centrality".
4414
4584
  mantissa : int , optional
4415
4585
  The desired length of the mantissa. The default is 6.
4416
4586
  tolerance : float , optional
@@ -4498,7 +4668,7 @@ class Graph:
4498
4668
  graph : topologicp.Graph
4499
4669
  The input topologic graph.
4500
4670
  key : str , optional
4501
- The dictionary key under which to save the closeness centrality score. The default is "community".
4671
+ The dictionary key under which to store the closeness centrality score. The default is "community".
4502
4672
  mantissa : int , optional
4503
4673
  The desired length of the mantissa. The default is 6.
4504
4674
  tolerance : float , optional
@@ -4610,7 +4780,7 @@ class Graph:
4610
4780
  vertices : list , optional
4611
4781
  The input list of vertices. The default is None.
4612
4782
  key : str , optional
4613
- The dictionary key under which to save the connectivity score. The default is "connectivity".
4783
+ The dictionary key under which to store the connectivity score. The default is "connectivity".
4614
4784
  edgeKey : str , optional
4615
4785
  If specified, the value in the connected edges' dictionary specified by the edgeKey string will be aggregated to calculate
4616
4786
  the vertex degree. If a numeric value cannot be retrieved from an edge, a value of 1 is used instead. This is used in weighted graphs.
@@ -4705,6 +4875,67 @@ class Graph:
4705
4875
  return None
4706
4876
  return graph.ContainsVertex(vertex, tolerance) # Hook to Core
4707
4877
 
4878
+ @staticmethod
4879
+ def CutVertices(graph, key: str = "cut", silent: bool = False):
4880
+ """
4881
+ Returns the list of cut vertices in the input graph. See: https://en.wikipedia.org/wiki/Bridge_(graph_theory)
4882
+
4883
+ Parameters
4884
+ ----------
4885
+ graph : topologic_core.Graph
4886
+ The input graph.
4887
+ key : str , optional
4888
+ The vertex dictionary key under which to store the cut status. 0 means the vertex is NOT a cut vertex. 1 means that the vertex IS a cut vertex. The default is "cut".
4889
+ silent : bool , optional
4890
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4891
+
4892
+ Returns
4893
+ -------
4894
+ list
4895
+ The list of bridge edges in the input graph.
4896
+
4897
+ """
4898
+ import os
4899
+ import warnings
4900
+ from topologicpy.Topology import Topology
4901
+ from topologicpy.Dictionary import Dictionary
4902
+
4903
+ try:
4904
+ import igraph as ig
4905
+ except:
4906
+ print("Graph.CutVertices - Installing required pyhon-igraph library.")
4907
+ try:
4908
+ os.system("pip install python-igraph")
4909
+ except:
4910
+ os.system("pip install python-igraph --user")
4911
+ try:
4912
+ import igraph as ig
4913
+ print("Graph.CutVertices - python-igraph library installed correctly.")
4914
+ except:
4915
+ warnings.warn("Graph.CutVertices - Error: Could not import python-igraph. Please install manually.")
4916
+
4917
+ if not Topology.IsInstance(graph, "graph"):
4918
+ if not silent:
4919
+ print("Graph.CutVertices - Error: The input graph parameter is not a valid topologic graph. Returning None")
4920
+ return None
4921
+
4922
+ vertices = Graph.Vertices(graph)
4923
+ mesh_data = Graph.MeshData(graph)
4924
+ graph_edges = mesh_data['edges']
4925
+ ig_graph = ig.Graph(edges=graph_edges)
4926
+ articulation_points = ig_graph.vs[ig_graph.articulation_points()]
4927
+ articulation_points_list = [v.index for v in articulation_points]
4928
+ cut_vertices = []
4929
+ for i, vertex in enumerate(vertices):
4930
+ d = Topology.Dictionary(vertex)
4931
+ if i in articulation_points_list:
4932
+ d = Dictionary.SetValueAtKey(d, key, 1)
4933
+ cut_vertices.append(vertex)
4934
+ else:
4935
+ d = Dictionary.SetValueAtKey(d, key, 0)
4936
+ vertex = Topology.SetDictionary(vertex, d)
4937
+
4938
+ return cut_vertices
4708
4939
 
4709
4940
  @staticmethod
4710
4941
  def Degree(graph, vertices=None, key: str = "degree", edgeKey: str = None, mantissa: int = 6, tolerance = 0.0001):
@@ -4718,7 +4949,7 @@ class Graph:
4718
4949
  vertices : list , optional
4719
4950
  The input list of vertices. The default is None.
4720
4951
  key : str , optional
4721
- The dictionary key under which to save the closeness centrality score. The default is "degree".
4952
+ The dictionary key under which to store the closeness centrality score. The default is "degree".
4722
4953
  edgeKey : str , optional
4723
4954
  If specified, the value in the connected edges' dictionary specified by the edgeKey string will be aggregated to calculate
4724
4955
  the vertex degree. If a numeric value cannot be retrieved from an edge, a value of 1 is used instead. This is used in weighted graphs.
@@ -4760,6 +4991,28 @@ class Graph:
4760
4991
  scores.append(degree)
4761
4992
  return scores
4762
4993
 
4994
+ @staticmethod
4995
+ def DegreeMatrix(graph):
4996
+ """
4997
+ Returns the degree matrix of the input graph. See https://en.wikipedia.org/wiki/Degree_matrix.
4998
+
4999
+ Parameters
5000
+ ----------
5001
+ graph : topologic_core.Graph
5002
+ The input graph.
5003
+
5004
+ Returns
5005
+ -------
5006
+ list
5007
+ The degree matrix of the input graph.
5008
+
5009
+ """
5010
+ import numpy as np
5011
+ adj_matrix = Graph.AdjacencyMatrix(g)
5012
+ np_adj_matrix = np.array(adj_matrix)
5013
+ degree_matrix = np.diag(np_adj_matrix.sum(axis=1))
5014
+ return degree_matrix.tolist()
5015
+
4763
5016
  @staticmethod
4764
5017
  def DegreeSequence(graph):
4765
5018
  """
@@ -4874,7 +5127,7 @@ class Graph:
4874
5127
  vertices : list , optional
4875
5128
  The input list of vertices. The default is None.
4876
5129
  key : str , optional
4877
- The dictionary key under which to save the depth score. The default is "depth".
5130
+ The dictionary key under which to store the depth score. The default is "depth".
4878
5131
  type : str , optional
4879
5132
  The type of depth distance to calculate. The options are "topological" or "metric". The default is "topological". See https://www.spacesyntax.online/overview-2/analysis-of-spatial-relations/.
4880
5133
  mantissa : int , optional
@@ -6127,7 +6380,134 @@ class Graph:
6127
6380
  f.close()
6128
6381
  return False
6129
6382
  return False
6130
-
6383
+
6384
+ @staticmethod
6385
+ def FiedlerVector(graph, mantissa = 6, silent: bool = False):
6386
+ """
6387
+ Computes the Fiedler vector of a graph. See https://en.wikipedia.org/wiki/Algebraic_connectivity.
6388
+
6389
+ Parameters
6390
+ ----------
6391
+ graph : topologic_core.Graph
6392
+ The input graph
6393
+ mantissa : int , optional
6394
+ The desired length of the mantissa. The default is 6.
6395
+ silent : bool , optional
6396
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
6397
+
6398
+ Returns
6399
+ -------
6400
+ list
6401
+ The Fiedler vector (eigenvector corresponding to the second smallest eigenvalue).
6402
+ """
6403
+ from topologicpy.Topology import Topology
6404
+ from topologicpy.Matrix import Matrix
6405
+ import numpy as np
6406
+
6407
+ if not Topology.IsInstance(graph, "graph"):
6408
+ if not silent:
6409
+ print("Graph.FiedlerVector - Error: The input graph parameter is not a valid graph. Returning None.")
6410
+
6411
+ laplacian = Graph.Laplacian(graph)
6412
+ eigenvalues, eigenvectors = Matrix.EigenvaluesAndVectors(laplacian, mantissa=mantissa)
6413
+ return eigenvectors[1]
6414
+
6415
+ @staticmethod
6416
+ def IsIsomorphic(graphA, graphB, maxIterations=10, silent=False):
6417
+ """
6418
+ Tests if the two input graphs are isomorphic according to the Weisfeiler Lehman graph isomorphism test. See https://en.wikipedia.org/wiki/Weisfeiler_Leman_graph_isomorphism_test
6419
+
6420
+ Parameters
6421
+ ----------
6422
+ graphA : topologic_core.Graph
6423
+ The first input graph.
6424
+ graphB : topologic_core.Graph
6425
+ The second input graph.
6426
+ maxIterations : int , optional
6427
+ This number limits the number of iterations to prevent the function from running indefinitely, particularly for very large or complex graphs.
6428
+ silent : bool , optional
6429
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
6430
+
6431
+ Returns
6432
+ -------
6433
+ bool
6434
+ True if the two input graphs are isomorphic. False otherwise
6435
+
6436
+ """
6437
+
6438
+ from topologicpy.Topology import Topology
6439
+
6440
+ def weisfeiler_lehman_test(graph1, graph2, max_iterations=10):
6441
+ """
6442
+ Test if two graphs are isomorphic using the Weisfeiler-Leman (WL) algorithm with early stopping.
6443
+
6444
+ Parameters:
6445
+ graph1 (dict): Adjacency list representation of the first graph.
6446
+ graph2 (dict): Adjacency list representation of the second graph.
6447
+ max_iterations (int): Maximum WL iterations allowed (default is 10).
6448
+
6449
+ Returns:
6450
+ bool: True if the graphs are WL-isomorphic, False otherwise.
6451
+ """
6452
+
6453
+ def wl_iteration(labels, graph):
6454
+ """Perform one WL iteration and return updated labels."""
6455
+ new_labels = {}
6456
+ for node in graph:
6457
+ neighborhood_labels = sorted([labels[neighbor] for neighbor in graph[node]])
6458
+ new_labels[node] = (labels[node], tuple(neighborhood_labels))
6459
+ unique_labels = {}
6460
+ count = 0
6461
+ for node in sorted(new_labels):
6462
+ if new_labels[node] not in unique_labels:
6463
+ unique_labels[new_labels[node]] = count
6464
+ count += 1
6465
+ new_labels[node] = unique_labels[new_labels[node]]
6466
+ return new_labels
6467
+
6468
+ # Initialize labels
6469
+ labels1 = {node: 1 for node in graph1}
6470
+ labels2 = {node: 1 for node in graph2}
6471
+
6472
+ for i in range(max_iterations):
6473
+ # Perform WL iteration for both graphs
6474
+ new_labels1 = wl_iteration(labels1, graph1)
6475
+ new_labels2 = wl_iteration(labels2, graph2)
6476
+
6477
+ # Check if the label distributions match
6478
+ if sorted(new_labels1.values()) != sorted(new_labels2.values()):
6479
+ return False
6480
+
6481
+ # Check for stability (early stopping)
6482
+ if new_labels1 == labels1 and new_labels2 == labels2:
6483
+ break
6484
+
6485
+ # Update labels for next iteration
6486
+ labels1, labels2 = new_labels1, new_labels2
6487
+
6488
+ return True
6489
+
6490
+ if not Topology.IsInstance(graphA, "Graph") and not Topology.IsInstance(graphB, "Graph"):
6491
+ if not silent:
6492
+ print("Graph.IsIsomorphic - Error: The input graph parameters are not valid graphs. Returning None.")
6493
+ return None
6494
+ if not Topology.IsInstance(graphA, "Graph"):
6495
+ if not silent:
6496
+ print("Graph.IsIsomorphic - Error: The input graphA parameter is not a valid graph. Returning None.")
6497
+ return None
6498
+ if not Topology.IsInstance(graphB, "Graph"):
6499
+ if not silent:
6500
+ print("Graph.IsIsomorphic - Error: The input graphB parameter is not a valid graph. Returning None.")
6501
+ return None
6502
+ if maxIterations <= 0:
6503
+ if not silent:
6504
+ print("Graph.IsIsomorphic - Error: The input maxIterations parameter is not within a valid range. Returning None.")
6505
+ return None
6506
+
6507
+ g1 = Graph.AdjacencyDictionary(graphA)
6508
+ g2 = Graph.AdjacencyDictionary(graphB)
6509
+ return weisfeiler_lehman_test(g1, g2, max_iterations=maxIterations)
6510
+
6131
6511
  @staticmethod
6132
6512
  def Reshape(graph,
6133
6513
  shape="spring 2D",
@@ -7382,6 +7762,59 @@ class Graph:
7382
7762
  return None
7383
7763
  return graph.IsComplete()
7384
7764
 
7765
+ @staticmethod
7766
+ def IsConnected(graph, vertexA, vertexB, silent: bool = False):
7767
+ """
7768
+ Returns True if the two input vertices are directly connected by an edge. Returns False otherwise.
7769
+
7770
+ Parameters
7771
+ ----------
7772
+ graph : topologic_core.Graph
7773
+ The input graph.
7774
+ vertexA : topologic_core.Vertex
7775
+ The first input vertex.
7776
+ vertexB : topologic_core.Vertex
7777
+ The second input vertex
7778
+ silent : bool , optional
7779
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
7780
+
7781
+ Returns
7782
+ -------
7783
+ bool
7784
+ True if the input vertices are connected by an edge. False otherwise.
7785
+
7786
+ """
7787
+ from topologicpy.Topology import Topology
7788
+
7789
+ if not Topology.IsInstance(graph, "graph"):
7790
+ if not silent:
7791
+ print("Graph.IsConnected - Error: The input graph parameter is not a valid graph. Returning None.")
7792
+ return None
7793
+
7794
+ if not Topology.IsInstance(vertexA, "vertex"):
7795
+ if not silent:
7796
+ print("Graph.IsConnected - Error: The input vertexA parameter is not a valid vertex. Returning None.")
7797
+ return None
7798
+
7799
+ if not Topology.IsInstance(vertexB, "vertex"):
7800
+ if not silent:
7801
+ print("Graph.IsConnected - Error: The input vertexB parameter is not a valid vertex. Returning None.")
7802
+ return None
7803
+
7804
+ if vertexA == vertexB:
7805
+ if not silent:
7806
+ print("Graph.IsConnected - Warrning: The two input vertices are the same vertex. Returning False.")
7807
+ return False
7808
+ shortest_path = Graph.ShortestPath(graph, vertexA, vertexB)
7809
+ if shortest_path == None:
7810
+ return False
7811
+ else:
7812
+ edges = Topology.Edges(shortest_path)
7813
+ if len(edges) == 1:
7814
+ return True
7815
+ else:
7816
+ return False
7817
+
7385
7818
  @staticmethod
7386
7819
  def IsErdoesGallai(graph, sequence):
7387
7820
  """
@@ -7648,6 +8081,61 @@ class Graph:
7648
8081
  json_string = json.dumps(json_data, indent=indent, sort_keys=sortKeys)
7649
8082
  return json_string
7650
8083
 
8084
+ def Laplacian(graph, silent: bool = False, normalized: bool = False):
8085
+ """
8086
+ Returns the Laplacian matrix of the input graph. See https://en.wikipedia.org/wiki/Laplacian_matrix.
8087
+
8088
+ Parameters
8089
+ ----------
8090
+ graph : topologic_core.Graph
8091
+ The input graph.
8092
+ silent : bool , optional
8093
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
8094
+ normalized : bool , optional
8095
+ If set to True, the returned Laplacian matrix is normalized. The default is False.
8096
+
8097
+ Returns
8098
+ -------
8099
+ list
8100
+ The Laplacian matrix as a nested list.
8101
+ """
8102
+ from topologicpy.Topology import Topology
8103
+ import numpy as np
8104
+
8105
+ if not Topology.IsInstance(graph, "graph"):
8106
+ if not silent:
8107
+ print("Graph.Laplacian - Error: The input graph parameter is not a valid graph. Returning None.")
8108
+ return None
8109
+
8110
+ # Get vertices of the graph
8111
+ vertices = Graph.Vertices(graph)
8112
+ n = len(vertices)
8113
+
8114
+ # Initialize Laplacian matrix
8115
+ laplacian = np.zeros((n, n))
8116
+
8117
+ # Fill Laplacian matrix
8118
+ for i, v1 in enumerate(vertices):
8119
+ for j, v2 in enumerate(vertices):
8120
+ if i == j:
8121
+ laplacian[i][j] = float(Graph.VertexDegree(graph, v1))
8122
+ elif Graph.IsConnected(graph, v1, v2):
8123
+ laplacian[i][j] = -1.0
8124
+ else:
8125
+ laplacian[i][j] = 0.0
8126
+
8127
+ # Normalize the Laplacian if requested
8128
+ if normalized:
8129
+ degree_matrix = np.diag(laplacian.diagonal())
8130
+ with np.errstate(divide='ignore'): # Suppress warnings for division by zero
8131
+ d_inv_sqrt = np.diag(1.0 / np.sqrt(degree_matrix.diagonal()))
8132
+ d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0 # Replace infinities with zero
8133
+
8134
+ normalized_laplacian = d_inv_sqrt @ laplacian @ d_inv_sqrt
8135
+ return normalized_laplacian.tolist()
8136
+
8137
+ return laplacian.tolist()
8138
+
7651
8139
  @staticmethod
7652
8140
  def Leaves(graph, edgeKey: str = None, tolerance: float = 0.0001, silent: bool = False):
7653
8141
  """
@@ -7767,7 +8255,7 @@ class Graph:
7767
8255
  vertices : list , optional
7768
8256
  The input list of vertices. If set to None, the local clustering coefficient of all vertices will be computed. The default is None.
7769
8257
  key : str , optional
7770
- The dictionary key under which to save the local clustering coefficient score. The default is "lcc".
8258
+ The dictionary key under which to store the local clustering coefficient score. The default is "lcc".
7771
8259
  mantissa : int , optional
7772
8260
  The desired length of the mantissa. The default is 6.
7773
8261
  tolerance : float , optional
@@ -8424,11 +8912,11 @@ class Graph:
8424
8912
  graph : topologic_core.Graph
8425
8913
  The input graph.
8426
8914
  xKey : str , optional
8427
- The dictionary key under which to save the X-Coordinate of the vertex. The default is 'x'.
8915
+ The dictionary key under which to store the X-Coordinate of the vertex. The default is 'x'.
8428
8916
  yKey : str , optional
8429
- The dictionary key under which to save the Y-Coordinate of the vertex. The default is 'y'.
8917
+ The dictionary key under which to store the Y-Coordinate of the vertex. The default is 'y'.
8430
8918
  zKey : str , optional
8431
- The dictionary key under which to save the Z-Coordinate of the vertex. The default is 'z'.
8919
+ The dictionary key under which to store the Z-Coordinate of the vertex. The default is 'z'.
8432
8920
  mantissa : int , optional
8433
8921
  The desired length of the mantissa. The default is 6.
8434
8922
  tolerance : float , optional
@@ -8627,7 +9115,7 @@ class Graph:
8627
9115
  directed : bool , optional
8628
9116
  If set to True, the graph is considered as a directed graph. Otherwise, it will be considered as an undirected graph. The default is False.
8629
9117
  key : str , optional
8630
- The dictionary key under which to save the page_rank score. The default is "page_rank"
9118
+ The dictionary key under which to store the page_rank score. The default is "page_rank"
8631
9119
  mantissa : int , optional
8632
9120
  The desired length of the mantissa.
8633
9121
  tolerance : float , optional
@@ -9390,6 +9878,86 @@ class Graph:
9390
9878
  return None
9391
9879
  return len(Graph.Edges(graph))
9392
9880
 
9881
+ @staticmethod
9882
+ def Subgraph(graph, vertices, vertexKey="cutVertex", edgeKey="cutEdge", tolerance=0.0001, silent: bool = False):
9883
+ """
9884
+ Returns a subgraph of the input graph as defined by the input vertices.
9885
+
9886
+ Parameters
9887
+ ----------
9888
+ graph : topologic_core.Graph
9889
+ The input graph.
9890
+ vertexKey : str , optional
9891
+ The dictionary key under which to store the cut vertex status of each vertex. See https://en.wikipedia.org/wiki/Cut_(graph_theory).
9892
+ vertex cuts are indicated with a value of 1. The default is "cutVertex".
9893
+ edgeKey : str , optional
9894
+ The dictionary key under which to store the cut edge status of each edge. See https://en.wikipedia.org/wiki/Cut_(graph_theory).
9895
+ edge cuts are indicated with a value of 1. The default is "cutVertex".
9896
+ tolerance : float , optional
9897
+ The desired tolerance. The default is 0.0001.
9898
+ silent : bool , optional
9899
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
9900
+
9901
+ Returns
9902
+ -------
9903
+ topologic_core.Graph
9904
+ The created subgraph.
9905
+
9906
+ """
9907
+ from topologicpy.Vertex import Vertex
9908
+ from topologicpy.Edge import Edge
9909
+ from topologicpy.Dictionary import Dictionary
9910
+ from topologicpy.Topology import Topology
9911
+
9912
+ if not Topology.IsInstance(graph, "graph"):
9913
+ if not silent:
9914
+ print("Graph.Subgraph - Error: The input graph parameter is not a valid graph. Returning None.")
9915
+ return None
9916
+
9917
+ if not isinstance(vertices, list):
9918
+ if not silent:
9919
+ print("Graph.Subgraph - Error: The input vertices parameter is not a valid list. Returning None.")
9920
+ return None
9921
+
9922
+ vertex_list = [v for v in vertices if Topology.IsInstance(v, "vertex")]
9923
+ if len(vertex_list) < 1:
9924
+ if not silent:
9925
+ print("Graph.Subgraph - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
9926
+ return None
9927
+
9928
+ edges = Graph.Edges(graph, vertices=vertex_list)
9929
+ # Set the vertexCut status to 0 for all input vertices
9930
+ for v in vertex_list:
9931
+ d = Topology.Dictionary(v)
9932
+ d = Dictionary.SetValueAtKey(d, vertexKey, 0)
9933
+ v = Topology.SetDictionary(v, d)
9934
+
9935
+ final_edges = []
9936
+ if not edges == None:
9937
+ for edge in edges:
9938
+ sv = Edge.StartVertex(edge)
9939
+ status_1 = any([Vertex.IsCoincident(sv, v, tolerance=tolerance) for v in vertices])
9940
+ ev = Edge.EndVertex(edge)
9941
+ status_2 = any([Vertex.IsCoincident(ev, v, tolerance=tolerance) for v in vertices])
9942
+ if status_1 and status_2:
9943
+ cutEdge = 0
9944
+ else:
9945
+ cutEdge = 1
9946
+ d = Topology.Dictionary(edge)
9947
+ d = Dictionary.SetValueAtKey(d, edgeKey, cutEdge)
9948
+ edge = Topology.SetDictionary(edge, d)
9949
+ final_edges.append(edge)
9950
+ return_graph = Graph.ByVerticesEdges(vertex_list, final_edges)
9951
+ graph_vertices = Graph.Vertices(return_graph)
9952
+ # Any vertex in the final graph that does not have a vertexCut of 0 is a new vertex and as such needs to have a vertexCut of 1.
9953
+ for v in graph_vertices:
9954
+ d = Topology.Dictionary(v)
9955
+ value = Dictionary.ValueAtKey(d, vertexKey)
9956
+ if not value == 0:
9957
+ d = Dictionary.SetValueAtKey(d, vertexKey, 1)
9958
+ v = Topology.SetDictionary(v, d)
9959
+ return return_graph
9960
+
9393
9961
  @staticmethod
9394
9962
  def _topological_distance(g, start, target):
9395
9963
  from collections import deque
topologicpy/Matrix.py CHANGED
@@ -164,6 +164,69 @@ class Matrix:
164
164
  [0,0,1,0],
165
165
  [translateX,translateY,translateZ,1]]
166
166
 
167
+ @staticmethod
168
+ def EigenvaluesAndVectors(matrix, mantissa: int = 6, silent: bool = False):
169
+ import numpy as np
170
+ """
171
+ Returns the eigenvalues and eigenvectors of the input matrix. See https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors
172
+
173
+ Parameters
174
+ ----------
175
+ matrix : list
176
+ The input matrix. Assumed to be a laplacian matrix.
177
+ mantissa : int , optional
178
+ The desired length of the mantissa. The default is 6.
179
+ silent : bool , optional
180
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
181
+
182
+ Returns
183
+ -------
184
+ list
185
+ The list of eigenvalues and eigenvectors of the input matrix.
186
+
187
+ """
188
+ from topologicpy.Helper import Helper
189
+ import numpy as np
190
+
191
+ if not isinstance(matrix, list):
192
+ if not silent:
193
+ print("Matrix.Eigenvalues - Error: The input matrix parameter is not a valid matrix. Returning None.")
194
+ return None
195
+
196
+ np_matrix = np.array(matrix)
197
+ if not isinstance(np_matrix, np.ndarray):
198
+ if not silent:
199
+ print("Matrix.Eigenvalues - Error: The input matrix parameter is not a valid matrix. Returning None.")
200
+ return None
201
+
202
+ # Square check
203
+ if np_matrix.shape[0] != np_matrix.shape[1]:
204
+ if not silent:
205
+ print("Matrix.Eigenvalues - Error: The input matrix parameter is not a square matrix. Returning None.")
206
+ return None
207
+
208
+ # Symmetry check
209
+ if not np.allclose(np_matrix, np_matrix.T):
210
+ if not silent:
211
+ print("Matrix.Eigenvalues - Error: The input matrix is not symmetric. Returning None.")
212
+ return None
213
+
214
+ # # Degree matrix
215
+ # degree_matrix = np.diag(np_matrix.sum(axis=1))
216
+
217
+ # # Laplacian matrix
218
+ # laplacian_matrix = degree_matrix - np_matrix
219
+
220
+ # Eigenvalues
221
+ eigenvalues, eigenvectors = np.linalg.eig(np_matrix)
222
+
223
+ e_values = [round(x, mantissa) for x in list(np.sort(eigenvalues))]
224
+ e_vectors = []
225
+ for eigenvector in eigenvectors:
226
+ e_vectors.append([round(x, mantissa) for x in eigenvector])
227
+ e_vectors = Helper.Sort(e_vectors, list(eigenvalues))
228
+ return e_values, e_vectors
229
+
167
230
  @staticmethod
168
231
  def Multiply(matA, matB):
169
232
  """
topologicpy/Vertex.py CHANGED
@@ -1710,6 +1710,89 @@ class Vertex():
1710
1710
  pt = project_point_onto_plane(Vertex.Coordinates(vertex), [eq["a"], eq["b"], eq["c"], eq["d"]], direction)
1711
1711
  return Vertex.ByCoordinates(pt[0], pt[1], pt[2])
1712
1712
 
1713
+ @staticmethod
1714
+ def Separate(*vertices, minDistance: float = 0.0001, silent: bool = False):
1715
+ """
1716
+ Separates the input vertices such that no two vertices are within the input minimum distance.
1717
+
1718
+ Parameters
1719
+ ----------
1720
+ vertices : *topologicpy.Vertex
1721
+ One or more instances of a topologic vertex to be processed.
1722
+ minDistance : float , optional
1723
+ The desired minimum distance. The default is 0.0001.
1724
+ silent : bool , optional
1725
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
1726
+
1727
+ Returns
1728
+ -------
1729
+ list
1730
+ The list of vertices with adjusted positions
1731
+
1732
+ """
1733
+ from topologicpy.Topology import Topology
1734
+ from topologicpy.Helper import Helper
1735
+ from topologicpy.Dictionary import Dictionary
1736
+ from math import sqrt
1737
+ from scipy.spatial import KDTree
1738
+ import numpy as np
1739
+
1740
+ if len(vertices) == 0:
1741
+ if not silent:
1742
+ print("Vertex.Separate - Error: The input vertices parameter is an empty list. Returning None.")
1743
+ return None
1744
+ if len(vertices) == 1:
1745
+ vertices = vertices[0]
1746
+ if isinstance(vertices, list):
1747
+ if len(vertices) == 0:
1748
+ if not silent:
1749
+ print("Vertex.Separate - Error: The input vertices parameter is an empty list. Returning None.")
1750
+ return None
1751
+ else:
1752
+ vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
1753
+ if len(vertexList) == 0:
1754
+ if not silent:
1755
+ print("Vertex.Separate - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
1756
+ return None
1757
+ else:
1758
+ if not silent:
1759
+ print("Vertex.Separate - Warning: The input vertices parameter contains only one vertex. Returning the same vertex.")
1760
+ return vertices
1761
+ else:
1762
+ vertexList = Helper.Flatten(list(vertices))
1763
+ vertexList = [x for x in vertexList if Topology.IsInstance(x, "Vertex")]
1764
+ if len(vertexList) == 0:
1765
+ if not silent:
1766
+ print("Vertex.Separate - Error: The input parameters do not contain any valid vertices. Returning None.")
1767
+ return None
1768
+
1769
+ coords = np.array([[v.X(), v.Y(), v.Z()] for v in vertexList]) # Extract coordinates
1770
+ tree = KDTree(coords) # Build k-d tree for efficient neighbor search
1771
+
1772
+ for i, vertex in enumerate(coords):
1773
+ neighbors = tree.query_ball_point(vertex, minDistance)
1774
+ for neighbor_index in neighbors:
1775
+ if neighbor_index != i: # Avoid self-comparison
1776
+ direction = coords[neighbor_index] - vertex
1777
+ distance = np.linalg.norm(direction)
1778
+ if distance < minDistance:
1779
+ # Move current vertex away from its neighbor
1780
+ adjustment = (minDistance - distance) / 2
1781
+ unit_vector = direction / distance if distance != 0 else np.random.rand(3)
1782
+ coords[i] -= unit_vector * adjustment
1783
+ coords[neighbor_index] += unit_vector * adjustment
1784
+
1785
+ # Rebuild the k-d tree after adjustment
1786
+ tree = KDTree(coords)
1787
+
1788
+ # Convert adjusted coordinates back to Vertex objects
1789
+ separated_vertices = [Vertex.ByCoordinates(x, y, z) for x, y, z in coords]
1790
+ for i, vertex in enumerate(vertexList):
1791
+ d = Topology.Dictionary(vertex)
1792
+ if len(Dictionary.Keys(d)) > 0:
1793
+ separated_vertices[i] = Topology.SetDictionary(separated_vertices[i], d)
1794
+ return separated_vertices
1795
+
1713
1796
  @staticmethod
1714
1797
  def X(vertex, mantissa: int = 6) -> float:
1715
1798
  """
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.7.96'
1
+ __version__ = '0.7.98'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: topologicpy
3
- Version: 0.7.96
3
+ Version: 0.7.98
4
4
  Summary: An AI-Powered Spatial Modelling and Analysis Software Library for Architecture, Engineering, and Construction.
5
5
  Author-email: Wassim Jabi <wassim.jabi@gmail.com>
6
6
  License: AGPL v3 License
@@ -11,11 +11,11 @@ topologicpy/Dictionary.py,sha256=t0O7Du-iPq46FyKqZfcjHfsUK1E8GS_e67R2V5cpkbw,331
11
11
  topologicpy/Edge.py,sha256=gaLqyjFOqFHpw69Ftr4rc-kvakYpauQwhOK4ZO-V35g,67287
12
12
  topologicpy/EnergyModel.py,sha256=UoQ9Jm-hYsN383CbcLKw-y6BKitRHj0uyh84yQ-8ACg,53856
13
13
  topologicpy/Face.py,sha256=wczXpMcfub8Eb10lA4rrXksvi5YYCbRjBdp3lOTUwK0,172618
14
- topologicpy/Graph.py,sha256=Zx4dzSTynaJP1dEZ1j5WuneaTv2N2fbeNTyMwaI947c,455395
14
+ topologicpy/Graph.py,sha256=IL7htimFIpRIFSGlNgXqdIkq6KgHFUe9T144yGIk2Qc,479404
15
15
  topologicpy/Grid.py,sha256=2s9cSlWldivn1i9EUz4OOokJyANveqmRe_vR93CAndI,18245
16
16
  topologicpy/Helper.py,sha256=F3h4_qcOD_PHAoVe0tEbEE7_jYyVcaHjtwVs4QHOZuI,23978
17
17
  topologicpy/Honeybee.py,sha256=Y_El6M8x3ixvvIe_VcRiwj_4C89ZZg5_WlT7adbCkpw,21849
18
- topologicpy/Matrix.py,sha256=umgR7An919-wGInXJ1wpqnoQ2jCPdyMe2rcWTZ16upk,8079
18
+ topologicpy/Matrix.py,sha256=tiPum1gTvkKxOyxHBDviH4BwLbdlAusBwMe7ZZfu6Po,10510
19
19
  topologicpy/Neo4j.py,sha256=BKOF29fRgXmdpMGkrNzuYbyqgCJ6ElPPMYlfTxXiVbc,22392
20
20
  topologicpy/Plotly.py,sha256=Tvo0_zKVEHtPhsMNNvLy5G0HIys5FPAOyp_o4QN_I_A,115760
21
21
  topologicpy/Polyskel.py,sha256=EFsuh2EwQJGPLiFUjvtXmAwdX-A4r_DxP5hF7Qd3PaU,19829
@@ -25,12 +25,12 @@ topologicpy/Speckle.py,sha256=AlsGlSDuKRtX5jhVsPNSSjjbZis079HbUchDH_5RJmE,18187
25
25
  topologicpy/Sun.py,sha256=42tDWMYpwRG7Z2Qjtp94eRgBuqySq7k8TgNUZDK7QxQ,36837
26
26
  topologicpy/Topology.py,sha256=kAnJrVyrwJX8c-C4q1cewJ80byG8uaoBWUuk0T6U4SY,441788
27
27
  topologicpy/Vector.py,sha256=Cl7besf20cAGmyNPh-9gbFAHnRU5ZWSMChJ3VyFIDs4,35416
28
- topologicpy/Vertex.py,sha256=QkeNPFTX-adKhEHMole0et9FCy0xXmTHVcmsYqqotSw,73904
28
+ topologicpy/Vertex.py,sha256=tv6C-rbuNgXHDGgVLT5fbalynLdXqlUuiCDKtkeQ0vk,77814
29
29
  topologicpy/Wire.py,sha256=bX8wO96gFa7HZPY0CFlmYQBOUP_1e0jCb02BPxaY-ao,222981
30
30
  topologicpy/__init__.py,sha256=vlPCanUbxe5NifC4pHcnhSzkmmYcs_UrZrTlVMsxcFs,928
31
- topologicpy/version.py,sha256=a9u4mrbOh52xm2kgTG2gE8Y-NjoOtc82edCsxZY1Zs0,23
32
- topologicpy-0.7.96.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
- topologicpy-0.7.96.dist-info/METADATA,sha256=nVJinnZ504srDkDfjHbmyKZWpWEy6TchQp9nfijkKUY,10513
34
- topologicpy-0.7.96.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
35
- topologicpy-0.7.96.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
- topologicpy-0.7.96.dist-info/RECORD,,
31
+ topologicpy/version.py,sha256=edt03iPp-fWm-GyKIDUNJimGnmtCRb6Pjv3dsEvhru4,23
32
+ topologicpy-0.7.98.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
+ topologicpy-0.7.98.dist-info/METADATA,sha256=U6xzVbESx3zzAX4CakRjfE90LNZLqEdPZMN3SKbRBdQ,10513
34
+ topologicpy-0.7.98.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
35
+ topologicpy-0.7.98.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
+ topologicpy-0.7.98.dist-info/RECORD,,