topologicpy 0.7.87__py3-none-any.whl → 0.7.91__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
@@ -1328,7 +1328,113 @@ class Graph:
1328
1328
  return bot_graph.serialize(format=format)
1329
1329
 
1330
1330
  @staticmethod
1331
- def BetweenessCentrality(graph, vertices=None, sources=None, destinations=None, key: str = "betweeness_centrality", mantissa: int = 6, tolerance: float = 0.001):
1331
+ def BetweenessCentrality(graph, key: str = "betweeness_centrality", mantissa: int = 6, tolerance: float = 0.001, silent: bool = False):
1332
+ """
1333
+ Returns the betweeness 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.
1334
+
1335
+ Parameters
1336
+ ----------
1337
+ graph : topologic_core.Graph
1338
+ The input graph.
1339
+ key : str , optional
1340
+ The dictionary key under which to save the betweeness centrality score. The default is "betweneess_centrality".
1341
+ mantissa : int , optional
1342
+ The desired length of the mantissa. The default is 6.
1343
+ tolerance : float , optional
1344
+ The desired tolerance. The default is 0.0001.
1345
+
1346
+ Returns
1347
+ -------
1348
+ list
1349
+ The betweeness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
1350
+
1351
+ """
1352
+ def vertex_betweenness_centrality(graph, vertices):
1353
+ """
1354
+ Compute the betweenness centrality for vertices in the given TopologicPy graph.
1355
+
1356
+ Args:
1357
+ graph: The TopologicPy Graph object.
1358
+
1359
+ Returns:
1360
+ dict: A dictionary mapping each vertex to its betweenness centrality value.
1361
+ """
1362
+ from collections import defaultdict
1363
+ n = len(vertices)
1364
+ idList = []
1365
+ vertex_map = {}
1366
+ for i, v in enumerate(vertices):
1367
+ d = Topology.Dictionary(v)
1368
+ d = Dictionary.SetValueAtKey(d, "__id_", str(i))
1369
+ v = Topology.SetDictionary(v, d)
1370
+ idList.append(str(i))
1371
+ vertex_map[str(i)] = v
1372
+ if n < 2:
1373
+ return {v: 0.0 for v in idList}
1374
+ centrality = defaultdict(float)
1375
+
1376
+ for source in idList:
1377
+ stack, paths, sigma = [], {}, {v: 0.0 for v in idList}
1378
+ sigma[source] = 1.0
1379
+ paths[source] = []
1380
+
1381
+ queue = [source]
1382
+ while queue:
1383
+ v = queue.pop(0)
1384
+ stack.append(v)
1385
+ vertex = vertex_map[v]
1386
+ for neighbor in Graph.AdjacentVertices(graph, vertex):
1387
+ d = Topology.Dictionary(neighbor)
1388
+ neighbor_id = Dictionary.ValueAtKey(d, "__id_")
1389
+ if neighbor_id not in paths:
1390
+ queue.append(neighbor_id)
1391
+ paths[neighbor_id] = [v]
1392
+ elif v not in paths[neighbor_id]:
1393
+ paths[neighbor_id].append(v)
1394
+ sigma[neighbor_id] += sigma[v]
1395
+
1396
+ delta = {v: 0.0 for v in idList}
1397
+ while stack:
1398
+ w = stack.pop()
1399
+ for v in paths.get(w, []):
1400
+ delta[v] += (sigma[v] / sigma[w]) * (1 + delta[w])
1401
+ if w != source:
1402
+ centrality[w] += delta[w]
1403
+ # Normalize centrality values
1404
+ max_centrality = max([centrality[v] for v in idList])
1405
+ min_centrality = min([centrality[v] for v in idList])
1406
+ centrality = [round((centrality[v]-min_centrality)/max_centrality, mantissa) for v in idList]
1407
+ for i, v in enumerate(vertices):
1408
+ d = Topology.Dictionary(v)
1409
+ d = Dictionary.SetValueAtKey(d, "betweeness_centrality", centrality[i])
1410
+ d = Dictionary.RemoveKey(d, "__id_")
1411
+ v = Topology.SetDictionary(v, d)
1412
+ return centrality
1413
+
1414
+ from topologicpy.Topology import Topology
1415
+ from topologicpy.Dictionary import Dictionary
1416
+
1417
+ if not Topology.IsInstance(graph, "Graph"):
1418
+ if not silent:
1419
+ print("Graph.BetweenessCentrality - Error: The input graph is not a valid graph. Returning None.")
1420
+ return None
1421
+
1422
+ vertices = Graph.Vertices(graph)
1423
+
1424
+ if len(vertices) < 1:
1425
+ if not silent:
1426
+ print("Graph.BetweenessCentrality - Error: The input graph does not contain valid vertices. Returning None.")
1427
+ return None
1428
+ if len(vertices) == 1:
1429
+ d = Topology.Dictionary(vertices[0])
1430
+ d = Dictionary.SetValueAtKey(d, key, 1.0)
1431
+ vertices[0] = Topology.SetDictionary(vertices[0], d)
1432
+ return [1.0]
1433
+
1434
+ return vertex_betweenness_centrality(graph, vertices)
1435
+
1436
+ @staticmethod
1437
+ def BetweenessCentrality_old(graph, vertices=None, sources=None, destinations=None, key: str = "betweeness_centrality", mantissa: int = 6, tolerance: float = 0.001):
1332
1438
  """
1333
1439
  Returns the betweeness 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.
1334
1440
 
@@ -2160,9 +2266,14 @@ class Graph:
2160
2266
  return {'graphs':graphs, 'labels':labels}
2161
2267
 
2162
2268
  @staticmethod
2163
- def ByIFCFile(file, includeTypes: list = [], excludeTypes: list = [],
2164
- includeRels: list = [], excludeRels: list = [],
2165
- transferDictionaries: bool = False, storeBREP: bool = False,
2269
+ def ByIFCFile(file,
2270
+ includeTypes: list = [],
2271
+ excludeTypes: list = [],
2272
+ includeRels: list = [],
2273
+ excludeRels: list = [],
2274
+ transferDictionaries: bool = False,
2275
+ useInternalVertex: bool = False,
2276
+ storeBREP: bool = False,
2166
2277
  removeCoplanarFaces: bool = False,
2167
2278
  xMin: float = -0.5, yMin: float = -0.5, zMin: float = -0.5,
2168
2279
  xMax: float = 0.5, yMax: float = 0.5, zMax: float = 0.5,
@@ -2184,6 +2295,8 @@ class Graph:
2184
2295
  A list of IFC relationship types to exclude from the graph. The default is [] which mean no relationship type is excluded.
2185
2296
  transferDictionaries : bool , optional
2186
2297
  If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. The default is False.
2298
+ useInternalVertex : bool , optional
2299
+ If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. The default is False.
2187
2300
  storeBREP : bool , optional
2188
2301
  If set to True, store the BRep of the subtopology in its representative vertex. The default is False.
2189
2302
  removeCoplanarFaces : bool , optional
@@ -2466,7 +2579,7 @@ class Graph:
2466
2579
  pset_python_dict = get_psets(ifc_object)
2467
2580
  pset_dict = Dictionary.ByPythonDictionary(pset_python_dict)
2468
2581
  topology_dict = Dictionary.ByMergedDictionaries([topology_dict, pset_dict])
2469
- if storeBREP == True:
2582
+ if storeBREP == True or useInternalVertex == True:
2470
2583
  shape_topology = None
2471
2584
  if hasattr(ifc_object, "Representation") and ifc_object.Representation:
2472
2585
  for rep in ifc_object.Representation.Representations:
@@ -2484,8 +2597,10 @@ class Graph:
2484
2597
  if not shape_topology == None:
2485
2598
  if removeCoplanarFaces == True:
2486
2599
  shape_topology = Topology.RemoveCoplanarFaces(shape_topology, epsilon=0.0001)
2487
- if not shape_topology == None:
2600
+ if not shape_topology == None and storeBREP:
2488
2601
  topology_dict = Dictionary.SetValuesAtKeys(topology_dict, ["brep", "brepType", "brepTypeString"], [Topology.BREPString(shape_topology), Topology.Type(shape_topology), Topology.TypeAsString(shape_topology)])
2602
+ if not shape_topology == None and useInternalVertex == True:
2603
+ centroid = Topology.InternalVertex(shape_topology)
2489
2604
  centroid = Topology.SetDictionary(centroid, topology_dict)
2490
2605
  return centroid
2491
2606
  return None
@@ -2571,7 +2686,16 @@ class Graph:
2571
2686
  return g
2572
2687
 
2573
2688
  @staticmethod
2574
- def ByIFCPath(path, includeTypes=[], excludeTypes=[], includeRels=[], excludeRels=[], transferDictionaries=False, storeBREP=False, removeCoplanarFaces=False, xMin=-0.5, yMin=-0.5, zMin=-0.5, xMax=0.5, yMax=0.5, zMax=0.5):
2689
+ def ByIFCPath(path,
2690
+ includeTypes=[],
2691
+ excludeTypes=[],
2692
+ includeRels=[],
2693
+ excludeRels=[],
2694
+ transferDictionaries=False,
2695
+ useInternalVertex=False,
2696
+ storeBREP=False,
2697
+ removeCoplanarFaces=False,
2698
+ xMin=-0.5, yMin=-0.5, zMin=-0.5, xMax=0.5, yMax=0.5, zMax=0.5):
2575
2699
  """
