topologicpy 0.7.90__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/Face.py CHANGED
@@ -1690,6 +1690,8 @@ class Face():
1690
1690
  """
1691
1691
  Returns the face representing the isovist projection from the input viewpoint.
1692
1692
  This method assumes all input is in 2D. Z coordinates are ignored.
1693
+ This method and the metrics are largely derived from isovists.org. Even if not explicitly listed, please assume that all credit
1694
+ goes to the authors of that website and its associated software.
1693
1695
 
1694
1696
  Parameters
1695
1697
  ----------
@@ -1731,6 +1733,12 @@ class Face():
1731
1733
  - d_f : float, Fractal Dimension measures the complexity of the isovist's boundary.
1732
1734
  - e_c : float , Edge Complexity measures how complex the edges of the isovist boundary are.
1733
1735
  - theta : float, Mean Visual Field Angle measures the average angular extent of the visible area from the observation point.
1736
+ - occlusivity: float, the proportion of edges of an isovist that are not physically defined.
1737
+ - drift: float, the distance from the observation point to the centroid of its isovist.
1738
+ - closed_perimeter: float, the total length of non-occluded edges of the isovist.
1739
+ - average_radial: float, "the mean view length of all space visible from a location." (from isovists.org)
1740
+ - variance: float, "the mean of the square of deviation between all radial lengths and average radial length of an isovist (Benedikt, 1979)." (from isovists.org)
1741
+ - skewness: float, "the mean of the cube of deviation between all radial lengths and average radial length of an isovist (Benediky, 1979)." (from isovists.org)
1734
1742
  triangles : bool , optional
1735
1743
  If set to True, the subtended triangles of the isovist are created and stored as contents of the returned isovist face. The default is False.
1736
1744
  mantissa : int , optional
@@ -1934,6 +1942,81 @@ class Face():
1934
1942
 
1935
1943
  return float(distance), Vertex.ByCoordinates(list(closest_point))
1936
1944
 
1945
+
1946
+
1947
+ def compute_average_radial_variance_skewness(vertex, edges, mantissa=6):
1948
+ from math import atan2, pi, sqrt, pow
1949
+
1950
+ def subtended_angle(vertex, edge, mantissa=6):
1951
+ """Compute the angle subtended by the edge at point V."""
1952
+ v = Vertex.Coordinates(vertex, mantissa=mantissa)
1953
+ start = Vertex.Coordinates(Edge.StartVertex(edge), mantissa=mantissa)
1954
+ end = Vertex.Coordinates(Edge.EndVertex(edge), mantissa=mantissa)
1955
+ # Calculate the angles of the start and end vertices relative to V
1956
+ angle_start = atan2(start[1] - v[1], start[0] - v[0])
1957
+ angle_end = atan2(end[1] - v[1], end[0] - v[0])
1958
+ # Ensure the angle is in the range [0, 2*pi]
1959
+ angle_start = angle_start if angle_start >= 0 else angle_start + 2 * pi
1960
+ angle_end = angle_end if angle_end >= 0 else angle_end + 2 * pi
1961
+ # Compute the difference and handle wrapping around 2*pi
1962
+ angle_diff = abs(angle_end - angle_start)
1963
+ return min(angle_diff, 2 * pi - angle_diff)
1964
+
1965
+ total_weighted_distance = 0
1966
+ total_angle_weight = 0
1967
+ total_weighted_squared_deviation = 0
1968
+ total_weighted_cubed_deviation = 0
1969
+ distances = []
1970
+ angles = []
1971
+ for edge in edges:
1972
+ # Calculate the distance between V and the edge
1973
+ distance = Vertex.Distance(vertex, edge, mantissa=mantissa)
1974
+ distances.append(distance)
1975
+
1976
+ # Calculate the subtended angle for the edge
1977
+ angle = subtended_angle(vertex, edge, mantissa=mantissa)
1978
+ angles.append(angle)
1979
+
1980
+ # Weight the distance by the subtended angle
1981
+ total_weighted_distance += distance * angle
1982
+ total_angle_weight += angle
1983
+
1984
+ # Compute the Average Radial value
1985
+ if total_angle_weight == 0:
1986
+ average_radial = 0 # Avoid division by zero
1987
+ else:
1988
+ average_radial = round(total_weighted_distance / total_angle_weight, mantissa)
1989
+
1990
+ # Compute Variance
1991
+ for i, edge in enumerate(edges):
1992
+ # Calculate the distance between V and the edge
1993
+ distance = distances[i]
1994
+ # Calculate the subtended angle for the edge
1995
+ angle = angles[i]
1996
+
1997
+ # Calculate the deviation squared from the average radial
1998
+ deviation_squared = (distance - average_radial) ** 2
1999
+ # Calculate the deviation cubed from the average radial
2000
+ deviation_cubed = (distance - average_radial) ** 3
2001
+ # Weight the squared deviation by the subtended angle
2002
+ total_weighted_squared_deviation += deviation_squared * angle
2003
+ total_weighted_cubed_deviation += deviation_cubed * angle
2004
+
2005
+ # Compute the Variance value
2006
+ if total_angle_weight == 0:
2007
+ variance = 0 # Avoid division by zero
2008
+ else:
2009
+ variance = round(sqrt(total_weighted_squared_deviation / total_angle_weight), mantissa)
2010
+
2011
+ # Compute the Skewness value
2012
+ if total_angle_weight == 0:
2013
+ skewness = 0 # Avoid division by zero
2014
+ else:
2015
+ skewness = round(pow(total_weighted_cubed_deviation / total_angle_weight, 1/3), mantissa)
2016
+
2017
+ return average_radial, variance, skewness
2018
+
2019
+ # Main Code
1937
2020
  origin = Topology.Centroid(face)
1938
2021
  normal = Face.Normal(face)
1939
2022
  flat_face = Topology.Flatten(face, origin=origin, direction=normal)
@@ -2022,7 +2105,7 @@ class Face():
2022
2105
  return None
2023
2106
  simpler_face = Face.RemoveCollinearEdges(return_face)
2024
2107
  if Topology.IsInstance(simpler_face, "face"):
2025
- if transferDictionaries == True:
2108
+ if transferDictionaries == True or metrics == True:
2026
2109
  j_edges = [Topology.Edges(t) for t in obstacles]
2027
2110
  j_edges = Helper.Flatten(j_edges)
2028
2111
  j_edges += Topology.Edges(face)
@@ -2030,19 +2113,24 @@ class Face():
2030
2113
  used = [0 for _ in range(len(j_edges))]
2031
2114
  for i, i_edge in enumerate(i_edges):
2032
2115
  d_i = Topology.Dictionary(i_edge)
2116
+ d_i = Dictionary.SetValueAtKey(d_i, "occlusive", True)
2117
+ i_edge = Topology.SetDictionary(i_edge, d_i)
2033
2118
  for j, j_edge in enumerate(j_edges):
2034
2119
  if used[j] == 0:
2035
2120
  if Edge.IsCollinear(i_edge, j_edge):
2121
+ d_i = Dictionary.SetValueAtKey(d_i, "occlusive", False)
2122
+ i_edge = Topology.SetDictionary(i_edge, d_i)
2036
2123
  d_j = Topology.Dictionary(j_edge)
2037
2124
  d_result = Dictionary.ByMergedDictionaries([d_i, d_j])
2038
- i_edge = Topology.SetDictionary(i_edge, d_result)
2125
+ if transferDictionaries == True:
2126
+ i_edge = Topology.SetDictionary(i_edge, d_result)
2039
2127
  used[j] == 1
2040
2128
 
2041
2129
  return_face = Topology.Unflatten(simpler_face, origin=origin, direction=normal)
2042
- return_face = Topology.Unflatten(return_face, origin=origin, direction=normal)
2130
+ else:
2131
+ return_face = Topology.Unflatten(return_face, origin=origin, direction=normal)
2043
2132
  if metrics == True:
2044
2133
  vertices = Topology.Vertices(return_face)
2045
- edges = Topology.Edges(return_face)
2046
2134
  # 1 Viewpoint
2047
2135
  viewpoint = Vertex.Coordinates(vertex, mantissa=mantissa)
2048
2136
  # 2 Direction
@@ -2110,6 +2198,27 @@ class Face():
2110
2198
  e_c = round(edge_complexity(coords), mantissa)
2111
2199
  # 18 Mean Visual Field Angle
2112
2200
  theta = round(mean_visual_field_angle(viewpoint, coords), mantissa)
2201
+ # 19 Occlusivity
2202
+ occ_length = 0
2203
+ edges = Topology.Edges(return_face)
2204
+ for edge in edges:
2205
+ d = Topology.Dictionary(edge)
2206
+ if Dictionary.ValueAtKey(d, "occlusive") == True:
2207
+ occ_length += Edge.Length(edge)
2208
+ if perimeter > 0:
2209
+ occlusivity = round(occ_length/perimeter, mantissa)
2210
+ else:
2211
+ occlusivity = round(0.0, 6)
2212
+
2213
+ # 20 Drift
2214
+ drift = Vertex.Distance(vertex, Topology.Centroid(return_face), mantissa=mantissa)
2215
+
2216
+ # 21 Closed Perimeter
2217
+ closed_perimeter = round(perimeter - occ_length, mantissa)
2218
+
2219
+ # 22/23/24 Average Radial, Variance, and Skewness
2220
+ average_radial, variance, skewness = compute_average_radial_variance_skewness(vertex, edges, mantissa=6)
2221
+
2113
2222
  keys = ["viewpoint",
2114
2223
  "direction",
2115
2224
  "fov",
@@ -2127,7 +2236,14 @@ class Face():
2127
2236
  "symmetry",
2128
2237
  "d_f",
2129
2238
  "e_c",
2130
- "theta"]
2239
+ "theta",
2240
+ "occlusivity",
2241
+ "drift",
2242
+ "closed_perimeter",
2243
+ "average_radial",
2244
+ "variance",
2245
+ "skewness"]
2246
+
2131
2247
  values = [viewpoint,
2132
2248
  direction,
2133
2249
  fov,
@@ -2145,7 +2261,13 @@ class Face():
2145
2261
  symmetry,
2146
2262
  d_f,
2147
2263
  e_c,
2148
- theta]
2264
+ theta,
2265
+ occlusivity,
2266
+ drift,
2267
+ closed_perimeter,
2268
+ average_radial,
2269
+ variance,
2270
+ skewness]
2149
2271
  d = Dictionary.ByKeysValues(keys, values)
2150
2272
  return_face = Topology.SetDictionary(return_face, d)
2151
2273
  if triangles:
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
 
@@ -2729,7 +2835,82 @@ class Graph:
2729
2835
  g_e = Topology.SetDictionary(g_e, d)
2730
2836
  g_edges.append(g_e)
2731
2837
  return Graph.ByVerticesEdges(g_vertices, g_edges)
2732
-
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
+
2733
2914
  @staticmethod
2734
2915
  def ByTopology(topology,
2735
2916
  direct: bool = True,
@@ -4102,6 +4283,48 @@ class Graph:
4102
4283
  v = Topology.SetDictionary(v, d)
4103
4284
  return graph
4104
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
+
4105
4328
  @staticmethod
4106
4329
  def ConnectedComponents(graph, tolerance: float = 0.0001, silent: bool = False):
4107
4330
  """
