topologicpy 0.7.97__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
@@ -1240,9 +1240,9 @@ class Graph:
1240
1240
  return bot_graph.serialize(format=format)
1241
1241
 
1242
1242
  @staticmethod
1243
- 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):
1244
1244
  """
1245
- 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.
1246
1246
 
1247
1247
  Parameters
1248
1248
  ----------
@@ -1250,6 +1250,8 @@ class Graph:
1250
1250
  The input graph.
1251
1251
  key : str , optional
1252
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".
1253
1255
  mantissa : int , optional
1254
1256
  The desired length of the mantissa. The default is 6.
1255
1257
  tolerance : float , optional
@@ -1276,7 +1278,7 @@ class Graph:
1276
1278
  shortest_paths[path[-1]].append(path)
1277
1279
  return shortest_paths
1278
1280
 
1279
- def calculate_betweenness():
1281
+ def calculate_vertex_betweenness():
1280
1282
  betweenness = {v: 0.0 for v in py_graph}
1281
1283
  for s in py_graph:
1282
1284
  shortest_paths = shortest_paths_count(s)
@@ -1291,6 +1293,56 @@ class Graph:
1291
1293
  betweenness[v] += dependency[v]
1292
1294
  return betweenness
1293
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
+
1294
1346
  from topologicpy.Topology import Topology
1295
1347
  from topologicpy.Dictionary import Dictionary
1296
1348
 
@@ -1299,38 +1351,75 @@ class Graph:
1299
1351
  print("Graph.BetweenessCentrality - Error: The input graph is not a valid graph. Returning None.")
1300
1352
  return None
1301
1353
 
1302
- vertices = Graph.Vertices(graph)
1303
-
1304
- if len(vertices) < 1:
1305
- if not silent:
1306
- print("Graph.BetweenessCentrality - Error: The input graph does not contain valid vertices. Returning None.")
1307
- return None
1308
- if len(vertices) == 1:
1309
- d = Topology.Dictionary(vertices[0])
1310
- d = Dictionary.SetValueAtKey(d, key, 1.0)
1311
- vertices[0] = Topology.SetDictionary(vertices[0], d)
1312
- return [1.0]
1313
-
1314
- py_graph = Graph.AdjacencyDictionary(graph)
1315
- betweenness = calculate_betweenness()
1316
- for v in betweenness:
1317
- betweenness[v] /= 2.0 # Each shortest path is counted twice
1354
+ if "v" in method.lower():
1355
+ vertices = Graph.Vertices(graph)
1318
1356
 
1319
- max_betweenness = max(betweenness.values())
1320
- if max_betweenness > 0:
1321
- for v in betweenness:
1322
- betweenness[v] /= max_betweenness # Normalize to [0, 1]
1323
-
1324
-
1325
- return_betweenness = [0]*len(vertices)
1326
- for i, v in betweenness.items():
1327
- vertex = vertices[int(i)]
1328
- d = Topology.Dictionary(vertex)
1329
- d = Dictionary.SetValueAtKey(d, key, round(v, mantissa))
1330
- vertex = Topology.SetDictionary(vertex, d)
1331
- return_betweenness[int(i)] = v
1332
-
1333
- return return_betweenness
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
1334
1423
 
1335
1424
  @staticmethod
1336
1425
  def Bridges(graph, key: str = "bridge", silent: bool = False):
@@ -1345,59 +1434,91 @@ class Graph:
1345
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".
1346
1435
  silent : bool , optional
1347
1436
  If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
1348
-
1437
+
1349
1438
  Returns
1350
1439
  -------
1351
1440
  list
1352
1441
  The list of bridge edges in the input graph.
1353
-
1354
1442
  """
1355
- import os
1356
- import warnings
1357
1443
  from topologicpy.Topology import Topology
1358
1444
  from topologicpy.Dictionary import Dictionary
1445
+ from topologicpy.Graph import Graph
1359
1446
 
1360
- try:
1361
- import igraph as ig
1362
- except:
1363
- print("Graph.Bridges - Installing required pyhon-igraph library.")
1364
- try:
1365
- os.system("pip install python-igraph")
1366
- except:
1367
- os.system("pip install python-igraph --user")
1368
- try:
1369
- import igraph as ig
1370
- print("Graph.Bridges - python-igraph library installed correctly.")
1371
- except:
1372
- warnings.warn("Graph.Bridges - Error: Could not import python-igraph. Please install manually.")
1373
-
1374
1447
  if not Topology.IsInstance(graph, "graph"):
1375
1448
  if not silent:
1376
1449
  print("Graph.Bridges - Error: The input graph parameter is not a valid topologic graph. Returning None")