2576
2700
  Create a Graph from an IFC path. This code is partially based on code from Bruno Postle.
2577
2701
 
@@ -2589,6 +2713,8 @@ class Graph:
2589
2713
  A list of IFC relationship types to exclude from the graph. The default is [] which mean no relationship type is excluded.
2590
2714
  transferDictionaries : bool , optional
2591
2715
  If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. The default is False.
2716
+ useInternalVertex : bool , optional
2717
+ If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. The default is False.
2592
2718
  storeBREP : bool , optional
2593
2719
  If set to True, store the BRep of the subtopology in its representative vertex. The default is False.
2594
2720
  removeCoplanarFaces : bool , optional
@@ -2647,6 +2773,7 @@ class Graph:
2647
2773
  includeRels=includeRels,
2648
2774
  excludeRels=excludeRels,
2649
2775
  transferDictionaries=transferDictionaries,
2776
+ useInternalVertex=useInternalVertex,
2650
2777
  storeBREP=storeBREP,
2651
2778
  removeCoplanarFaces=removeCoplanarFaces,
2652
2779
  xMin=xMin, yMin=yMin, zMin=zMin, xMax=xMax, yMax=yMax, zMax=zMax)
@@ -2708,7 +2835,82 @@ class Graph:
2708
2835
  g_e = Topology.SetDictionary(g_e, d)
2709
2836
  g_edges.append(g_e)
2710
2837
  return Graph.ByVerticesEdges(g_vertices, g_edges)
2711
-
2838
+
2839
+ @staticmethod
2840
+ def ByNetworkXGraph(nxGraph, xKey="x", yKey="y", zKey="z", range=(-1, 1), mantissa: int = 6, tolerance: float = 0.0001):
2841
+ """
2842
+ Converts the input NetworkX graph into a topologic Graph. See http://networkx.org
2843
+
2844
+ Parameters
2845
+ ----------
2846
+ nxGraph : NetworkX graph
2847
+ The input NetworkX graph.
2848
+ xKey : str , optional
2849
+ The dictionary key under which to find the X-Coordinate of the vertex. The default is 'x'.
2850
+ yKey : str , optional
2851
+ The dictionary key under which to find the Y-Coordinate of the vertex. The default is 'y'.
2852
+ zKey : str , optional
2853
+ The dictionary key under which to find the Z-Coordinate of the vertex. The default is 'z'.
2854
+ range : tuple , optional
2855
+ The range to use for position coordinates if no values are found in the dictionaries. The default is (-1,1)
2856
+ mantissa : int , optional
2857
+ The desired length of the mantissa. The default is 6.
2858
+ tolerance : float , optional
2859
+ The desired tolerance. The default is 0.0001.
2860
+
2861
+ Returns
2862
+ -------
2863
+ topologicpy.Graph
2864
+ The created topologic graph.
2865
+
2866
+ """
2867
+ from topologicpy.Vertex import Vertex
2868
+ from topologicpy.Edge import Edge
2869
+ from topologicpy.Topology import Topology
2870
+ from topologicpy.Dictionary import Dictionary
2871
+
2872
+ import random
2873
+ import numpy as np
2874
+
2875
+ # Create a mapping from NetworkX nodes to TopologicPy vertices
2876
+ nx_to_topologic_vertex = {}
2877
+
2878
+ # Create TopologicPy vertices for each node in the NetworkX graph
2879
+ vertices = []
2880
+ for node, data in nxGraph.nodes(data=True):
2881
+ # Attempt to get X, Y, Z from the node data
2882
+ x = round(data.get(xKey, random.uniform(*range)), mantissa)
2883
+ y = round(data.get(yKey, random.uniform(*range)), mantissa)
2884
+ z = round(data.get(zKey, 0), mantissa) # If there are no Z values, this is probably a flat graph.
2885
+ # Create a TopologicPy vertex with the node data dictionary
2886
+ vertex = Vertex.ByCoordinates(x,y,z)
2887
+ cleaned_values = []
2888
+ for value in data.values():
2889
+ if isinstance(value, np.ndarray):
2890
+ value = list(value)
2891
+ cleaned_values.append(value)
2892
+
2893
+ node_dict = Dictionary.ByKeysValues(list(data.keys()), cleaned_values)
2894
+ vertex = Topology.SetDictionary(vertex, node_dict)
2895
+ nx_to_topologic_vertex[node] = vertex
2896
+ vertices.append(vertex)
2897
+
2898
+ # Create TopologicPy edges for each edge in the NetworkX graph
2899
+ edges = []
2900
+ for u, v, data in nxGraph.edges(data=True):
2901
+ start_vertex = nx_to_topologic_vertex[u]
2902
+ end_vertex = nx_to_topologic_vertex[v]
2903
+
2904
+ # Create a TopologicPy edge with the edge data dictionary
2905
+ edge_dict = Dictionary.ByKeysValues(list(data.keys()), list(data.values()))
2906
+ edge = Edge.ByVertices([start_vertex, end_vertex], tolerance=tolerance)
2907
+ edge = Topology.SetDictionary(edge, edge_dict)
2908
+ edges.append(edge)
2909
+
2910
+ # Create and return the TopologicPy graph
2911
+ topologic_graph = Graph.ByVerticesEdges(vertices, edges)
2912
+ return topologic_graph
2913
+
2712
2914
  @staticmethod