@@ -4366,6 +4589,68 @@ class Graph:
4366
4589
  return_centralities.append(centralities[i])
4367
4590
  return centralities
4368
4591
 
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
+
4621
+ try:
4622
+ import community as community_louvain
4623
+ except:
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
4653
+
4369
4654
  @staticmethod
4370
4655
  def Connect(graph, verticesA, verticesB, tolerance=0.0001):
4371
4656
  """
@@ -4413,6 +4698,53 @@ class Graph:
4413
4698
  _ = graph.Connect(verticesA, verticesB, tolerance) # Hook to Core
4414
4699
  return graph
4415
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
+
4416
4748
  @staticmethod
4417
4749
  def ContainsEdge(graph, edge, tolerance=0.0001):
4418
4750
  """
@@ -5898,11 +6230,12 @@ class Graph:
5898
6230
 
5899
6231
  @staticmethod
5900
6232
  def Reshape(graph,
5901
- shape="spring_2d",
6233
+ shape="spring 2D",
5902
6234
  k=0.8, seed=None,
5903
6235
  iterations=50,
5904
6236
  rootVertex=None,
5905
6237
  size=1,
6238
+ factor=1,
5906
6239
  sides=16,
5907
6240
  key="",
5908
6241
  tolerance=0.0001,
@@ -5916,16 +6249,17 @@ class Graph:
5916
6249
  The input graph.
5917
6250
  shape : str , optional
5918
6251
  The desired shape of the graph.
5919
- 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.
5920
- If set to 'radial_2d', the nodes will be distributed along concentric circles in the XY plane.
5921
- If set to 'tree_2d' or 'tree_3d', the nodes will be distributed using the Reingold-Tillford layout.
5922
- 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).
5923
- 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).
5924
- 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).
5925
- 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).
5926
- 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).
5927
- 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)
5928
- 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'.
5929
6263
  k : float, optional
