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 +709 -141
- topologicpy/Matrix.py +63 -0
- topologicpy/Vertex.py +83 -0
- topologicpy/version.py +1 -1
- {topologicpy-0.7.96.dist-info → topologicpy-0.7.98.dist-info}/METADATA +1 -1
- {topologicpy-0.7.96.dist-info → topologicpy-0.7.98.dist-info}/RECORD +9 -9
- {topologicpy-0.7.96.dist-info → topologicpy-0.7.98.dist-info}/LICENSE +0 -0
- {topologicpy-0.7.96.dist-info → topologicpy-0.7.98.dist-info}/WHEEL +0 -0
- {topologicpy-0.7.96.dist-info → topologicpy-0.7.98.dist-info}/top_level.txt +0 -0
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
|
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
|
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
|
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
|
-
|
1354
|
+
if "v" in method.lower():
|
1355
|
+
vertices = Graph.Vertices(graph)
|
1399
1356
|
|
1400
|
-
|
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.
|
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
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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.
|
1
|
+
__version__ = '0.7.98'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: topologicpy
|
3
|
-
Version: 0.7.
|
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=
|
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=
|
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=
|
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=
|
32
|
-
topologicpy-0.7.
|
33
|
-
topologicpy-0.7.
|
34
|
-
topologicpy-0.7.
|
35
|
-
topologicpy-0.7.
|
36
|
-
topologicpy-0.7.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|