2713
2915
  def ByTopology(topology,
2714
2916
  direct: bool = True,
@@ -4081,6 +4283,121 @@ class Graph:
4081
4283
  v = Topology.SetDictionary(v, d)
4082
4284
  return graph
4083
4285
 
4286
+ @staticmethod
4287
+ def Complete(graph, silent: bool = False):
4288
+ """
4289
+ Completes the graph by conneting unconnected vertices.
4290
+
4291
+ Parameters
4292
+ ----------
4293
+ graph : topologic_core.Graph
4294
+ The input graph.
4295
+ tolerance : float , optional
4296
+ The desired tolerance. The default is 0.0001.
4297
+ silent : bool , optional
4298
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4299
+
4300
+ Returns
4301
+ -------
4302
+ topologicpy.Graph
4303
+ the completed graph
4304
+ """
4305
+ from topologicpy.Edge import Edge
4306
+ from topologicpy.Topology import Topology
4307
+
4308
+ if not Topology.IsInstance(graph, "Graph"):
4309
+ if not silent:
4310
+ print("Graph.ConnectedComponents - Error: The input graph is not a valid graph. Returning None.")
4311
+ return None
4312
+
4313
+ vertices = Graph.Vertices(graph)
4314
+ edges = Graph.Edges(graph)
4315
+ visited = set()
4316
+ new_edges = []
4317
+ for sv in vertices:
4318
+ for ev in vertices:
4319
+ if sv != ev and not (sv, ev) in visited:
4320
+ visited.add((sv, ev))
4321
+ visited.add((ev,sv))
4322
+ edge = Graph.Edge(graph, sv, ev)
4323
+ if edge == None:
4324
+ new_edges.append(Edge.ByVertices(sv, ev))
4325
+ edges += new_edges
4326
+ return Graph.ByVerticesEdges(vertices, edges)
4327
+
4328
+ @staticmethod
4329
+ def ConnectedComponents(graph, tolerance: float = 0.0001, silent: bool = False):
4330
+ """
4331
+ Returns the connected components (islands) of the input graph.
4332
+
4333
+ Parameters
4334
+ ----------
4335
+ graph : topologic_core.Graph
4336
+ The input graph.
4337
+ tolerance : float , optional
4338
+ The desired tolerance. The default is 0.0001.
4339
+ silent : bool , optional
4340
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4341
+
4342
+ Returns
4343
+ -------
4344
+ list
4345
+ The list of connected components (island graphs).
4346
+ The list is sorted by the number of vertices in each component (from highest to lowest).
4347
+
4348
+ """
4349
+ def find_connected_components(adjacency_dict):
4350
+ visited = set()
4351
+ components = []
4352
+
4353
+ for vertex_id in adjacency_dict:
4354
+ if vertex_id not in visited:
4355
+ # Perform DFS using a stack
4356
+ stack = [vertex_id]
4357
+ current_island = set()
4358
+
4359
+ while stack:
4360
+ current = stack.pop()
4361
+ if current not in visited:
4362
+ visited.add(current)
4363
+ current_island.add(current)
4364
+ stack.extend(set(adjacency_dict[current]) - visited)
4365
+
4366
+ components.append(current_island)
4367
+
4368
+ return components
4369
+
4370
+ from topologicpy.Topology import Topology
4371
+ from topologicpy.Dictionary import Dictionary
4372
+ from topologicpy.Helper import Helper
4373
+
4374
+ if not Topology.IsInstance(graph, "Graph"):
4375
+ if not silent:
4376
+ print("Graph.ConnectedComponents - Error: The input graph is not a valid graph. Returning None.")
4377
+ return None
4378
+
4379
+ labelKey = "__label__"
4380
+ lengths = [] #List of lengths to sort the list of components by number of their vertices
4381
+ vertices = Graph.Vertices(graph)
4382
+ g_dict = Graph.AdjacencyDictionary(graph, vertexLabelKey=labelKey)
4383
+ components = find_connected_components(g_dict)
4384
+ return_components = []
4385
+ for component in components:
4386
+ i_verts = []
4387
+ for v in component:
4388
+ vert = Topology.Filter(vertices, searchType="equal to", key=labelKey, value=v)['filtered'][0]
4389
+ d = Topology.Dictionary(vert)
4390
+ d = Dictionary.RemoveKey(d, labelKey)
4391
+ vert = Topology.SetDictionary(vert, d)
4392
+ i_verts.append(vert)
4393
+ i_edges = Graph.Edges(graph, i_verts)
4394
+ lengths.append(len(i_verts))
4395
+ g_component = Graph.ByVerticesEdges(i_verts, i_edges)
4396
+ return_components.append(g_component)
4397
+ return_components = Helper.Sort(return_components, lengths)
4398
+ return_components.reverse()
4399
+ return return_components
4400
+
4084
4401
  @staticmethod
4085
4402
  def ContractEdge(graph, edge, vertex=None, tolerance=0.0001):
4086
4403
  """
@@ -4184,7 +4501,7 @@ class Graph:
4184
4501
  return graph
4185
4502
 
4186
4503
  @staticmethod
4187
- def ClosenessCentrality(graph, vertices=None, key: str = "closeness_centrality", mantissa: int = 6, tolerance = 0.0001):
4504
+ def ClosenessCentrality(graph, vertices=None, key: str = "closeness_centrality", mantissa: int = 6, tolerance = 0.0001, silent = False):
4188
4505
  """
4189
4506
  Return the closeness 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 closeness centrality of all the vertices in the input graph is computed. See https://en.wikipedia.org/wiki/Closeness_centrality.
4190
4507
 
@@ -4200,6 +4517,8 @@ class Graph:
4200
4517
  The desired length of the mantissa. The default is 6.
4201
4518
  tolerance : float , optional
4202
4519
  The desired tolerance. The default is 0.0001.
4520
+ silent : bool , optional
4521
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4203
4522
 
4204
4523
  Returns
4205
4524
  -------
@@ -4207,56 +4526,130 @@ class Graph:
4207
4526
  The closeness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
4208
4527
 
4209
4528
  """
4529
+
4530
+ def closeness_centrality(g):
4531
+ """
4532
+ Computes the closeness centrality for each vertex in the graph.
4533
+
4534
+ Parameters:
4535
+ graph (dict): A dictionary representing the graph where keys are vertices and
4536
+ values are lists of neighboring vertices.
4537
+
4538
+ Returns:
4539
+ dict: A dictionary where keys are vertices and values are their closeness centrality.
4540
+ """
4541
+ keys = list(g.keys())
4542
+ N = len(keys)
4543
+
4544
+ centralities = []
4545
+ for v in keys:
4546
+ total_distance = 0
4547
+ reachable_count = 0
4548
+
4549
+ for u in keys:
4550
+ if v != u:
4551
+ distance = Graph._topological_distance(g, v, u)
4552
+ if distance != None:
4553
+ total_distance += distance
4554
+ reachable_count += 1
4555
+
4556
+ if reachable_count > 0: # Avoid division by zero
4557
+ centrality = (reachable_count / total_distance)
4558
+ else:
4559
+ centrality = 0.0 # Isolated vertex
4560
+
4561
+ centralities.append(centrality)
4562
+ return centralities
4563
+
4564
+ from topologicpy.Vertex import Vertex
4210
4565
  from topologicpy.Topology import Topology
4211
4566
  from topologicpy.Dictionary import Dictionary
4567
+ from topologicpy.Helper import Helper
4212
4568
 
4213
4569
  if not Topology.IsInstance(graph, "Graph"):
4214
- print("Graph.ClosenessCentrality - Error: The input graph is not a valid graph. Returning None.")
4570
+ if not silent:
4571
+ print("Graph.ClosenessCentrality - Error: The input graph is not a valid graph. Returning None.")
4215
4572
  return None
4573
+ g = Graph.AdjacencyDictionary(graph)
4574
+ centralities = closeness_centrality(g)
4216
4575
  graphVertices = Graph.Vertices(graph)
4217
- if not isinstance(vertices, list):
4218
- vertices = graphVertices
4576
+ if vertices == None:
4577
+ for i, v in enumerate(graphVertices):
4578
+ d = Topology.Dictionary(v)
4579
+ d = Dictionary.SetValueAtKey(d, key, centralities[i])
4580
+ v = Topology.SetDictionary(v, d)
4581
+ return centralities
4219
4582
  else:
4220
- vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
4221
- if len(vertices) < 1:
4222
- print("Graph.ClosenessCentrality - Error: The input list of vertices does not contain any valid vertices. Returning None.")
4223
- return None
4224
- n = len(graphVertices)
4583
+ return_centralities = []
4584
+ for v in vertices:
4585
+ i = Vertex.Index(v, graphVertices)
4586
+ d = Topology.Dictionary(v)
4587
+ d = Dictionary.SetValueAtKey(d, key, centralities[i])
4588
+ v = Topology.SetDictionary(v, d)
4589
+ return_centralities.append(centralities[i])
4590
+ return centralities
4225
4591
 
4226
- scores = []
4592
+ @staticmethod
4593
+ def Community(graph, key: str = "community", mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
4594
+ """
4595
+ Computes the best community partition of the input graph based on the Louvain method. See https://en.wikipedia.org/wiki/Louvain_method.
4596
+ This method depends on NetworkX and the python-louvain libraries
4597
+
4598
+ Parameters
4599
+ ----------
4600
+ graph : topologicp.Graph
4601
+ The input topologic graph.
4602
+ key : str , optional
4603
+ The dictionary key under which to save the closeness centrality score. The default is "community".
4604
+ mantissa : int , optional
4605
+ The desired length of the mantissa. The default is 6.
4606
+ tolerance : float , optional
4607
+ The desired tolerance. The default is 0.0001.
4608
+ silent : bool , optional
4609
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4610
+ Returns
4611
+ -------
4612
+ topologicpy.Graph
4613
+ The created topologic graph.
4614
+
4615
+ """
4616
+ from topologicpy.Topology import Topology
4617
+ from topologicpy.Dictionary import Dictionary
4618
+ import os
4619
+ import warnings
4620
+
4227
4621
  try:
4228
- for va in tqdm(vertices, desc="Computing Closeness Centrality", leave=False):
4229
- top_dist = 0
4230
- for vb in graphVertices:
4231
- if Topology.IsSame(va, vb):
4232
- d = 0
4233
- else:
4234
- d = Graph.TopologicalDistance(graph, va, vb, tolerance=tolerance)
4235
- top_dist += d
4236
- if top_dist == 0:
4237
- print("Graph.ClosenessCentrality - Warning: Topological Distance is Zero.")
4238
- scores.append(0)
4239
- else:
4240
- scores.append(round((n-1)/top_dist, mantissa))
4622
+ import community as community_louvain
4241
4623
  except:
4242
- print("Graph.ClosenessCentrality - Warning: Could not use tqdm.")
4243
- for va in vertices:
4244
- top_dist = 0
4245
- for vb in graphVertices:
4246
- if Topology.IsSame(va, vb):
4247
- d = 0
4248
- else:
4249
- d = Graph.TopologicalDistance(graph, va, vb, tolerance=tolerance)
4250
- top_dist += d
4251
- if top_dist == 0:
4252
- scores.append(0)
4253
- else:
4254
- scores.append(round((n-1)/top_dist, mantissa))
4255
- for i, v in enumerate(vertices):
4256
- d = Topology.Dictionary(v)
4257
- d = Dictionary.SetValueAtKey(d, key, scores[i])
4258
- v = Topology.SetDictionary(v, d)
4259
- return scores
4624
+ print("Graph.Community - Installing required pyhon-louvain library.")
4625
+ try:
4626
+ os.system("pip install python-louvain")
4627
+ except:
4628
+ os.system("pip install python-louvain --user")
4629
+ try:
4630
+ import community as community_louvain
4631
+ print("Graph.Community - python-louvain library installed correctly.")
4632
+ except:
4633
+ warnings.warn("Graph.Community - Error: Could not import python-louvain. Please install manually.")
4634
+
4635
+ if not Topology.IsInstance(graph, "graph"):
4636
+ if not silent:
4637
+ print("Graph.Community - Error: The input graph parameter is not a valid topologic graph. Returning None")
4638
+ return None
4639
+
4640
+ vertices = Graph.Vertices(graph)
4641
+ nx_graph = Graph.NetworkXGraph(graph, mantissa=mantissa, tolerance=tolerance)
4642
+ # Apply the Louvain algorithm
4643
+ partition = community_louvain.best_partition(nx_graph)
4644
+ communities = []
4645
+ # Add the partition value to each node's properties
4646
+ for node, community_id in partition.items():
4647
+ nx_graph.nodes[node][key] = community_id
4648
+ d = Topology.Dictionary(vertices[node])
4649
+ d = Dictionary.SetValueAtKey(d, key, community_id)
4650
+ vertices[node] = Topology.SetDictionary(vertices[node], d)
4651
+ communities.append(community_id)
4652
+ return communities
4260
4653
 
4261
4654
  @staticmethod
4262
4655
  def Connect(graph, verticesA, verticesB, tolerance=0.0001):
@@ -4305,6 +4698,53 @@ class Graph:
4305
4698
  _ = graph.Connect(verticesA, verticesB, tolerance) # Hook to Core
4306
4699
  return graph
4307
4700
 
4701
+ @staticmethod
4702
+ def Connectivity(graph, vertices=None, key: str = "connectivity", edgeKey: str = None, tolerance = 0.0001, silent = False):
4703
+ """
4704
+ Return the connectivity 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 connectivity of all the vertices in the input graph is computed. See https://www.spacesyntax.online/term/connectivity/.
4705
+
4706
+ Parameters
4707
+ ----------
4708
+ graph : topologic_core.Graph
4709
+ The input graph.
4710
+ vertices : list , optional
4711
+ The input list of vertices. The default is None.
4712
+ key : str , optional
4713
+ The dictionary key under which to save the connectivity score. The default is "connectivity".
4714
+ edgeKey : str , optional
4715
+ If specified, the value in the connected edges' dictionary specified by the edgeKey string will be aggregated to calculate
4716
+ 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.
4717
+ tolerance : float , optional
4718
+ The desired tolerance. The default is 0.0001.
4719
+
4720
+ tolerance : float , optional
4721
+ The desired tolerance. The default is 0.0001.
4722
+ silent : bool , optional
4723
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4724
+
4725
+ Returns
4726
+ -------
4727
+ list
4728
+ The closeness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
4729
+
4730
+ """
4731
+
4732
+ from topologicpy.Topology import Topology
4733
+ from topologicpy.Dictionary import Dictionary
4734
+
4735
+ if not Topology.IsInstance(graph, "Graph"):
4736
+ if not silent:
4737
+ print("Graph.ClosenessCentrality - Error: The input graph is not a valid graph. Returning None.")
4738
+ return None
4739
+ if vertices == None:
4740
+ vertices = Graph.Vertices(graph)
4741
+ connectivities = [Graph.VertexDegree(graph, v, edgeKey=edgeKey, tolerance=tolerance, silent=silent) for v in vertices]
4742
+ for i, v in enumerate(vertices):
4743
+ d = Topology.Dictionary(v)
4744
+ d = Dictionary.SetValueAtKey(d, key, connectivities[i])
4745
+ v = Topology.SetDictionary(v, d)
4746
+ return connectivities
4747
+
4308
4748
  @staticmethod
4309
4749
  def ContainsEdge(graph, edge, tolerance=0.0001):
4310
4750
  """
@@ -5790,11 +6230,12 @@ class Graph:
5790
6230
 
5791
6231
  @staticmethod
5792
6232
  def Reshape(graph,
5793
- shape="spring_2d",
6233
+ shape="spring 2D",
5794
6234
  k=0.8, seed=None,
5795
6235
  iterations=50,
5796
6236
  rootVertex=None,
5797
6237
  size=1,
6238
+ factor=1,
5798
6239
  sides=16,
5799
6240
  key="",
5800
6241
  tolerance=0.0001,
@@ -5808,16 +6249,17 @@ class Graph:
5808
6249
  The input graph.
5809
6250
  shape : str , optional
5810
6251
  The desired shape of the graph.
5811
- If set to 'spring_2d' or 'spring_3d', the algorithm uses a simplified version of the Fruchterman-Reingold force-directed algorithm to distribute the vertices.
5812
- If set to 'radial_2d', the nodes will be distributed along concentric circles in the XY plane.
5813
- If set to 'tree_2d' or 'tree_3d', the nodes will be distributed using the Reingold-Tillford layout.
5814
- If set to 'circle_2d', the nodes will be distributed on the cirumference of a segemented circles in the XY plane, based on the size and sides input parameter (radius=size/2).
5815
- If set to 'line_2d', the nodes will be distributed on a line in the XY plane based on the size input parameter (length=size).
5816
- If set to 'spehere_3d', the nodes will be distributed on the surface of a sphere based on the size input parameter raidus=size/2).
5817
- If set to 'grid_2d', the nodes will be distributed on a grid in the XY plane with size based on the size input parameter (length=width=size).
5818
- If set to 'grid_3d', the nodes will be distributed on a 3D cubic grid/matrix based on the size input parameter(width=length=height=size).
5819
- If set to 'cluster_2d', or 'cluster_3d, the nodes will be clustered according to the 'key' input parameter. The overall radius of the cluster is determined by the size input parameter (radius = size/2)
5820
- The default is 'spring_2d'.
6252
+ ['circle 2D', 'grid 2D', 'line 2D', 'radial 2D', 'spring 2D', 'tree 2D', 'grid 3D', 'sphere 3D', 'tree 3D']
6253
+ If set to 'spring 2D' or 'spring_3d', the algorithm uses a simplified version of the Fruchterman-Reingold force-directed algorithm to distribute the vertices.
6254
+ If set to 'radial 2D', the nodes will be distributed along concentric circles in the XY plane.
6255
+ If set to 'tree 2D' or 'tree 3D', the nodes will be distributed using the Reingold-Tillford layout.
6256
+ If set to 'circle 2D', the nodes will be distributed on the cirumference of a segemented circles in the XY plane, based on the size and sides input parameter (radius=size/2).
6257
+ If set to 'line 2D', the nodes will be distributed on a line in the XY plane based on the size input parameter (length=size).
6258
+ If set to 'spehere 3D', the nodes will be distributed on the surface of a sphere based on the size input parameter raidus=size/2).
6259
+ If set to 'grid 2D', the nodes will be distributed on a grid in the XY plane with size based on the size input parameter (length=width=size).
6260
+ If set to 'grid 3D', the nodes will be distributed on a 3D cubic grid/matrix based on the size input parameter(width=length=height=size).
6261
+ If set to 'cluster 2D', or 'cluster 3D, the nodes will be clustered according to the 'key' input parameter. The overall radius of the cluster is determined by the size input parameter (radius = size/2)
6262
+ The default is 'spring 2D'.
5821
6263
  k : float, optional