5930
6264
  The desired spring constant to use for the attractive and repulsive forces. The default is 0.8.
5931
6265
  seed : int , optional
@@ -5934,6 +6268,8 @@ class Graph:
5934
6268
  The desired maximum number of iterations to solve the forces in the 'spring' mode. The default is 50.
5935
6269
  rootVertex : topologic_core.Vertex , optional
5936
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.
5937
6273
  sides : int , optional
5938
6274
  The desired number of sides of the circle layout option. The default is 16
5939
6275
  length : float, optional
@@ -6163,7 +6499,7 @@ class Graph:
6163
6499
 
6164
6500
  for i, c_v in enumerate(c_vertices):
6165
6501
  d = Topology.Dictionary(vertices[i])
6166
- c_v = Topology.SetDictionary(c_v, d)
6502
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6167
6503
  adj_dict = Graph.AdjacencyDictionary(graph)
6168
6504
  keys = adj_dict.keys()
6169
6505
 
@@ -6185,7 +6521,7 @@ class Graph:
6185
6521
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6186
6522
  if orig_edge_index:
6187
6523
  d = Topology.Dictionary(edges[orig_edge_index])
6188
- e = Topology.SetDictionary(e, d)
6524
+ e = Topology.SetDictionary(e, d, silent=True)
6189
6525
  c_edges.append(e)