1377
1450
  return None
1378
1451
 
1379
- edges = Graph.Edges(graph)
1380
- if len(edges) < 1:
1381
- return []
1382
- if len(edges) == 1:
1452
+ graph_edges = Graph.Edges(graph)
1453
+ for edge in graph_edges:
1383
1454
  d = Topology.Dictionary(edge)
1384
- d = Dictionary.SetValueAtKey(d, key, 1)
1455
+ d = Dictionary.SetValueAtKey(d, key, 0)
1385
1456
  edge = Topology.SetDictionary(edge, d)
1386
- return [edge]
1387
1457
  mesh_data = Graph.MeshData(graph)
1388
- graph_edges = mesh_data['edges']
1389
- ig_graph = ig.Graph(edges=graph_edges)
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)])
1390
1482
 
1391
- bridges = ig_graph.bridges()
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
1392
1507
  bridge_edges = []
1393
- for i, edge in enumerate(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:
1394
1518
  d = Topology.Dictionary(edge)
1395
- if i in bridges:
1396
- d = Dictionary.SetValueAtKey(d, key, 1)
1397
- bridge_edges.append(edge)
1398
- else:
1399
- d = Dictionary.SetValueAtKey(d, key, 0)
1519
+ d = Dictionary.SetValueAtKey(d, key, 1)
1400
1520
  edge = Topology.SetDictionary(edge, d)
1521
+
1401
1522
  return bridge_edges
1402
1523
 
1403
1524
  @staticmethod
@@ -1486,7 +1607,8 @@ class Graph:
1486
1607
  for i in range(len(adjacencyMatrix)):
1487
1608
  x, y, z = random.uniform(xMin,xMax), random.uniform(yMin,yMax), random.uniform(zMin,zMax)
1488
1609
  v = Vertex.ByCoordinates(x, y, z)
1489
- v = Topology.SetDictionary(v, dictionaries[i])
1610
+ if isinstance(dictionaries, list):
1611
+ v = Topology.SetDictionary(v, dictionaries[i])
1490
1612
  vertices.append(v)
1491
1613
 
1492
1614
  # Create the graph using vertices and edges
@@ -4153,6 +4275,82 @@ class Graph:
4153
4275
  v = Topology.SetDictionary(v, d)
4154
4276
  return graph
4155
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
+
4156
4354
  @staticmethod
4157
4355
  def Complete(graph, silent: bool = False):
4158
4356
  """
@@ -4793,6 +4991,28 @@ class Graph:
4793
4991
  scores.append(degree)
4794
4992
  return scores
4795
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
+
4796
5016
  @staticmethod
4797
5017
  def DegreeSequence(graph):
4798
5018
  """
@@ -6161,6 +6381,37 @@ class Graph:
6161
6381
  return False
6162
6382
  return False
6163
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
+
6164
6415
  @staticmethod
6165
6416
  def IsIsomorphic(graphA, graphB, maxIterations=10, silent=False):
6166
6417
  """
@@ -7511,6 +7762,59 @@ class Graph:
7511
7762
  return None
7512
7763
  return graph.IsComplete()
7513
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
+
7514
7818
  @staticmethod
7515
7819
  def IsErdoesGallai(graph, sequence):
7516
7820
  """
@@ -7777,6 +8081,61 @@ class Graph:
7777
8081
  json_string = json.dumps(json_data, indent=indent, sort_keys=sortKeys)
7778
8082
  return json_string
7779
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
+
7780
8139
  @staticmethod
7781
8140
  def Leaves(graph, edgeKey: str = None, tolerance: float = 0.0001, silent: bool = False):
7782
8141
  """
@@ -9519,6 +9878,86 @@ class Graph:
9519
9878
  return None
9520
9879
  return len(Graph.Edges(graph))
9521
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
+
9522
9961
  @staticmethod
9523
9962
  def _topological_distance(g, start, target):
9524
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.97'
1
+ __version__ = '0.7.98'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: topologicpy
3
- Version: 0.7.97
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=tAm-kvvHAgadPJIcXvqgEoMuAdAo3RS3mu9uhhgx7TI,460497
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=eF2oD9YC4qAbKpy4BgX03mi3PRuOcHb_V4K8lN5jAn0,23
32
- topologicpy-0.7.97.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
- topologicpy-0.7.97.dist-info/METADATA,sha256=pingqvbKxBUf-eX6CrZQCoIyEV1tduXqvZwpol4DDuQ,10513
34
- topologicpy-0.7.97.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
35
- topologicpy-0.7.97.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
- topologicpy-0.7.97.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,,