5822
6264
  The desired spring constant to use for the attractive and repulsive forces. The default is 0.8.
5823
6265
  seed : int , optional
@@ -5826,6 +6268,8 @@ class Graph:
5826
6268
  The desired maximum number of iterations to solve the forces in the 'spring' mode. The default is 50.
5827
6269
  rootVertex : topologic_core.Vertex , optional
5828
6270
  The desired vertex to use as the root of the tree and radial layouts.
6271
+ size : float , optional
6272
+ The desired overall size of the graph.
5829
6273
  sides : int , optional
5830
6274
  The desired number of sides of the circle layout option. The default is 16
5831
6275
  length : float, optional
@@ -6055,7 +6499,7 @@ class Graph:
6055
6499
 
6056
6500
  for i, c_v in enumerate(c_vertices):
6057
6501
  d = Topology.Dictionary(vertices[i])
6058
- c_v = Topology.SetDictionary(c_v, d)
6502
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6059
6503
  adj_dict = Graph.AdjacencyDictionary(graph)
6060
6504
  keys = adj_dict.keys()
6061
6505
 
@@ -6077,7 +6521,7 @@ class Graph:
6077
6521
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6078
6522
  if orig_edge_index:
6079
6523
  d = Topology.Dictionary(edges[orig_edge_index])