6190
6526
  used[x][y] = 1
6191
6527
  used[y][x] = 1
@@ -6355,7 +6691,7 @@ class Graph:
6355
6691
  c_vertices = [Vertex.ByCoordinates(coord) for coord in c_points]
6356
6692
  for i, c_v in enumerate(c_vertices):
6357
6693
  d = Topology.Dictionary(vertices[i])
6358
- c_v = Topology.SetDictionary(c_v, d)
6694
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6359
6695
  adj_dict = Graph.AdjacencyDictionary(graph)
6360
6696
  keys = adj_dict.keys()
6361
6697
 
@@ -6377,7 +6713,7 @@ class Graph:
6377
6713
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6378
6714
  if orig_edge_index:
6379
6715
  d = Topology.Dictionary(edges[orig_edge_index])
6380
- e = Topology.SetDictionary(e, d)
6716
+ e = Topology.SetDictionary(e, d, silent=True)
6381
6717
  c_edges.append(e)
6382
6718
  used[x][y] = 1
6383
6719
  used[y][x] = 1
@@ -6407,7 +6743,7 @@ class Graph:
6407
6743
 
6408
6744
  for i, c_v in enumerate(c_vertices):
6409
6745
  d = Topology.Dictionary(vertices[i])
6410
- c_v = Topology.SetDictionary(c_v, d)
6746
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6411
6747
  adj_dict = Graph.AdjacencyDictionary(graph)
6412
6748
  keys = adj_dict.keys()
6413
6749
 
@@ -6422,7 +6758,7 @@ class Graph:
6422
6758
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6423
6759
  if orig_edge_index:
6424
6760
  d = Topology.Dictionary(edges[orig_edge_index])
6425
- e = Topology.SetDictionary(e, d)
6761
+ e = Topology.SetDictionary(e, d, silent=True)
6426
6762
  c_edges.append(e)
6427
6763
  used[x][y] = 1
6428
6764
  used[y][x] = 1
@@ -6448,7 +6784,7 @@ class Graph:
6448
6784
 
6449
6785
  for i, c_v in enumerate(c_vertices):
6450
6786
  d = Topology.Dictionary(vertices[i])
6451
- c_v = Topology.SetDictionary(c_v, d)
6787
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6452
6788
  adj_dict = Graph.AdjacencyDictionary(graph)
6453
6789
  keys = adj_dict.keys()
6454
6790
  c_edges = []
@@ -6465,7 +6801,7 @@ class Graph:
6465
6801
 