6080
- e = Topology.SetDictionary(e, d)
6524
+ e = Topology.SetDictionary(e, d, silent=True)
6081
6525
  c_edges.append(e)
6082
6526
  used[x][y] = 1
6083
6527
  used[y][x] = 1
@@ -6247,7 +6691,7 @@ class Graph:
6247
6691
  c_vertices = [Vertex.ByCoordinates(coord) for coord in c_points]
6248
6692
  for i, c_v in enumerate(c_vertices):
6249
6693
  d = Topology.Dictionary(vertices[i])
6250
- c_v = Topology.SetDictionary(c_v, d)
6694
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6251
6695
  adj_dict = Graph.AdjacencyDictionary(graph)
6252
6696
  keys = adj_dict.keys()
6253
6697
 
@@ -6269,7 +6713,7 @@ class Graph:
6269
6713
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6270
6714
  if orig_edge_index:
6271
6715
  d = Topology.Dictionary(edges[orig_edge_index])
6272
- e = Topology.SetDictionary(e, d)
6716
+ e = Topology.SetDictionary(e, d, silent=True)
6273
6717
  c_edges.append(e)
6274
6718
  used[x][y] = 1
6275
6719
  used[y][x] = 1
@@ -6299,7 +6743,7 @@ class Graph:
6299
6743
 
6300
6744
  for i, c_v in enumerate(c_vertices):
6301
6745
  d = Topology.Dictionary(vertices[i])
6302
- c_v = Topology.SetDictionary(c_v, d)
6746
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6303
6747
  adj_dict = Graph.AdjacencyDictionary(graph)
6304
6748
  keys = adj_dict.keys()
6305
6749
 
@@ -6314,7 +6758,7 @@ class Graph:
6314
6758
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6315
6759
  if orig_edge_index:
6316
6760
  d = Topology.Dictionary(edges[orig_edge_index])
6317
- e = Topology.SetDictionary(e, d)
6761
+ e = Topology.SetDictionary(e, d, silent=True)
6318
6762
  c_edges.append(e)
6319
6763
  used[x][y] = 1
6320
6764
  used[y][x] = 1
@@ -6340,7 +6784,7 @@ class Graph:
6340
6784
 
6341
6785
  for i, c_v in enumerate(c_vertices):
6342
6786
  d = Topology.Dictionary(vertices[i])
6343
- c_v = Topology.SetDictionary(c_v, d)
6787
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6344
6788
  adj_dict = Graph.AdjacencyDictionary(graph)
6345
6789
  keys = adj_dict.keys()
6346
6790
  c_edges = []
@@ -6357,7 +6801,7 @@ class Graph:
6357
6801
 
6358
6802
  orig_edge_index = edge_dict[str(x)+"_"+str(y)]
6359
6803
  d = Topology.Dictionary(edges[orig_edge_index])
6360
- e = Topology.SetDictionary(e, d)
6804
+ e = Topology.SetDictionary(e, d, silent=True)
6361
6805
  c_edges.append(e)
6362
6806
  used[x][y] = 1
6363
6807
  used[y][x] = 1
@@ -6382,7 +6826,7 @@ class Graph:
6382
6826
 
6383
6827
  for i, c_v in enumerate(c_vertices):
6384
6828
  d = Topology.Dictionary(vertices[i])
6385
- c_v = Topology.SetDictionary(c_v, d)
6829
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6386
6830
  adj_dict = Graph.AdjacencyDictionary(graph)
6387
6831
  keys = adj_dict.keys()
6388
6832
 
@@ -6397,7 +6841,7 @@ class Graph:
6397
6841
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6398
6842
  if orig_edge_index:
6399
6843
  d = Topology.Dictionary(edges[orig_edge_index])
6400
- e = Topology.SetDictionary(e, d)
6844
+ e = Topology.SetDictionary(e, d, silent=True)
6401
6845
  c_edges.append(e)
6402
6846
  used[x][y] = 1
6403
6847
  used[y][x] = 1
@@ -6574,46 +7018,97 @@ class Graph:
6574
7018
 
6575
7019
  return pos
6576
7020
 
7021
+
7022
+
6577
7023
  def radial_layout_2d(edge_list, root_index=0):
7024
+ import numpy as np
7025
+ from collections import deque
7026
+ # Build tree and get layout from Buchheim
6578
7027
  root, num_nodes = tree_from_edge_list(edge_list, root_index)
6579
7028
  dt = buchheim(root)
6580
- pos = np.zeros((num_nodes, 2))
6581
-
6582
- pos[int(dt.tree.node), 0] = dt.x
6583
- pos[int(dt.tree.node), 1] = dt.y
6584
-
6585
- old_roots = [dt]
6586
- new_roots = []
6587
7029
 
6588
- while(len(old_roots) > 0):
6589
- new_roots = []
6590
- for temp_root in old_roots:
6591
- children = temp_root.children
6592
- for child in children:
6593
- pos[int(child.tree.node), 0] = child.x
6594
- pos[int(child.tree.node), 1] = child.y
6595
- new_roots.extend(children)
6596
-
6597
- old_roots = new_roots
6598
-
6599
- # pos[:, 1] = np.max(pos[:, 1]) - pos[:, 1]
6600
- pos[:, 0] = pos[:, 0] - np.min(pos[:, 0])
6601
- pos[:, 1] = pos[:, 1] - np.min(pos[:, 1])
6602
-
6603
- pos[:, 0] = pos[:, 0] / np.max(pos[:, 0])
6604
- pos[:, 0] = pos[:, 0] - pos[:, 0][root_index]
6605
-
6606
- range_ = np.max(pos[:, 0]) - np.min(pos[:, 0])
6607
- pos[:, 0] = pos[:, 0] / range_
6608
-
6609
- pos[:, 0] = pos[:, 0] * np.pi * 1.98
6610
- pos[:, 1] = pos[:, 1] / np.max(pos[:, 1])
7030
+ # Initialize positions array
7031
+ pos = np.zeros((num_nodes, 2))
7032
+ pos[int(dt.tree.node)] = [dt.x, dt.y]
6611
7033
 
6612
- new_pos = np.zeros((num_nodes, 2))
7034
+ # Efficient tree traversal using a queue
7035
+ queue = deque([dt])
7036
+ while queue:
7037
+ current = queue.popleft()
7038
+ for child in current.children:
7039
+ pos[int(child.tree.node)] = [child.x, child.y]
7040
+ queue.append(child)
7041
+
7042
+ # Normalize positions
7043
+ pos[:, 0] -= np.min(pos[:, 0])
7044
+ pos[:, 1] -= np.min(pos[:, 1])
7045
+ pos[:, 0] /= np.max(pos[:, 0])
7046
+ pos[:, 1] /= np.max(pos[:, 1])
7047
+
7048
+ # Center the root and scale the x-coordinates
7049
+ pos[:, 0] -= pos[root_index, 0]
7050
+ pos[:, 0] /= (np.max(pos[:, 0]) - np.min(pos[:, 0]))
7051
+ pos[:, 0] *= np.pi * 1.98
7052
+
7053
+ # Convert to polar coordinates
7054
+ new_pos = np.zeros_like(pos)
6613
7055
  new_pos[:, 0] = pos[:, 1] * np.cos(pos[:, 0])
6614
7056
  new_pos[:, 1] = pos[:, 1] * np.sin(pos[:, 0])
6615
-
7057
+
6616
7058
  return new_pos
7059
+
7060
+ def dendrimer_layout_2d(graph, root_index=0, base_radius=1, radius_factor=1.5):
7061
+ """
7062
+ Given a graph as an adjacency dictionary, this function generates a dendrimer layout
7063
+ and returns positions of the nodes in 2D space.
7064
+
7065
+ :param graph: dict, adjacency dictionary where keys are node ids and values are sets/lists of neighboring nodes
7066
+ :param root_index: int, index of the node to start the layout (default: 0)
7067
+ :return: list of positions [x, y] for each node, sorted by node id
7068
+ """
7069
+ import numpy as np
7070
+
7071
+ # Initialize variables
7072
+ positions = {}
7073
+ visited = set()
7074
+ layers = {}
7075
+
7076
+ # Helper function to perform a DFS and organize nodes in layers
7077
+ def dfs(node, depth):
7078
+ visited.add(node)
7079
+ if depth not in layers:
7080
+ layers[depth] = []
7081
+ layers[depth].append(node)
7082
+ for neighbor in graph.get(node, []):
7083
+ if neighbor not in visited:
7084
+ dfs(neighbor, depth + 1)
7085
+
7086
+ # Start DFS from the given root node
7087
+ starting_node = list(graph.keys())[root_index]
7088
+ dfs(starting_node, 0)
7089
+
7090
+ # Perform DFS for all nodes to handle disconnected components
7091
+ for node in graph.keys():
7092
+ if node not in visited:
7093
+ dfs(node, 0) # Start a new DFS for each unvisited node
7094
+
7095
+ # Compute positions based on layers
7096
+ for depth, nodes in layers.items():
7097
+ print("depth:", depth)
7098
+ # Place nodes in a circular arrangement at each layer
7099
+ num_nodes = len(nodes)
7100
+ angle_step = 2 * np.pi / num_nodes if num_nodes > 0 else 0
7101
+ for i, node in enumerate(nodes):
7102
+ angle = i * angle_step
7103
+ x = base_radius*depth*np.cos(angle)
7104
+ y = base_radius*depth*np.sin(angle)
7105
+ positions[node] = (x, y)
7106
+
7107
+ # Sort the positions by node id and return them
7108
+ keys = list(positions.keys())
7109
+ keys.sort()
7110
+ return_values = [list(positions[key]) for key in keys]
7111
+ return return_values
6617
7112
 