6466
6802
  orig_edge_index = edge_dict[str(x)+"_"+str(y)]
6467
6803
  d = Topology.Dictionary(edges[orig_edge_index])
6468
- e = Topology.SetDictionary(e, d)
6804
+ e = Topology.SetDictionary(e, d, silent=True)
6469
6805
  c_edges.append(e)
6470
6806
  used[x][y] = 1
6471
6807
  used[y][x] = 1
@@ -6490,7 +6826,7 @@ class Graph:
6490
6826
 
6491
6827
  for i, c_v in enumerate(c_vertices):
6492
6828
  d = Topology.Dictionary(vertices[i])
6493
- c_v = Topology.SetDictionary(c_v, d)
6829
+ c_v = Topology.SetDictionary(c_v, d, silent=True)
6494
6830
  adj_dict = Graph.AdjacencyDictionary(graph)
6495
6831
  keys = adj_dict.keys()
6496
6832
 
@@ -6505,7 +6841,7 @@ class Graph:
6505
6841
  orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6506
6842
  if orig_edge_index:
6507
6843
  d = Topology.Dictionary(edges[orig_edge_index])
6508
- e = Topology.SetDictionary(e, d)
6844
+ e = Topology.SetDictionary(e, d, silent=True)
6509
6845
  c_edges.append(e)
6510
6846
  used[x][y] = 1
6511
6847
  used[y][x] = 1
@@ -6682,46 +7018,97 @@ class Graph:
6682
7018
 
6683
7019
  return pos
6684
7020
 
7021
+
7022
+
6685
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
6686
7027
  root, num_nodes = tree_from_edge_list(edge_list, root_index)
6687
7028
  dt = buchheim(root)
6688
- pos = np.zeros((num_nodes, 2))
6689
7029
 
6690
- pos[int(dt.tree.node), 0] = dt.x
6691
- pos[int(dt.tree.node), 1] = dt.y
6692
-
6693
- old_roots = [dt]
6694
- new_roots = []
6695
-
6696
- while(len(old_roots) > 0):
6697
- new_roots = []
6698
- for temp_root in old_roots:
6699
- children = temp_root.children
6700
- for child in children:
6701
- pos[int(child.tree.node), 0] = child.x
6702
- pos[int(child.tree.node), 1] = child.y
6703
- new_roots.extend(children)
6704
-
6705
- old_roots = new_roots
6706
-
6707
- # pos[:, 1] = np.max(pos[:, 1]) - pos[:, 1]
6708
- pos[:, 0] = pos[:, 0] - np.min(pos[:, 0])
6709
- pos[:, 1] = pos[:, 1] - np.min(pos[:, 1])
6710
-
6711
- pos[:, 0] = pos[:, 0] / np.max(pos[:, 0])
6712
- pos[:, 0] = pos[:, 0] - pos[:, 0][root_index]
6713
-
6714
- range_ = np.max(pos[:, 0]) - np.min(pos[:, 0])
6715
- pos[:, 0] = pos[:, 0] / range_
6716
-
6717
- pos[:, 0] = pos[:, 0] * np.pi * 1.98
6718
- 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]
6719
7033
 
6720
- 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)
6721
7055
  new_pos[:, 0] = pos[:, 1] * np.cos(pos[:, 0])
6722
7056
  new_pos[:, 1] = pos[:, 1] * np.sin(pos[:, 0])
6723
-
7057
+
6724
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
6725
7112
 
6726
7113
  def spherical_layout_3d(edge_list, root_index=0):
6727
7114
  root, num_nodes = tree_from_edge_list(edge_list, root_index)
@@ -6794,7 +7181,7 @@ class Graph:
6794
7181
  return line_layout_2d(graph, length=size)
6795
7182
  elif 'grid' in shape.lower() and '2d' in shape.lower():
6796
7183
  return grid_layout_2d(graph, size=size)
6797
- elif 'sphere' == shape.lower() and '3d' in shape.lower():
7184
+ elif 'sphere' in shape.lower() and '3d' in shape.lower():
6798
7185
  return sphere_layout_3d(graph, radius=size/2)
6799
7186
  elif 'grid' in shape.lower() and '3d' in shape.lower():
6800
7187
  return grid_layout_3d(graph, size=size)
@@ -6825,12 +7212,13 @@ class Graph:
6825
7212
  elif 'tree' in shape.lower() and '2d' in shape.lower():
6826
7213
  positions = tree_layout_2d(edges, root_index=root_index)
6827
7214
  elif 'tree' in shape.lower() and '3d' in shape.lower():
6828
- 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)
6829
7218
  else:
6830
7219
  if not silent:
6831
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.")
6832
7221
  return None
6833
- positions = positions.tolist()
6834
7222
  if len(positions[0]) == 3:
6835
7223
  positions = [[p[0], p[1], p[2]] for p in positions]
6836
7224
  else:
@@ -7361,6 +7749,113 @@ class Graph:
7361
7749
  json_string = json.dumps(json_data, indent=indent, sort_keys=sortKeys)
7362
7750
  return json_string
7363
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
+
7364
7859
  @staticmethod
7365
7860
  def LocalClusteringCoefficient(graph, vertices: list = None, key: str = "lcc", mantissa: int = 6, tolerance: float = 0.0001):
7366
7861
  """
@@ -8020,18 +8515,26 @@ class Graph:
8020
8515
  return nearestVertex
8021
8516
 
8022
8517
  @staticmethod
8023
- 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):
8024
8519
  """
8025
- 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
8026
8521
 
8027
8522
  Parameters
8028
8523
  ----------
8029
8524
  graph : topologic_core.Graph
8030
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'.
8031
8532
  mantissa : int , optional
8032
8533
  The desired length of the mantissa. The default is 6.
8033
8534
  tolerance : float , optional
8034
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.
8035
8538
 
8036
8539
  Returns
8037
8540
  -------
@@ -8059,7 +8562,8 @@ class Graph:
8059
8562
  return None
8060
8563
 
8061
8564
  if not Topology.IsInstance(graph, "Graph"):
8062
- 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.")
8063
8567
  return None
8064
8568
 
8065
8569
  nxGraph = nx.Graph()
@@ -8076,8 +8580,7 @@ class Graph:
8076
8580
  values = Dictionary.Values(d)
8077
8581
  if not values:
8078
8582
  values = []
8079
- keys += ["x","y","z"]
8080
- import random
8583
+ keys += [xKey,yKey,zKey]
8081
8584
  values += [Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa), Vertex.Z(v, mantissa=mantissa)]
8082
8585
  d = Dictionary.ByKeysValues(keys,values)
8083
8586
  pythonD = Dictionary.PythonDictionary(d)
@@ -9160,7 +9663,7 @@ class Graph:
9160
9663
  return Graph.ByVerticesEdges(dictionary['vertices'], dictionary['edges'])
9161
9664
 
9162
9665
  @staticmethod