6618
7113
  def spherical_layout_3d(edge_list, root_index=0):
6619
7114
  root, num_nodes = tree_from_edge_list(edge_list, root_index)
@@ -6686,7 +7181,7 @@ class Graph:
6686
7181
  return line_layout_2d(graph, length=size)
6687
7182
  elif 'grid' in shape.lower() and '2d' in shape.lower():
6688
7183
  return grid_layout_2d(graph, size=size)
6689
- elif 'sphere' == shape.lower() and '3d' in shape.lower():
7184
+ elif 'sphere' in shape.lower() and '3d' in shape.lower():
6690
7185
  return sphere_layout_3d(graph, radius=size/2)
6691
7186
  elif 'grid' in shape.lower() and '3d' in shape.lower():
6692
7187
  return grid_layout_3d(graph, size=size)
@@ -6717,12 +7212,13 @@ class Graph:
6717
7212
  elif 'tree' in shape.lower() and '2d' in shape.lower():
6718
7213
  positions = tree_layout_2d(edges, root_index=root_index)
6719
7214
  elif 'tree' in shape.lower() and '3d' in shape.lower():
6720
- positions = tree_layout_3d(edges, root_index=root_index, base_radius=1.0, radius_factor=1.5)
7215
+ positions = tree_layout_3d(edges, root_index=root_index, base_radius=size/2, radius_factor=factor)
7216
+ elif 'dendrimer' in shape.lower() and '2d' in shape.lower():
7217
+ positions = dendrimer_layout_2d(Graph.AdjacencyDictionary(graph), root_index=root_index, base_radius=size/2, radius_factor=factor)
6721
7218
  else:
6722
7219
  if not silent:
6723
7220
  print(f"{shape} is not implemented yet. Please choose from ['circle 2D', 'grid 2D', 'line 2D', 'radial 2D', 'spring 2D', 'tree 2D', 'grid 3D', 'sphere 3D', 'tree 3D']. Returning None.")
6724
7221
  return None
6725
- positions = positions.tolist()
6726
7222
  if len(positions[0]) == 3:
6727
7223
  positions = [[p[0], p[1], p[2]] for p in positions]
6728
7224
  else:
@@ -7253,6 +7749,113 @@ class Graph:
7253
7749
  json_string = json.dumps(json_data, indent=indent, sort_keys=sortKeys)
7254
7750
  return json_string
7255
7751
 
7752
+ @staticmethod
7753
+ def Leaves(graph, edgeKey: str = None, tolerance: float = 0.0001, silent: bool = False):
7754
+ """
7755
+ Returns a list of all vertices that have a degree of 1, also called leaf nodes.
7756
+
7757
+ Parameters
7758
+ ----------
7759
+ graph : topologic_core.Graph
7760
+ The input graph.
7761
+ edgeKey : str , optional
7762
+ If specified, the value in the connected edges' dictionary specified by the edgeKey string will be aggregated to calculate
7763
+ 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.
7764
+ tolerance : float , optional
7765
+ The desired tolerance. The default is 0.0001.
7766
+ silent : bool , optional
7767
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
7768
+
7769
+ Returns
7770
+ -------
7771
+ list
7772
+ The list of leaf nodes
7773
+
7774
+ """
7775
+ from topologicpy.Topology import Topology
7776
+
7777
+ if not Topology.IsInstance(graph, "graph"):
7778
+ if not silent:
7779
+ print("Graph.Leaves - Error: The input graph parameter is not a valid graph. Returning None.")
7780
+ return None
7781
+ return [v for v in Graph.Vertices(graph) if Graph.VertexDegree(graph, v, edgeKey=edgeKey, tolerance=tolerance, silent=silent) == 1]
7782
+
7783
+ @staticmethod
7784
+ def LineGraph(graph, transferVertexDictionaries=False, transferEdgeDictionaries=False, tolerance=0.0001, silent=False):
7785
+ """
7786
+ Create a line graph based on the input graph. See https://en.wikipedia.org/wiki/Line_graph.
7787
+
7788
+ Parameters
7789
+ ----------
7790
+ graph : topologic_core.Graph
7791
+ The input graph.
7792
+ transferVertexDictionaries : bool, optional
7793
+ If set to True, the dictionaries of the vertices of the input graph are transferred to the edges of the line graph.
7794
+ transferEdgeDictionaries : bool, optional
7795
+ If set to True, the dictionaries of the edges of the input graph are transferred to the vertices of the line graph.
7796
+ tolerance : float, optional
7797
+ The desired tolerance. The default is 0.0001.
7798
+ silent : bool , optional
7799
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
7800
+
7801
+ Returns
7802
+ -------
7803
+ topologic_core.Graph
7804
+ The created line graph.
7805
+
7806
+ """
7807
+ from topologicpy.Edge import Edge
7808
+ from topologicpy.Topology import Topology
7809
+
7810
+ if not Topology.IsInstance(graph, "graph"):
7811
+ if not silent:
7812
+ print("Graph.LineGraph - Error: The input graph parameter is not a valid graph. Returning None.")
7813
+ return None
7814
+
7815
+ graph_vertices = Graph.Vertices(graph)
7816
+ graph_edges = Graph.Edges(graph)
7817
+
7818
+ # Create line graph vertices (centroids of original graph edges)
7819
+ if transferEdgeDictionaries == True:
7820
+ lg_vertices = [
7821
+ Topology.SetDictionary(Topology.Centroid(edge), Topology.Dictionary(edge), silent=silent)
7822
+ for edge in graph_edges
7823
+ ]
7824
+ else:
7825
+ lg_vertices = [Topology.Centroid(edge) for edge in graph_edges]
7826
+
7827
+ lg_edges = []
7828
+ if transferVertexDictionaries == True:
7829
+ for v in graph_vertices:
7830
+ edges = Graph.Edges(graph, vertices=[v])
7831
+ if len(edges) > 1:
7832
+ d = Topology.Dictionary(v) # Only need to call Dictionary once
7833
+ visited = set() # Use a set to track visited pairs of edges
7834
+ centroids = [Topology.Centroid(e) for e in edges] # Precompute centroids once
7835
+ for i in range(len(edges)):
7836
+ for j in range(i + 1, len(edges)): # Only loop over pairs (i, j) where i < j
7837
+ if (i, j) not in visited:
7838
+ lg_edge = Edge.ByVertices([centroids[i], centroids[j]], tolerance=tolerance, silent=silent)
7839
+ lg_edge = Topology.SetDictionary(lg_edge, d, silent=silent)
7840
+ lg_edges.append(lg_edge)
7841
+ visited.add((i, j))
7842
+ visited.add((j, i)) # Ensure both directions are marked as visited
7843
+ else:
7844
+ for v in graph_vertices:
7845
+ edges = Graph.Edges(graph, vertices=[v])
7846
+ if len(edges) > 1:
7847
+ visited = set() # Use a set to track visited pairs of edges
7848
+ centroids = [Topology.Centroid(e) for e in edges] # Precompute centroids once
7849
+ for i in range(len(edges)):
7850
+ for j in range(i + 1, len(edges)): # Only loop over pairs (i, j) where i < j
7851
+ if (i, j) not in visited:
7852
+ lg_edge = Edge.ByVertices([centroids[i], centroids[j]], tolerance=tolerance, silent=silent)
7853
+ lg_edges.append(lg_edge)
7854
+ visited.add((i, j))
7855
+ visited.add((j, i)) # Ensure both directions are marked as visited
7856
+
7857
+ return Graph.ByVerticesEdges(lg_vertices, lg_edges)
7858
+
7256
7859
  @staticmethod