9163
- 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):
9164
9667
  """
9165
9668
  Returns the degree of the input vertex. See https://en.wikipedia.org/wiki/Degree_(graph_theory).
9166
9669
 
@@ -9175,6 +9678,8 @@ class Graph:
9175
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.
9176
9679
  tolerance : float , optional
9177
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.
9178
9683
 
9179
9684
  Returns
9180
9685
  -------
@@ -9187,10 +9692,12 @@ class Graph:
9187
9692
  import numbers
9188
9693
 
9189
9694
  if not Topology.IsInstance(graph, "Graph"):
9190
- 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.")
9191
9697
  return None
9192
9698
  if not Topology.IsInstance(vertex, "Vertex"):
9193
- 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.")
9194
9701
  return None
9195
9702
  if not isinstance(edgeKey, str):
9196
9703
  edgeKey = ""
topologicpy/Plotly.py CHANGED
@@ -411,29 +411,46 @@ class Plotly:
411
411
 
412
412
  if showVertices:
413
413
  vertices = Graph.Vertices(graph)
414
- #v_dictionaries = [Topology.Dictionary(v) for v in vertices]
414
+ v_dictionaries = [Topology.Dictionary(v) for v in vertices]
415
415
  e_cluster = Cluster.ByTopologies(vertices)
416
416
  geo = Topology.Geometry(e_cluster, mantissa=mantissa)
417
417
  vertices = geo['vertices']
418
418
  if len(vertices) > 0:
419
- v_data = Plotly.DataByTopology(e_cluster,
420
- vertexColor=vertexColor,
421
- vertexColorKey=vertexColorKey,
422
- vertexSize=vertexSize,
423
- vertexSizeKey=vertexSizeKey,
424
- vertexLabelKey=vertexLabelKey,
425
- showVertexLabel=showVertexLabel,
426
- vertexGroupKey=vertexGroupKey,
427
- vertexMinGroup=vertexMinGroup,
428
- vertexMaxGroup=vertexMaxGroup,
429
- vertexGroups=vertexGroups,
430
- showVertexLegend=showVertexLegend,
431
- vertexLegendLabel=vertexLegendLabel,
432
- vertexLegendGroup=vertexLegendGroup,
433
- vertexLegendRank=vertexLegendRank,
434
- colorScale=colorScale)
435
-
436
- data += v_data
419
+ v_data = Plotly.vertexData(vertices,
420
+ dictionaries=v_dictionaries,
421
+ color=vertexColor,
422
+ colorKey=vertexColorKey,
423
+ size=vertexSize,
424
+ sizeKey=vertexSizeKey,
425
+ labelKey=vertexLabelKey,
426
+ showVertexLabel=showVertexLabel,
427
+ groupKey=vertexGroupKey,
428
+ minGroup=vertexMinGroup,
429
+ maxGroup=vertexMaxGroup,
430
+ groups=vertexGroups,
431
+ legendLabel=vertexLegendLabel,
432
+ legendGroup=vertexLegendGroup,
433
+ legendRank=vertexLegendRank,
434
+ showLegend=showVertexLegend,
435
+ colorScale=colorScale)
436
+
437
+ # v_data = Plotly.DataByTopology(e_cluster,
438
+ # vertexColor=vertexColor,
439
+ # vertexColorKey=vertexColorKey,
440
+ # vertexSize=vertexSize,
441
+ # vertexSizeKey=vertexSizeKey,
442
+ # vertexLabelKey=vertexLabelKey,
443
+ # showVertexLabel=showVertexLabel,
444
+ # vertexGroupKey=vertexGroupKey,
445
+ # vertexMinGroup=vertexMinGroup,
446
+ # vertexMaxGroup=vertexMaxGroup,
447
+ # vertexGroups=vertexGroups,
448
+ # showVertexLegend=showVertexLegend,
449
+ # vertexLegendLabel=vertexLegendLabel,
450
+ # vertexLegendGroup=vertexLegendGroup,
451
+ # vertexLegendRank=vertexLegendRank,
452
+ # colorScale=colorScale)
453
+ data += [v_data]
437
454
 
438
455
  if showEdges:
439
456
  e_dictionaries = []
@@ -501,7 +518,7 @@ class Plotly:
501
518
  z.append(v[2])
502
519
  colors.append(Color.AnyToHex(color))
503
520
  labels.append("Vertex_"+str(m+1).zfill(n))
504
- sizes.append(size)
521
+ sizes.append(max(size, 1.1))
505
522
  if len(dictionaries) > 0:
506
523
  d = dictionaries[m]
507
524
  if d:
@@ -512,7 +529,11 @@ class Plotly:
512
529
  if not labelKey == None:
513
530
  labels[m] = str(Dictionary.ValueAtKey(d, key=labelKey)) or labels[m]
514
531
  if not sizeKey == None:
515
- sizes[m] = Dictionary.ValueAtKey(d, key=sizeKey) or sizes[m]
532
+ sizes[m] = Dictionary.ValueAtKey(d, key=sizeKey)
533
+ if sizes[m] == None:
534
+ sizes[m] = size
535
+ if sizes[m] <= 0:
536
+ sizes[m] = 1.1
516
537
  if not groupKey == None:
517
538
  c_value = Dictionary.ValueAtKey(d, key=groupKey)
518
539
  if not c_value == None:
@@ -540,7 +561,7 @@ class Plotly:
540
561
  if len(labels) < 1:
541
562
  labels = "Vertex_1"
542
563
  if len(sizes) < 1:
543
- sizes = size
564
+ sizes = [size]*len(x)
544
565
  if showVertexLabel == True:
545
566
  mode = "markers+text"
546
567
  else:
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.7.90'
1
+ __version__ = '0.7.91'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: topologicpy
3
- Version: 0.7.90
3
+ Version: 0.7.91
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
@@ -10,14 +10,14 @@ topologicpy/DGL.py,sha256=Dd6O08D-vSxpjHYgKm45JpKiaeGvWlg1BRMzYMAXGNc,138991
10
10
  topologicpy/Dictionary.py,sha256=t0O7Du-iPq46FyKqZfcjHfsUK1E8GS_e67R2V5cpkbw,33186
11
11
  topologicpy/Edge.py,sha256=KWOJCkLDwCWyZJ5MKwDhT5umWwCYBHtLOz6ulHrSOfY,67205
12
12
  topologicpy/EnergyModel.py,sha256=AqTtmXE35SxvRXhG3vYAQd7GQDW-6HtjYPHua6ME4Eg,53762
13
- topologicpy/Face.py,sha256=f4DrZJ2AgwnTwIorJpwuOXtWq501wOMRixMgHZC78so,143843
14
- topologicpy/Graph.py,sha256=Zxmoy2FAl5m_MGUd62jDeajmZAFlKXVHf1Fy68TrsSw,436694
13
+ topologicpy/Face.py,sha256=2k1vSRK1M-s588RcqGHEalwIHJ9nT3qgx3pU4ktN0dU,150077
14
+ topologicpy/Graph.py,sha256=Qm0V17oeIlhr62faoYqJpNUMzAB5d0F-3zes4jCtRi8,460350
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=HfTaEV1R8K1xOVQQy9sBOhBTF_ap8A2RxZOYhirp_Mw,21835
18
18
  topologicpy/Matrix.py,sha256=umgR7An919-wGInXJ1wpqnoQ2jCPdyMe2rcWTZ16upk,8079
19
19
  topologicpy/Neo4j.py,sha256=t52hgE9cVsqkGc7m7fjRsLnyfRHakVHwdvF4ms7ow78,22342
20
- topologicpy/Plotly.py,sha256=Ld-VrYAn_FwONYwdVoz-7Kqt-5fs-qbVl35Utmcu7OE,114508
20
+ topologicpy/Plotly.py,sha256=Tvo0_zKVEHtPhsMNNvLy5G0HIys5FPAOyp_o4QN_I_A,115760
21
21
  topologicpy/Polyskel.py,sha256=EFsuh2EwQJGPLiFUjvtXmAwdX-A4r_DxP5hF7Qd3PaU,19829
22
22
  topologicpy/PyG.py,sha256=LU9LCCzjxGPUM31qbaJXZsTvniTtgugxJY7y612t4A4,109757
23
23
  topologicpy/Shell.py,sha256=UdDz3zfIYmGRjoZIseviJ2cXNtR5Kx5tIsZLhWMyO_U,87906
@@ -28,9 +28,9 @@ topologicpy/Vector.py,sha256=A1g83zDHep58iVPY8WQ8iHNrSOfGWFEzvVeDuMnjDNY,33078
28
28
  topologicpy/Vertex.py,sha256=sYWTbAHqKGRUAJRCIUqrCO_xFhvsXK09Sx7E4dafPLQ,73754
29
29
  topologicpy/Wire.py,sha256=HjagWKoJb8Z3zhgOij_4k6ZnKIl5gk8LletHbsT1ZKU,190632
30
30
  topologicpy/__init__.py,sha256=vlPCanUbxe5NifC4pHcnhSzkmmYcs_UrZrTlVMsxcFs,928
31
- topologicpy/version.py,sha256=mYCzbNDO5wSdD-Yv5YhtOF5hGlgyQ-E65V4XiSUOLnk,23
32
- topologicpy-0.7.90.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
- topologicpy-0.7.90.dist-info/METADATA,sha256=h8bGLDHNJXflEvDHHSsD0bDwwj39F_AvSqvvwVvi5Ec,10513
34
- topologicpy-0.7.90.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
35
- topologicpy-0.7.90.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
- topologicpy-0.7.90.dist-info/RECORD,,
31
+ topologicpy/version.py,sha256=em3O4PG8JKL9957EgCcS5zUXOhgyMhAa6bmjZHQ7UmE,23
32
+ topologicpy-0.7.91.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
+ topologicpy-0.7.91.dist-info/METADATA,sha256=m96FnumZC4Hy2QqaPuRryOZron2mQdjkGJvv5HJs3kM,10513
34
+ topologicpy-0.7.91.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
35
+ topologicpy-0.7.91.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
+ topologicpy-0.7.91.dist-info/RECORD,,