7257
7860
  def LocalClusteringCoefficient(graph, vertices: list = None, key: str = "lcc", mantissa: int = 6, tolerance: float = 0.0001):
7258
7861
  """
@@ -7912,18 +8515,26 @@ class Graph:
7912
8515
  return nearestVertex
7913
8516
 
7914
8517
  @staticmethod
7915
- def NetworkXGraph(graph, mantissa: int = 6, tolerance: float = 0.0001):
8518
+ def NetworkXGraph(graph, xKey='x', yKey='y', zKey='z', mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
7916
8519
  """
7917
- converts the input graph into a NetworkX Graph. See http://networkx.org
8520
+ Converts the input graph into a NetworkX Graph. See http://networkx.org
7918
8521
 
7919
8522
  Parameters
7920
8523
  ----------
7921
8524
  graph : topologic_core.Graph
7922
8525
  The input graph.
8526
+ xKey : str , optional
8527
+ The dictionary key under which to save the X-Coordinate of the vertex. The default is 'x'.
8528
+ yKey : str , optional
8529
+ The dictionary key under which to save the Y-Coordinate of the vertex. The default is 'y'.
8530
+ zKey : str , optional
8531
+ The dictionary key under which to save the Z-Coordinate of the vertex. The default is 'z'.
7923
8532
  mantissa : int , optional
7924
8533
  The desired length of the mantissa. The default is 6.
7925
8534
  tolerance : float , optional
7926
8535
  The desired tolerance. The default is 0.0001.
8536
+ silent : bool , optional
8537
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
7927
8538
 
7928
8539
  Returns
7929
8540
  -------
@@ -7951,7 +8562,8 @@ class Graph:
7951
8562
  return None
7952
8563
 
7953
8564
  if not Topology.IsInstance(graph, "Graph"):
7954
- print("Graph.NetworkXGraph - Error: The input graph is not a valid graph. Returning None.")
8565
+ if not silent:
8566
+ print("Graph.NetworkXGraph - Error: The input graph is not a valid graph. Returning None.")
7955
8567
  return None
7956
8568
 
7957
8569
  nxGraph = nx.Graph()
@@ -7968,8 +8580,7 @@ class Graph:
7968
8580
  values = Dictionary.Values(d)
7969
8581
  if not values:
7970
8582
  values = []
7971
- keys += ["x","y","z"]
7972
- import random
8583
+ keys += [xKey,yKey,zKey]
7973
8584
  values += [Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa), Vertex.Z(v, mantissa=mantissa)]
7974
8585
  d = Dictionary.ByKeysValues(keys,values)
7975
8586
  pythonD = Dictionary.PythonDictionary(d)
@@ -8879,6 +9490,28 @@ class Graph:
8879
9490
  return None
8880
9491
  return len(Graph.Edges(graph))
8881
9492
 
9493
+ @staticmethod
9494
+ def _topological_distance(g, start, target):
9495
+ from collections import deque
9496
+ if start == target:
9497
+ return 0
9498
+ visited = set()
9499
+ queue = deque([(start, 0)]) # Each element is a tuple (vertex, distance)
9500
+
9501
+ while queue:
9502
+ current, distance = queue.popleft()
9503
+ if current in visited:
9504
+ continue
9505
+
9506
+ visited.add(current)
9507
+ for neighbor in g.get(current, []):
9508
+ if neighbor == target:
9509
+ return distance + 1
9510
+ if neighbor not in visited:
9511
+ queue.append((neighbor, distance + 1))
9512
+
9513
+ return None # Target not reachable
9514
+
8882
9515
  @staticmethod
8883
9516
  def TopologicalDistance(graph, vertexA, vertexB, tolerance=0.0001):
8884
9517
  """
@@ -8901,6 +9534,8 @@ class Graph:
8901
9534
  The topological distance between the input vertices.
8902
9535
 
8903
9536
  """
9537
+
9538
+ from topologicpy.Vertex import Vertex
8904
9539
  from topologicpy.Topology import Topology
8905
9540
 
8906
9541
  if not Topology.IsInstance(graph, "Graph"):
@@ -8912,7 +9547,19 @@ class Graph:
8912
9547
  if not Topology.IsInstance(vertexB, "Vertex"):
8913
9548
  print("Graph.TopologicalDistance - Error: The input vertexB is not a valid vertex. Returning None.")
8914
9549
  return None
8915
- return graph.TopologicalDistance(vertexA, vertexB, tolerance) # Hook to Core
9550
+
9551
+ g = Graph.AdjacencyDictionary(graph)
9552
+ vertices = Graph.Vertices(graph)
9553
+ keys = list(g.keys())
9554
+ index_a = Vertex.Index(vertexA, vertices, tolerance=tolerance)
9555
+ if index_a == None:
9556
+ return 0
9557
+ start = keys[index_a]
9558
+ index_b = Vertex.Index(vertexB, vertices, tolerance=tolerance)
9559
+ if index_b == None:
9560
+ return 0
9561
+ target = keys[index_b]
9562
+ return Graph._topological_distance(g, start, target)
8916
9563
 
8917
9564
  @staticmethod
8918
9565
  def Topology(graph):
@@ -9016,7 +9663,7 @@ class Graph:
9016
9663
  return Graph.ByVerticesEdges(dictionary['vertices'], dictionary['edges'])
9017
9664
 
9018
9665
  @staticmethod
9019
- def VertexDegree(graph, vertex, edgeKey: str = None, tolerance: float = 0.0001):
9666
+ def VertexDegree(graph, vertex, edgeKey: str = None, tolerance: float = 0.0001, silent: bool = False):
9020
9667
  """
9021
9668
  Returns the degree of the input vertex. See https://en.wikipedia.org/wiki/Degree_(graph_theory).
9022
9669
 
@@ -9031,6 +9678,8 @@ class Graph:
9031
9678
  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.
9032
9679
  tolerance : float , optional
9033
9680
  The desired tolerance. The default is 0.0001.
9681
+ silent : bool , optional
9682
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
9034
9683
 
9035
9684
  Returns
9036
9685
  -------
@@ -9043,10 +9692,12 @@ class Graph:
9043
9692
  import numbers
9044
9693
 
9045
9694
  if not Topology.IsInstance(graph, "Graph"):
9046
- print("Graph.VertexDegree - Error: The input graph is not a valid graph. Returning None.")
9695
+ if not silent:
9696
+ print("Graph.VertexDegree - Error: The input graph is not a valid graph. Returning None.")
9047
9697
  return None
9048
9698
  if not Topology.IsInstance(vertex, "Vertex"):
9049
- print("Graph.VertexDegree - Error: The input vertex is not a valid vertex. Returning None.")
9699
+ if not silent:
9700
+ print("Graph.VertexDegree - Error: The input vertex is not a valid vertex. Returning None.")
9050
9701
  return None
9051
9702
  if not isinstance(edgeKey, str):
9052
9703
  edgeKey = ""