topologicpy 0.8.58__py3-none-any.whl → 0.8.61__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
@@ -64,20 +64,6 @@ except:
64
64
  print("Graph - tqdm library installed correctly.")
65
65
  except:
66
66
  warnings.warn("Graph - Error: Could not import tqdm.")
67
-
68
- try:
69
- from graphviz import Digraph
70
- except:
71
- print("Graph - Installing required graphviz library.")
72
- try:
73
- os.system("pip install graphviz")
74
- except:
75
- os.system("pip install graphviz --user")
76
- try:
77
- from graphviz import Digraph
78
- print("Graph - graphviz library installed correctly.")
79
- except:
80
- warnings.warn("Graph - Error: Could not import graphviz.")
81
67
 
82
68
  GraphQueueItem = namedtuple('GraphQueueItem', ['edges'])
83
69
 
@@ -1901,11 +1887,28 @@ class Graph:
1901
1887
  mantissa= mantissa)
1902
1888
  return bot_graph.serialize(format=format)
1903
1889
 
1890
+
1904
1891
  @staticmethod
1905
- def BetweennessCentrality(graph, method: str = "vertex", weightKey="length", normalize: bool = False, nxCompatible: bool = False, key: str = "betweenness_centrality", colorKey="bc_color", colorScale="viridis", mantissa: int = 6, tolerance: float = 0.001, silent: bool = False):
1892
+ def BetweennessCentrality(
1893
+ graph,
1894
+ method: str = "vertex",
1895
+ weightKey: str = "length",
1896
+ normalize: bool = False,
1897
+ nxCompatible: bool = False,
1898
+ key: str = "betweenness_centrality",
1899
+ colorKey: str = "bc_color",
1900
+ colorScale: str = "viridis",
1901
+ mantissa: int = 6,
1902
+ tolerance: float = 0.001,
1903
+ silent: bool = False
1904
+ ):
1906
1905
  """
1907
- Returns the betweenness centrality of the input graph. The order of the returned list is the same as the order of vertices/edges. See https://en.wikipedia.org/wiki/Betweenness_centrality.
1908
-
1906
+ Returns the betweenness centrality of the input graph. The order of the returned list is the same as the order of vertices/edges. See https://en.wikipedia.org/wiki/Betweenness_centrality.
1907
+ Optimized betweenness centrality (undirected) using Brandes:
1908
+ - Unweighted: O(VE) BFS per source
1909
+ - Weighted: Dijkstra-Brandes with binary heap
1910
+ - Vertex or Edge mode
1911
+ - Optional NetworkX-compatible normalization or 0..1 rescale
1909
1912
  Parameters
1910
1913
  ----------
1911
1914
  graph : topologic_core.Graph
@@ -1936,60 +1939,337 @@ class Graph:
1936
1939
  -------
1937
1940
  list
1938
1941
  The betweenness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
1939
-
1940
1942
  """
1941
- import warnings
1943
+ from collections import deque
1944
+ import math
1942
1945
 
1943
- try:
1944
- import networkx as nx
1945
- except:
1946
- print("Graph.BetwennessCentrality - Information: Installing required networkx library.")
1947
- try:
1948
- os.system("pip install networkx")
1949
- except:
1950
- os.system("pip install networkx --user")
1951
- try:
1952
- import networkx as nx
1953
- print("Graph.BetwennessCentrality - Infromation: networkx library installed correctly.")
1954
- except:
1955
- warnings.warn("Graph.BetwennessCentrality - Error: Could not import networkx. Please try to install networkx manually. Returning None.")
1956
- return None
1957
-
1946
+ from topologicpy.Topology import Topology
1958
1947
  from topologicpy.Dictionary import Dictionary
1959
1948
  from topologicpy.Color import Color
1960
- from topologicpy.Topology import Topology
1961
1949
  from topologicpy.Helper import Helper
1950
+ from topologicpy.Vertex import Vertex
1951
+ from topologicpy.Edge import Edge
1952
+ # We are inside Graph.* context; Graph.<...> methods available.
1953
+
1954
+ # ---------- validate ----------
1955
+ if not Topology.IsInstance(graph, "graph"):
1956
+ if not silent:
1957
+ print("Graph.BetweennessCentrality - Error: The input is not a valid Graph. Returning None.")
1958
+ return None
1959
+
1960
+ vertices = Graph.Vertices(graph)
1961
+ n = len(vertices)
1962
+ if n == 0:
1963
+ if not silent:
1964
+ print("Graph.BetweennessCentrality - Warning: Graph has no vertices. Returning [].")
1965
+ return []
1966
+
1967
+ method_l = (method or "vertex").lower()
1968
+ compute_edges = "edge" in method_l
1969
+
1970
+ # ---------- stable vertex indexing ----------
1971
+ def vkey(v, r=9):
1972
+ d = Topology.Dictionary(v)
1973
+ vid = Dictionary.ValueAtKey(d, "id")
1974
+ if vid is not None:
1975
+ return ("id", vid)
1976
+ return ("xyz", round(Vertex.X(v), r), round(Vertex.Y(v), r), round(Vertex.Z(v), r))
1977
+
1978
+ idx_of = {vkey(v): i for i, v in enumerate(vertices)}
1962
1979
 
1963
- if weightKey:
1964
- if "len" in weightKey.lower() or "dis" in weightKey.lower():
1980
+ # ---------- weight handling ----------
1981
+ dist_attr = None
1982
+ if isinstance(weightKey, str) and weightKey:
1983
+ wl = weightKey.lower()
1984
+ if ("len" in wl) or ("dis" in wl):
1965
1985
  weightKey = "length"
1966
- nx_graph = Graph.NetworkXGraph(graph)
1967
- if "vert" in method.lower():
1968
- elements = Graph.Vertices(graph)
1969
- elements_dict = nx.betweenness_centrality(nx_graph, normalized=normalize, weight=weightKey)
1970
- values = [round(value, mantissa) for value in list(elements_dict.values())]
1971
- else:
1972
- elements = Graph.Edges(graph)
1973
- elements_dict = nx.edge_betweenness_centrality(nx_graph, normalized=normalize, weight=weightKey)
1974
- values = [round(value, mantissa) for value in list(elements_dict.values())]
1975
- if nxCompatible == False:
1976
- if mantissa > 0: # We cannot have values in the range 0 to 1 with a mantissa < 1
1977
- values = [round(v, mantissa) for v in Helper.Normalize(values)]
1986
+ dist_attr = weightKey
1987
+
1988
+ def edge_weight(e):
1989
+ if dist_attr == "length":
1990
+ try:
1991
+ return float(Edge.Length(e))
1992
+ except Exception:
1993
+ return 1.0
1994
+ elif dist_attr:
1995
+ try:
1996
+ d = Topology.Dictionary(e)
1997
+ w = Dictionary.ValueAtKey(d, dist_attr)
1998
+ return float(w) if (w is not None) else 1.0
1999
+ except Exception:
2000
+ return 1.0
1978
2001
  else:
1979
- values = Helper.Normalize(values)
1980
- min_value = 0
1981
- max_value = 1
2002
+ return 1.0
2003
+
2004
+ # ---------- build undirected adjacency (min weight on multi-edges) ----------
2005
+ edges = Graph.Edges(graph)
2006
+ # For per-edge outputs in input order:
2007
+ edge_end_idx = [] # [(iu, iv)] aligned with edges list (undirected as sorted pair)
2008
+ tmp_adj = [dict() for _ in range(n)] # temporary: dedup by neighbor with min weight
2009
+
2010
+ for e in edges:
2011
+ try:
2012
+ u = Edge.StartVertex(e)
2013
+ v = Edge.EndVertex(e)
2014
+ except Exception:
2015
+ continue
2016
+ iu = idx_of.get(vkey(u))
2017
+ iv = idx_of.get(vkey(v))
2018
+ if iu is None or iv is None or iu == iv:
2019
+ # still store mapping for return list to avoid index error
2020
+ pair = None
2021
+ else:
2022
+ w = edge_weight(e)
2023
+ # keep minimal weight for duplicates
2024
+ pu = tmp_adj[iu].get(iv)
2025
+ if (pu is None) or (w < pu):
2026
+ tmp_adj[iu][iv] = w
2027
+ tmp_adj[iv][iu] = w
2028
+ pair = (iu, iv) if iu < iv else (iv, iu)
2029
+ edge_end_idx.append(pair)
2030
+
2031
+ # finalize adjacency as list-of-tuples for fast loops
2032
+ adj = [list(neigh.items()) for neigh in tmp_adj] # adj[i] = [(j, w), ...]
2033
+ del tmp_adj
2034
+
2035
+ # detect weightedness
2036
+ weighted = False
2037
+ for i in range(n):
2038
+ if any(abs(w - 1.0) > 1e-12 for _, w in adj[i]):
2039
+ weighted = True
2040
+ break
2041
+
2042
+ # ---------- Brandes ----------
2043
+ CB_v = [0.0] * n
2044
+ CB_e = {} # key: (min_i, max_j) -> score (only if compute_edges)
2045
+
2046
+ if n > 1:
2047
+ if not weighted:
2048
+ # Unweighted BFS Brandes
2049
+ for s in range(n):
2050
+ S = []
2051
+ P = [[] for _ in range(n)]
2052
+ sigma = [0.0] * n
2053
+ sigma[s] = 1.0
2054
+ dist = [-1] * n
2055
+ dist[s] = 0
2056
+ Q = deque([s])
2057
+ pushQ, popQ = Q.append, Q.popleft
2058
+
2059
+ while Q:
2060
+ v = popQ()
2061
+ S.append(v)
2062
+ dv = dist[v]
2063
+ sv = sigma[v]
2064
+ for w, _ in adj[v]:
2065
+ if dist[w] < 0:
2066
+ dist[w] = dv + 1
2067
+ pushQ(w)
2068
+ if dist[w] == dv + 1:
2069
+ sigma[w] += sv
2070
+ P[w].append(v)
2071
+
2072
+ delta = [0.0] * n
2073
+ while S:
2074
+ w = S.pop()
2075
+ sw = sigma[w]
2076
+ dw = 1.0 + delta[w]
2077
+ for v in P[w]:
2078
+ c = (sigma[v] / sw) * dw
2079
+ delta[v] += c
2080
+ if compute_edges:
2081
+ a, b = (v, w) if v < w else (w, v)
2082
+ CB_e[a, b] = CB_e.get((a, b), 0.0) + c
2083
+ if w != s:
2084
+ CB_v[w] += delta[w]
2085
+ else:
2086
+ # Weighted Dijkstra-Brandes
2087
+ import heapq
2088
+ EPS = 1e-12
2089
+ for s in range(n):
2090
+ S = []
2091
+ P = [[] for _ in range(n)]
2092
+ sigma = [0.0] * n
2093
+ sigma[s] = 1.0
2094
+ dist = [math.inf] * n
2095
+ dist[s] = 0.0
2096
+ H = [(0.0, s)]
2097
+ pushH, popH = heapq.heappush, heapq.heappop
2098
+
2099
+ while H:
2100
+ dv, v = popH(H)
2101
+ if dv > dist[v] + EPS:
2102
+ continue
2103
+ S.append(v)
2104
+ sv = sigma[v]
2105
+ for w, wgt in adj[v]:
2106
+ nd = dv + wgt
2107
+ dw = dist[w]
2108
+ if nd + EPS < dw:
2109
+ dist[w] = nd
2110
+ sigma[w] = sv
2111
+ P[w] = [v]
2112
+ pushH(H, (nd, w))
2113
+ elif abs(nd - dw) <= EPS:
2114
+ sigma[w] += sv
2115
+ P[w].append(v)
2116
+
2117
+ delta = [0.0] * n
2118
+ while S:
2119
+ w = S.pop()
2120
+ sw = sigma[w]
2121
+ if sw == 0.0:
2122
+ continue
2123
+ dw = 1.0 + delta[w]
2124
+ for v in P[w]:
2125
+ c = (sigma[v] / sw) * dw
2126
+ delta[v] += c
2127
+ if compute_edges:
2128
+ a, b = (v, w) if v < w else (w, v)
2129
+ CB_e[a, b] = CB_e.get((a, b), 0.0) + c
2130
+ if w != s:
2131
+ CB_v[w] += delta[w]
2132
+
2133
+ # ---------- normalization ----------
2134
+ # NetworkX-compatible normalization (undirected):
2135
+ # vertices/edges factor = 2/((n-1)(n-2)) for n > 2 when normalized=True
2136
+ if nxCompatible:
2137
+ if normalize and n > 2:
2138
+ scale = 2.0 / ((n - 1) * (n - 2))
2139
+ CB_v = [v * scale for v in CB_v]
2140
+ if compute_edges:
2141
+ for k in list(CB_e.keys()):
2142
+ CB_e[k] *= scale
2143
+ # else: leave raw Brandes scores (normalized=False behavior)
2144
+ values_raw = CB_v if not compute_edges else [
2145
+ CB_e.get(tuple(sorted(pair)) if pair else None, 0.0) if pair else 0.0
2146
+ for pair in edge_end_idx
2147
+ ]
2148
+ values_for_return = values_raw
1982
2149
  else:
1983
- min_value = min(values)
1984
- max_value = max(values)
2150
+ # Rescale to [0,1] regardless of theoretical normalization
2151
+ values_raw = CB_v if not compute_edges else [
2152
+ CB_e.get(tuple(sorted(pair)) if pair else None, 0.0) if pair else 0.0
2153
+ for pair in edge_end_idx
2154
+ ]
2155
+ values_for_return = Helper.Normalize(values_raw)
1985
2156
 
1986
- for i, value in enumerate(values):
1987
- d = Topology.Dictionary(elements[i])
1988
- color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
1989
- d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
1990
- elements[i] = Topology.SetDictionary(elements[i], d)
2157
+ # rounding once
2158
+ if mantissa is not None and mantissa >= 0:
2159
+ values_for_return = [round(v, mantissa) for v in values_for_return]
1991
2160
 
1992
- return values
2161
+ # ---------- color mapping ----------
2162
+ if values_for_return:
2163
+ min_v, max_v = min(values_for_return), max(values_for_return)
2164
+ else:
2165
+ min_v, max_v = 0.0, 1.0
2166
+ if abs(max_v - min_v) < tolerance:
2167
+ max_v = min_v + tolerance
2168
+
2169
+ # annotate (vertices or edges) in input order
2170
+ if compute_edges:
2171
+ elems = edges
2172
+ else:
2173
+ elems = vertices
2174
+ for i, value in enumerate(values_for_return):
2175
+ d = Topology.Dictionary(elems[i])
2176
+ color_hex = Color.AnyToHex(
2177
+ Color.ByValueInRange(value, minValue=min_v, maxValue=max_v, colorScale=colorScale)
2178
+ )
2179
+ d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color_hex])
2180
+ elems[i] = Topology.SetDictionary(elems[i], d)
2181
+
2182
+ return values_for_return
2183
+
2184
+ # @staticmethod
2185
+ # def BetweennessCentrality_old(graph, method: str = "vertex", weightKey="length", normalize: bool = False, nxCompatible: bool = False, key: str = "betweenness_centrality", colorKey="bc_color", colorScale="viridis", mantissa: int = 6, tolerance: float = 0.001, silent: bool = False):
2186
+ # """
2187
+ # Returns the betweenness centrality of the input graph. The order of the returned list is the same as the order of vertices/edges. See https://en.wikipedia.org/wiki/Betweenness_centrality.
2188
+
2189
+ # Parameters
2190
+ # ----------
2191
+ # graph : topologic_core.Graph
2192
+ # The input graph.
2193
+ # method : str , optional
2194
+ # The method of computing the betweenness centrality. The options are "vertex" or "edge". Default is "vertex".
2195
+ # weightKey : str , optional
2196
+ # If specified, the value in the connected edges' dictionary specified by the weightKey string will be aggregated to calculate
2197
+ # the shortest path. If a numeric value cannot be retrieved from an edge, a value of 1 is used instead.
2198
+ # This is used in weighted graphs. if weightKey is set to "Length" or "Distance", the length of the edge will be used as its weight.
2199
+ # normalize : bool , optional
2200
+ # If set to True, the values are normalized to be in the range 0 to 1. Otherwise they are not. Default is False.
2201
+ # nxCompatible : bool , optional
2202
+ # If set to True, and normalize input parameter is also set to True, the values are set to be identical to NetworkX values. Otherwise, they are normalized between 0 and 1. Default is False.
2203
+ # key : str , optional
2204
+ # The desired dictionary key under which to store the betweenness centrality score. Default is "betweenness_centrality".
2205
+ # colorKey : str , optional
2206
+ # The desired dictionary key under which to store the betweenness centrality color. Default is "betweenness_centrality".
2207
+ # colorScale : str , optional
2208
+ # The desired type of plotly color scales to use (e.g. "viridis", "plasma"). Default is "viridis". For a full list of names, see https://plotly.com/python/builtin-colorscales/.
2209
+ # In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
2210
+ # mantissa : int , optional
2211
+ # The number of decimal places to round the result to. Default is 6.
2212
+ # tolerance : float , optional
2213
+ # The desired tolerance. Default is 0.0001.
2214
+
2215
+ # Returns
2216
+ # -------
2217
+ # list
2218
+ # The betweenness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
2219
+
2220
+ # """
2221
+ # import warnings
2222
+
2223
+ # try:
2224
+ # import networkx as nx
2225
+ # except:
2226
+ # print("Graph.BetwennessCentrality - Information: Installing required networkx library.")
2227
+ # try:
2228
+ # os.system("pip install networkx")
2229
+ # except:
2230
+ # os.system("pip install networkx --user")
2231
+ # try:
2232
+ # import networkx as nx
2233
+ # print("Graph.BetwennessCentrality - Infromation: networkx library installed correctly.")
2234
+ # except:
2235
+ # warnings.warn("Graph.BetwennessCentrality - Error: Could not import networkx. Please try to install networkx manually. Returning None.")
2236
+ # return None
2237
+
2238
+ # from topologicpy.Dictionary import Dictionary
2239
+ # from topologicpy.Color import Color
2240
+ # from topologicpy.Topology import Topology
2241
+ # from topologicpy.Helper import Helper
2242
+
2243
+ # if weightKey:
2244
+ # if "len" in weightKey.lower() or "dis" in weightKey.lower():
2245
+ # weightKey = "length"
2246
+ # nx_graph = Graph.NetworkXGraph(graph)
2247
+ # if "vert" in method.lower():
2248
+ # elements = Graph.Vertices(graph)
2249
+ # elements_dict = nx.betweenness_centrality(nx_graph, normalized=normalize, weight=weightKey)
2250
+ # values = [round(value, mantissa) for value in list(elements_dict.values())]
2251
+ # else:
2252
+ # elements = Graph.Edges(graph)
2253
+ # elements_dict = nx.edge_betweenness_centrality(nx_graph, normalized=normalize, weight=weightKey)
2254
+ # values = [round(value, mantissa) for value in list(elements_dict.values())]
2255
+ # if nxCompatible == False:
2256
+ # if mantissa > 0: # We cannot have values in the range 0 to 1 with a mantissa < 1
2257
+ # values = [round(v, mantissa) for v in Helper.Normalize(values)]
2258
+ # else:
2259
+ # values = Helper.Normalize(values)
2260
+ # min_value = 0
2261
+ # max_value = 1
2262
+ # else:
2263
+ # min_value = min(values)
2264
+ # max_value = max(values)
2265
+
2266
+ # for i, value in enumerate(values):
2267
+ # d = Topology.Dictionary(elements[i])
2268
+ # color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
2269
+ # d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
2270
+ # elements[i] = Topology.SetDictionary(elements[i], d)
2271
+
2272
+ # return values
1993
2273
 
1994
2274
  @staticmethod
1995
2275
  def BetweennessPartition(graph, n=2, m=10, key="partition", tolerance=0.0001, silent=False):
@@ -3246,432 +3526,432 @@ class Graph:
3246
3526
  edges = get_edges(relationships, vertices)
3247
3527
  return Graph.ByVerticesEdges(vertices, edges)
3248
3528
 
3249
- @staticmethod
3250
- def ByIFCFile_old(file,
3251
- includeTypes: list = [],
3252
- excludeTypes: list = [],
3253
- includeRels: list = [],
3254
- excludeRels: list = [],
3255
- transferDictionaries: bool = False,
3256
- useInternalVertex: bool = False,
3257
- storeBREP: bool = False,
3258
- removeCoplanarFaces: bool = False,
3259
- xMin: float = -0.5, yMin: float = -0.5, zMin: float = -0.5,
3260
- xMax: float = 0.5, yMax: float = 0.5, zMax: float = 0.5,
3261
- tolerance: float = 0.0001):
3262
- """
3263
- Create a Graph from an IFC file. This code is partially based on code from Bruno Postle.
3529
+ # @staticmethod
3530
+ # def ByIFCFile_old(file,
3531
+ # includeTypes: list = [],
3532
+ # excludeTypes: list = [],
3533
+ # includeRels: list = [],
3534
+ # excludeRels: list = [],
3535
+ # transferDictionaries: bool = False,
3536
+ # useInternalVertex: bool = False,
3537
+ # storeBREP: bool = False,
3538
+ # removeCoplanarFaces: bool = False,
3539
+ # xMin: float = -0.5, yMin: float = -0.5, zMin: float = -0.5,
3540
+ # xMax: float = 0.5, yMax: float = 0.5, zMax: float = 0.5,
3541
+ # tolerance: float = 0.0001):
3542
+ # """
3543
+ # Create a Graph from an IFC file. This code is partially based on code from Bruno Postle.
3264
3544
 
3265
- Parameters
3266
- ----------
3267
- file : file
3268
- The input IFC file
3269
- includeTypes : list , optional
3270
- A list of IFC object types to include in the graph. Default is [] which means all object types are included.
3271
- excludeTypes : list , optional
3272
- A list of IFC object types to exclude from the graph. Default is [] which mean no object type is excluded.
3273
- includeRels : list , optional
3274
- A list of IFC relationship types to include in the graph. Default is [] which means all relationship types are included.
3275
- excludeRels : list , optional
3276
- A list of IFC relationship types to exclude from the graph. Default is [] which mean no relationship type is excluded.
3277
- transferDictionaries : bool , optional
3278
- If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. Default is False.
3279
- useInternalVertex : bool , optional
3280
- If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. Default is False.
3281
- storeBREP : bool , optional
3282
- If set to True, store the BRep of the subtopology in its representative vertex. Default is False.
3283
- removeCoplanarFaces : bool , optional
3284
- If set to True, coplanar faces are removed. Otherwise they are not. Default is False.
3285
- xMin : float, optional
3286
- The desired minimum value to assign for a vertex's X coordinate. Default is -0.5.
3287
- yMin : float, optional
3288
- The desired minimum value to assign for a vertex's Y coordinate. Default is -0.5.
3289
- zMin : float, optional
3290
- The desired minimum value to assign for a vertex's Z coordinate. Default is -0.5.
3291
- xMax : float, optional
3292
- The desired maximum value to assign for a vertex's X coordinate. Default is 0.5.
3293
- yMax : float, optional
3294
- The desired maximum value to assign for a vertex's Y coordinate. Default is 0.5.
3295
- zMax : float, optional
3296
- The desired maximum value to assign for a vertex's Z coordinate. Default is 0.5.
3297
- tolerance : float , optional
3298
- The desired tolerance. Default is 0.0001.
3545
+ # Parameters
3546
+ # ----------
3547
+ # file : file
3548
+ # The input IFC file
3549
+ # includeTypes : list , optional
3550
+ # A list of IFC object types to include in the graph. Default is [] which means all object types are included.
3551
+ # excludeTypes : list , optional
3552
+ # A list of IFC object types to exclude from the graph. Default is [] which mean no object type is excluded.
3553
+ # includeRels : list , optional
3554
+ # A list of IFC relationship types to include in the graph. Default is [] which means all relationship types are included.
3555
+ # excludeRels : list , optional
3556
+ # A list of IFC relationship types to exclude from the graph. Default is [] which mean no relationship type is excluded.
3557
+ # transferDictionaries : bool , optional
3558
+ # If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. Default is False.
3559
+ # useInternalVertex : bool , optional
3560
+ # If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. Default is False.
3561
+ # storeBREP : bool , optional
3562
+ # If set to True, store the BRep of the subtopology in its representative vertex. Default is False.
3563
+ # removeCoplanarFaces : bool , optional
3564
+ # If set to True, coplanar faces are removed. Otherwise they are not. Default is False.
3565
+ # xMin : float, optional
3566
+ # The desired minimum value to assign for a vertex's X coordinate. Default is -0.5.
3567
+ # yMin : float, optional
3568
+ # The desired minimum value to assign for a vertex's Y coordinate. Default is -0.5.
3569
+ # zMin : float, optional
3570
+ # The desired minimum value to assign for a vertex's Z coordinate. Default is -0.5.
3571
+ # xMax : float, optional
3572
+ # The desired maximum value to assign for a vertex's X coordinate. Default is 0.5.
3573
+ # yMax : float, optional
3574
+ # The desired maximum value to assign for a vertex's Y coordinate. Default is 0.5.
3575
+ # zMax : float, optional
3576
+ # The desired maximum value to assign for a vertex's Z coordinate. Default is 0.5.
3577
+ # tolerance : float , optional
3578
+ # The desired tolerance. Default is 0.0001.
3299
3579
 
3300
- Returns
3301
- -------
3302
- topologic_core.Graph
3303
- The created graph.
3580
+ # Returns
3581
+ # -------
3582
+ # topologic_core.Graph
3583
+ # The created graph.
3304
3584
 
3305
- """
3306
- from topologicpy.Topology import Topology
3307
- from topologicpy.Vertex import Vertex
3308
- from topologicpy.Edge import Edge
3309
- from topologicpy.Graph import Graph
3310
- from topologicpy.Dictionary import Dictionary
3311
- try:
3312
- import ifcopenshell
3313
- import ifcopenshell.util.placement
3314
- import ifcopenshell.util.element
3315
- import ifcopenshell.util.shape
3316
- import ifcopenshell.geom
3317
- except:
3318
- print("Graph.ByIFCFile - Warning: Installing required ifcopenshell library.")
3319
- try:
3320
- os.system("pip install ifcopenshell")
3321
- except:
3322
- os.system("pip install ifcopenshell --user")
3323
- try:
3324
- import ifcopenshell
3325
- import ifcopenshell.util.placement
3326
- import ifcopenshell.util.element
3327
- import ifcopenshell.util.shape
3328
- import ifcopenshell.geom
3329
- print("Graph.ByIFCFile - Warning: ifcopenshell library installed correctly.")
3330
- except:
3331
- warnings.warn("Graph.ByIFCFile - Error: Could not import ifcopenshell. Please try to install ifcopenshell manually. Returning None.")
3332
- return None
3585
+ # """
3586
+ # from topologicpy.Topology import Topology
3587
+ # from topologicpy.Vertex import Vertex
3588
+ # from topologicpy.Edge import Edge
3589
+ # from topologicpy.Graph import Graph
3590
+ # from topologicpy.Dictionary import Dictionary
3591
+ # try:
3592
+ # import ifcopenshell
3593
+ # import ifcopenshell.util.placement
3594
+ # import ifcopenshell.util.element
3595
+ # import ifcopenshell.util.shape
3596
+ # import ifcopenshell.geom
3597
+ # except:
3598
+ # print("Graph.ByIFCFile - Warning: Installing required ifcopenshell library.")
3599
+ # try:
3600
+ # os.system("pip install ifcopenshell")
3601
+ # except:
3602
+ # os.system("pip install ifcopenshell --user")
3603
+ # try:
3604
+ # import ifcopenshell
3605
+ # import ifcopenshell.util.placement
3606
+ # import ifcopenshell.util.element
3607
+ # import ifcopenshell.util.shape
3608
+ # import ifcopenshell.geom
3609
+ # print("Graph.ByIFCFile - Warning: ifcopenshell library installed correctly.")
3610
+ # except:
3611
+ # warnings.warn("Graph.ByIFCFile - Error: Could not import ifcopenshell. Please try to install ifcopenshell manually. Returning None.")
3612
+ # return None
3333
3613
 
3334
- import random
3335
-
3336
- def vertexAtKeyValue(vertices, key, value):
3337
- for v in vertices:
3338
- d = Topology.Dictionary(v)
3339
- d_value = Dictionary.ValueAtKey(d, key)
3340
- if value == d_value:
3341
- return v
3342
- return None
3343
-
3344
- def IFCObjects(ifc_file, include=[], exclude=[]):
3345
- include = [s.lower() for s in include]
3346
- exclude = [s.lower() for s in exclude]
3347
- all_objects = ifc_file.by_type('IfcProduct')
3348
- return_objects = []
3349
- for obj in all_objects:
3350
- is_a = obj.is_a().lower()
3351
- if is_a in exclude:
3352
- continue
3353
- if is_a in include or len(include) == 0:
3354
- return_objects.append(obj)
3355
- return return_objects
3356
-
3357
- def IFCObjectTypes(ifc_file):
3358
- products = IFCObjects(ifc_file)
3359
- obj_types = []
3360
- for product in products:
3361
- obj_types.append(product.is_a())
3362
- obj_types = list(set(obj_types))
3363
- obj_types.sort()
3364
- return obj_types
3365
-
3366
- def IFCRelationshipTypes(ifc_file):
3367
- rel_types = [ifc_rel.is_a() for ifc_rel in ifc_file.by_type("IfcRelationship")]
3368
- rel_types = list(set(rel_types))
3369
- rel_types.sort()
3370
- return rel_types
3371
-
3372
- def IFCRelationships(ifc_file, include=[], exclude=[]):
3373
- include = [s.lower() for s in include]
3374
- exclude = [s.lower() for s in exclude]
3375
- rel_types = [ifc_rel.is_a() for ifc_rel in ifc_file.by_type("IfcRelationship")]
3376
- rel_types = list(set(rel_types))
3377
- relationships = []
3378
- for ifc_rel in ifc_file.by_type("IfcRelationship"):
3379
- rel_type = ifc_rel.is_a().lower()
3380
- if rel_type in exclude:
3381
- continue
3382
- if rel_type in include or len(include) == 0:
3383
- relationships.append(ifc_rel)
3384
- return relationships
3614
+ # import random
3615
+
3616
+ # def vertexAtKeyValue(vertices, key, value):
3617
+ # for v in vertices:
3618
+ # d = Topology.Dictionary(v)
3619
+ # d_value = Dictionary.ValueAtKey(d, key)
3620
+ # if value == d_value:
3621
+ # return v
3622
+ # return None
3385
3623
 
3386
- def get_psets(entity):
3387
- # Initialize the PSET dictionary for this entity
3388
- psets = {}
3624
+ # def IFCObjects(ifc_file, include=[], exclude=[]):
3625
+ # include = [s.lower() for s in include]
3626
+ # exclude = [s.lower() for s in exclude]
3627
+ # all_objects = ifc_file.by_type('IfcProduct')
3628
+ # return_objects = []
3629
+ # for obj in all_objects:
3630
+ # is_a = obj.is_a().lower()
3631
+ # if is_a in exclude:
3632
+ # continue
3633
+ # if is_a in include or len(include) == 0:
3634
+ # return_objects.append(obj)
3635
+ # return return_objects
3636
+
3637
+ # def IFCObjectTypes(ifc_file):
3638
+ # products = IFCObjects(ifc_file)
3639
+ # obj_types = []
3640
+ # for product in products:
3641
+ # obj_types.append(product.is_a())
3642
+ # obj_types = list(set(obj_types))
3643
+ # obj_types.sort()
3644
+ # return obj_types
3645
+
3646
+ # def IFCRelationshipTypes(ifc_file):
3647
+ # rel_types = [ifc_rel.is_a() for ifc_rel in ifc_file.by_type("IfcRelationship")]
3648
+ # rel_types = list(set(rel_types))
3649
+ # rel_types.sort()
3650
+ # return rel_types
3651
+
3652
+ # def IFCRelationships(ifc_file, include=[], exclude=[]):
3653
+ # include = [s.lower() for s in include]
3654
+ # exclude = [s.lower() for s in exclude]
3655
+ # rel_types = [ifc_rel.is_a() for ifc_rel in ifc_file.by_type("IfcRelationship")]
3656
+ # rel_types = list(set(rel_types))
3657
+ # relationships = []
3658
+ # for ifc_rel in ifc_file.by_type("IfcRelationship"):
3659
+ # rel_type = ifc_rel.is_a().lower()
3660
+ # if rel_type in exclude:
3661
+ # continue
3662
+ # if rel_type in include or len(include) == 0:
3663
+ # relationships.append(ifc_rel)
3664
+ # return relationships
3665
+
3666
+ # def get_psets(entity):
3667
+ # # Initialize the PSET dictionary for this entity
3668
+ # psets = {}
3389
3669
 
3390
- # Check if the entity has a GlobalId
3391
- if not hasattr(entity, 'GlobalId'):
3392
- raise ValueError("The provided entity does not have a GlobalId.")
3670
+ # # Check if the entity has a GlobalId
3671
+ # if not hasattr(entity, 'GlobalId'):
3672
+ # raise ValueError("The provided entity does not have a GlobalId.")
3393
3673
 
3394
- # Get the property sets related to this entity
3395
- for definition in entity.IsDefinedBy:
3396
- if definition.is_a('IfcRelDefinesByProperties'):
3397
- property_set = definition.RelatingPropertyDefinition
3674
+ # # Get the property sets related to this entity
3675
+ # for definition in entity.IsDefinedBy:
3676
+ # if definition.is_a('IfcRelDefinesByProperties'):
3677
+ # property_set = definition.RelatingPropertyDefinition
3398
3678
 
3399
- # Check if it is a property set
3400
- if not property_set == None:
3401
- if property_set.is_a('IfcPropertySet'):
3402
- pset_name = "IFC_"+property_set.Name
3679
+ # # Check if it is a property set
3680
+ # if not property_set == None:
3681
+ # if property_set.is_a('IfcPropertySet'):
3682
+ # pset_name = "IFC_"+property_set.Name
3403
3683
 
3404
- # Dictionary to hold individual properties
3405
- properties = {}
3684
+ # # Dictionary to hold individual properties
3685
+ # properties = {}
3406
3686
 
3407
- # Iterate over the properties in the PSET
3408
- for prop in property_set.HasProperties:
3409
- if prop.is_a('IfcPropertySingleValue'):
3410
- # Get the property name and value
3411
- prop_name = "IFC_"+prop.Name
3412
- prop_value = prop.NominalValue.wrappedValue if prop.NominalValue else None
3413
- properties[prop_name] = prop_value
3687
+ # # Iterate over the properties in the PSET
3688
+ # for prop in property_set.HasProperties:
3689
+ # if prop.is_a('IfcPropertySingleValue'):
3690
+ # # Get the property name and value
3691
+ # prop_name = "IFC_"+prop.Name
3692
+ # prop_value = prop.NominalValue.wrappedValue if prop.NominalValue else None
3693
+ # properties[prop_name] = prop_value
3414
3694
 
3415
- # Add this PSET to the dictionary for this entity
3416
- psets[pset_name] = properties
3417
- return psets
3695
+ # # Add this PSET to the dictionary for this entity
3696
+ # psets[pset_name] = properties
3697
+ # return psets
3418
3698
 
3419
- def get_color_transparency_material(entity):
3420
- import random
3699
+ # def get_color_transparency_material(entity):
3700
+ # import random
3421
3701
 
3422
- # Set default Material Name and ID
3423
- material_list = []
3424
- # Set default transparency based on entity type or material
3425
- default_transparency = 0.0
3702
+ # # Set default Material Name and ID
3703
+ # material_list = []
3704
+ # # Set default transparency based on entity type or material
3705
+ # default_transparency = 0.0
3426
3706
 
3427
- # Check if the entity is an opening or made of glass
3428
- is_a = entity.is_a().lower()
3429
- if "opening" in is_a or "window" in is_a or "door" in is_a or "space" in is_a:
3430
- default_transparency = 0.7
3431
- elif "space" in is_a:
3432
- default_transparency = 0.8
3707
+ # # Check if the entity is an opening or made of glass
3708
+ # is_a = entity.is_a().lower()
3709
+ # if "opening" in is_a or "window" in is_a or "door" in is_a or "space" in is_a:
3710
+ # default_transparency = 0.7
3711
+ # elif "space" in is_a:
3712
+ # default_transparency = 0.8
3433
3713
 
3434
- # Check if the entity has constituent materials (e.g., glass)
3435
- else:
3436
- # Check for associated materials (ConstituentMaterial or direct material assignment)
3437
- materials_checked = False
3438
- if hasattr(entity, 'HasAssociations'):
3439
- for rel in entity.HasAssociations:
3440
- if rel.is_a('IfcRelAssociatesMaterial'):
3441
- material = rel.RelatingMaterial
3442
- if material.is_a('IfcMaterial') and 'glass' in material.Name.lower():
3443
- default_transparency = 0.5
3444
- materials_checked = True
3445
- elif material.is_a('IfcMaterialLayerSetUsage'):
3446
- material_layers = material.ForLayerSet.MaterialLayers
3447
- for layer in material_layers:
3448
- material_list.append(layer.Material.Name)
3449
- if 'glass' in layer.Material.Name.lower():
3450
- default_transparency = 0.5
3451
- materials_checked = True
3714
+ # # Check if the entity has constituent materials (e.g., glass)
3715
+ # else:
3716
+ # # Check for associated materials (ConstituentMaterial or direct material assignment)
3717
+ # materials_checked = False
3718
+ # if hasattr(entity, 'HasAssociations'):
3719
+ # for rel in entity.HasAssociations:
3720
+ # if rel.is_a('IfcRelAssociatesMaterial'):
3721
+ # material = rel.RelatingMaterial
3722
+ # if material.is_a('IfcMaterial') and 'glass' in material.Name.lower():
3723
+ # default_transparency = 0.5
3724
+ # materials_checked = True
3725
+ # elif material.is_a('IfcMaterialLayerSetUsage'):
3726
+ # material_layers = material.ForLayerSet.MaterialLayers
3727
+ # for layer in material_layers:
3728
+ # material_list.append(layer.Material.Name)
3729
+ # if 'glass' in layer.Material.Name.lower():
3730
+ # default_transparency = 0.5
3731
+ # materials_checked = True
3452
3732
 
3453
- # Check for ConstituentMaterial if available
3454
- if hasattr(entity, 'HasAssociations') and not materials_checked:
3455
- for rel in entity.HasAssociations:
3456
- if rel.is_a('IfcRelAssociatesMaterial'):
3457
- material = rel.RelatingMaterial
3458
- if material.is_a('IfcMaterialConstituentSet'):
3459
- for constituent in material.MaterialConstituents:
3460
- material_list.append(constituent.Material.Name)
3461
- if 'glass' in constituent.Material.Name.lower():
3462
- default_transparency = 0.5
3463
- materials_checked = True
3464
-
3465
- # Check if the entity has ShapeAspects with associated materials or styles
3466
- if hasattr(entity, 'HasShapeAspects') and not materials_checked:
3467
- for shape_aspect in entity.HasShapeAspects:
3468
- if hasattr(shape_aspect, 'StyledByItem') and shape_aspect.StyledByItem:
3469
- for styled_item in shape_aspect.StyledByItem:
3470
- for style in styled_item.Styles:
3471
- if style.is_a('IfcSurfaceStyle'):
3472
- for surface_style in style.Styles:
3473
- if surface_style.is_a('IfcSurfaceStyleRendering'):
3474
- transparency = getattr(surface_style, 'Transparency', default_transparency)
3475
- if transparency > 0:
3476
- default_transparency = transparency
3477
-
3478
- # Try to get the actual color and transparency if defined
3479
- if hasattr(entity, 'Representation') and entity.Representation:
3480
- for rep in entity.Representation.Representations:
3481
- for item in rep.Items:
3482
- if hasattr(item, 'StyledByItem') and item.StyledByItem:
3483
- for styled_item in item.StyledByItem:
3484
- if hasattr(styled_item, 'Styles'):
3485
- for style in styled_item.Styles:
3486
- if style.is_a('IfcSurfaceStyle'):
3487
- for surface_style in style.Styles:
3488
- if surface_style.is_a('IfcSurfaceStyleRendering'):
3489
- color = surface_style.SurfaceColour
3490
- transparency = getattr(surface_style, 'Transparency', default_transparency)
3491
- return (color.Red*255, color.Green*255, color.Blue*255), transparency, material_list
3733
+ # # Check for ConstituentMaterial if available
3734
+ # if hasattr(entity, 'HasAssociations') and not materials_checked:
3735
+ # for rel in entity.HasAssociations:
3736
+ # if rel.is_a('IfcRelAssociatesMaterial'):
3737
+ # material = rel.RelatingMaterial
3738
+ # if material.is_a('IfcMaterialConstituentSet'):
3739
+ # for constituent in material.MaterialConstituents:
3740
+ # material_list.append(constituent.Material.Name)
3741
+ # if 'glass' in constituent.Material.Name.lower():
3742
+ # default_transparency = 0.5
3743
+ # materials_checked = True
3744
+
3745
+ # # Check if the entity has ShapeAspects with associated materials or styles
3746
+ # if hasattr(entity, 'HasShapeAspects') and not materials_checked:
3747
+ # for shape_aspect in entity.HasShapeAspects:
3748
+ # if hasattr(shape_aspect, 'StyledByItem') and shape_aspect.StyledByItem:
3749
+ # for styled_item in shape_aspect.StyledByItem:
3750
+ # for style in styled_item.Styles:
3751
+ # if style.is_a('IfcSurfaceStyle'):
3752
+ # for surface_style in style.Styles:
3753
+ # if surface_style.is_a('IfcSurfaceStyleRendering'):
3754
+ # transparency = getattr(surface_style, 'Transparency', default_transparency)
3755
+ # if transparency > 0:
3756
+ # default_transparency = transparency
3757
+
3758
+ # # Try to get the actual color and transparency if defined
3759
+ # if hasattr(entity, 'Representation') and entity.Representation:
3760
+ # for rep in entity.Representation.Representations:
3761
+ # for item in rep.Items:
3762
+ # if hasattr(item, 'StyledByItem') and item.StyledByItem:
3763
+ # for styled_item in item.StyledByItem:
3764
+ # if hasattr(styled_item, 'Styles'):
3765
+ # for style in styled_item.Styles:
3766
+ # if style.is_a('IfcSurfaceStyle'):
3767
+ # for surface_style in style.Styles:
3768
+ # if surface_style.is_a('IfcSurfaceStyleRendering'):
3769
+ # color = surface_style.SurfaceColour
3770
+ # transparency = getattr(surface_style, 'Transparency', default_transparency)
3771
+ # return (color.Red*255, color.Green*255, color.Blue*255), transparency, material_list
3492
3772
 
3493
- # If no color is defined, return a consistent random color based on the entity type
3494
- if "wall" in is_a:
3495
- color = (175, 175, 175)
3496
- elif "slab" in is_a:
3497
- color = (200, 200, 200)
3498
- elif "space" in is_a:
3499
- color = (250, 250, 250)
3500
- else:
3501
- random.seed(hash(is_a))
3502
- color = (random.random(), random.random(), random.random())
3773
+ # # If no color is defined, return a consistent random color based on the entity type
3774
+ # if "wall" in is_a:
3775
+ # color = (175, 175, 175)
3776
+ # elif "slab" in is_a:
3777
+ # color = (200, 200, 200)
3778
+ # elif "space" in is_a:
3779
+ # color = (250, 250, 250)
3780
+ # else:
3781
+ # random.seed(hash(is_a))
3782
+ # color = (random.random(), random.random(), random.random())
3503
3783
 
3504
- return color, default_transparency, material_list
3505
-
3506
- def vertexByIFCObject(ifc_object, object_types, restrict=False):
3507
- settings = ifcopenshell.geom.settings()
3508
- settings.set(settings.USE_WORLD_COORDS,True)
3509
- try:
3510
- shape = ifcopenshell.geom.create_shape(settings, ifc_object)
3511
- except:
3512
- shape = None
3513
- if shape or restrict == False: #Only add vertices of entities that have 3D geometries.
3514
- obj_id = ifc_object.id()
3515
- psets = ifcopenshell.util.element.get_psets(ifc_object)
3516
- obj_type = ifc_object.is_a()
3517
- obj_type_id = object_types.index(obj_type)
3518
- name = "Untitled"
3519
- LongName = "Untitled"
3520
- try:
3521
- name = ifc_object.Name
3522
- except:
3523
- name = "Untitled"
3524
- try:
3525
- LongName = ifc_object.LongName
3526
- except:
3527
- LongName = name
3784
+ # return color, default_transparency, material_list
3528
3785
 
3529
- if name == None:
3530
- name = "Untitled"
3531
- if LongName == None:
3532
- LongName = "Untitled"
3533
- label = str(obj_id)+" "+LongName+" ("+obj_type+" "+str(obj_type_id)+")"
3534
- try:
3535
- grouped_verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
3536
- vertices = [Vertex.ByCoordinates(list(coords)) for coords in grouped_verts]
3537
- centroid = Vertex.Centroid(vertices)
3538
- except:
3539
- x = random.uniform(xMin,xMax)
3540
- y = random.uniform(yMin,yMax)
3541
- z = random.uniform(zMin,zMax)
3542
- centroid = Vertex.ByCoordinates(x, y, z)
3786
+ # def vertexByIFCObject(ifc_object, object_types, restrict=False):
3787
+ # settings = ifcopenshell.geom.settings()
3788
+ # settings.set(settings.USE_WORLD_COORDS,True)
3789
+ # try:
3790
+ # shape = ifcopenshell.geom.create_shape(settings, ifc_object)
3791
+ # except:
3792
+ # shape = None
3793
+ # if shape or restrict == False: #Only add vertices of entities that have 3D geometries.
3794
+ # obj_id = ifc_object.id()
3795
+ # psets = ifcopenshell.util.element.get_psets(ifc_object)
3796
+ # obj_type = ifc_object.is_a()
3797
+ # obj_type_id = object_types.index(obj_type)
3798
+ # name = "Untitled"
3799
+ # LongName = "Untitled"
3800
+ # try:
3801
+ # name = ifc_object.Name
3802
+ # except:
3803
+ # name = "Untitled"
3804
+ # try:
3805
+ # LongName = ifc_object.LongName
3806
+ # except:
3807
+ # LongName = name
3808
+
3809
+ # if name == None:
3810
+ # name = "Untitled"
3811
+ # if LongName == None:
3812
+ # LongName = "Untitled"
3813
+ # label = str(obj_id)+" "+LongName+" ("+obj_type+" "+str(obj_type_id)+")"
3814
+ # try:
3815
+ # grouped_verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
3816
+ # vertices = [Vertex.ByCoordinates(list(coords)) for coords in grouped_verts]
3817
+ # centroid = Vertex.Centroid(vertices)
3818
+ # except:
3819
+ # x = random.uniform(xMin,xMax)
3820
+ # y = random.uniform(yMin,yMax)
3821
+ # z = random.uniform(zMin,zMax)
3822
+ # centroid = Vertex.ByCoordinates(x, y, z)
3543
3823
 
3544
- # Store relevant information
3545
- if transferDictionaries == True:
3546
- color, transparency, material_list = get_color_transparency_material(ifc_object)
3547
- if color == None:
3548
- color = "white"
3549
- if transparency == None:
3550
- transparency = 0
3551
- entity_dict = {
3552
- "TOPOLOGIC_id": str(Topology.UUID(centroid)),
3553
- "TOPOLOGIC_name": getattr(ifc_object, 'Name', "Untitled"),
3554
- "TOPOLOGIC_type": Topology.TypeAsString(centroid),
3555
- "TOPOLOGIC_color": color,
3556
- "TOPOLOGIC_opacity": 1.0 - transparency,
3557
- "IFC_global_id": getattr(ifc_object, 'GlobalId', 0),
3558
- "IFC_name": getattr(ifc_object, 'Name', "Untitled"),
3559
- "IFC_type": ifc_object.is_a(),
3560
- "IFC_material_list": material_list,
3561
- }
3562
- topology_dict = Dictionary.ByPythonDictionary(entity_dict)
3563
- # Get PSETs dictionary
3564
- pset_python_dict = get_psets(ifc_object)
3565
- pset_dict = Dictionary.ByPythonDictionary(pset_python_dict)
3566
- topology_dict = Dictionary.ByMergedDictionaries([topology_dict, pset_dict])
3567
- if storeBREP == True or useInternalVertex == True:
3568
- shape_topology = None
3569
- if hasattr(ifc_object, "Representation") and ifc_object.Representation:
3570
- for rep in ifc_object.Representation.Representations:
3571
- if rep.is_a("IfcShapeRepresentation"):
3572
- try:
3573
- # Generate the geometry for this entity
3574
- shape = ifcopenshell.geom.create_shape(settings, ifc_object)
3575
- # Get grouped vertices and grouped faces
3576
- grouped_verts = shape.geometry.verts
3577
- verts = [ [grouped_verts[i], grouped_verts[i + 1], grouped_verts[i + 2]] for i in range(0, len(grouped_verts), 3)]
3578
- grouped_edges = shape.geometry.edges
3579
- edges = [[grouped_edges[i], grouped_edges[i + 1]] for i in range(0, len(grouped_edges), 2)]
3580
- grouped_faces = shape.geometry.faces
3581
- faces = [ [grouped_faces[i], grouped_faces[i + 1], grouped_faces[i + 2]] for i in range(0, len(grouped_faces), 3)]
3582
- shape_topology = Topology.ByGeometry(verts, edges, faces, silent=True)
3583
- if not shape_topology == None:
3584
- if removeCoplanarFaces == True:
3585
- shape_topology = Topology.RemoveCoplanarFaces(shape_topology, epsilon=0.0001)
3586
- except:
3587
- pass
3588
- if not shape_topology == None and storeBREP:
3589
- topology_dict = Dictionary.SetValuesAtKeys(topology_dict, ["brep", "brepType", "brepTypeString"], [Topology.BREPString(shape_topology), Topology.Type(shape_topology), Topology.TypeAsString(shape_topology)])
3590
- if not shape_topology == None and useInternalVertex == True:
3591
- centroid = Topology.InternalVertex(shape_topology)
3592
- centroid = Topology.SetDictionary(centroid, topology_dict)
3593
- return centroid
3594
- return None
3595
-
3596
- def edgesByIFCRelationships(ifc_relationships, ifc_types, vertices):
3597
- tuples = []
3598
- edges = []
3824
+ # # Store relevant information
3825
+ # if transferDictionaries == True:
3826
+ # color, transparency, material_list = get_color_transparency_material(ifc_object)
3827
+ # if color == None:
3828
+ # color = "white"
3829
+ # if transparency == None:
3830
+ # transparency = 0
3831
+ # entity_dict = {
3832
+ # "TOPOLOGIC_id": str(Topology.UUID(centroid)),
3833
+ # "TOPOLOGIC_name": getattr(ifc_object, 'Name', "Untitled"),
3834
+ # "TOPOLOGIC_type": Topology.TypeAsString(centroid),
3835
+ # "TOPOLOGIC_color": color,
3836
+ # "TOPOLOGIC_opacity": 1.0 - transparency,
3837
+ # "IFC_global_id": getattr(ifc_object, 'GlobalId', 0),
3838
+ # "IFC_name": getattr(ifc_object, 'Name', "Untitled"),
3839
+ # "IFC_type": ifc_object.is_a(),
3840
+ # "IFC_material_list": material_list,
3841
+ # }
3842
+ # topology_dict = Dictionary.ByPythonDictionary(entity_dict)
3843
+ # # Get PSETs dictionary
3844
+ # pset_python_dict = get_psets(ifc_object)
3845
+ # pset_dict = Dictionary.ByPythonDictionary(pset_python_dict)
3846
+ # topology_dict = Dictionary.ByMergedDictionaries([topology_dict, pset_dict])
3847
+ # if storeBREP == True or useInternalVertex == True:
3848
+ # shape_topology = None
3849
+ # if hasattr(ifc_object, "Representation") and ifc_object.Representation:
3850
+ # for rep in ifc_object.Representation.Representations:
3851
+ # if rep.is_a("IfcShapeRepresentation"):
3852
+ # try:
3853
+ # # Generate the geometry for this entity
3854
+ # shape = ifcopenshell.geom.create_shape(settings, ifc_object)
3855
+ # # Get grouped vertices and grouped faces
3856
+ # grouped_verts = shape.geometry.verts
3857
+ # verts = [ [grouped_verts[i], grouped_verts[i + 1], grouped_verts[i + 2]] for i in range(0, len(grouped_verts), 3)]
3858
+ # grouped_edges = shape.geometry.edges
3859
+ # edges = [[grouped_edges[i], grouped_edges[i + 1]] for i in range(0, len(grouped_edges), 2)]
3860
+ # grouped_faces = shape.geometry.faces
3861
+ # faces = [ [grouped_faces[i], grouped_faces[i + 1], grouped_faces[i + 2]] for i in range(0, len(grouped_faces), 3)]
3862
+ # shape_topology = Topology.ByGeometry(verts, edges, faces, silent=True)
3863
+ # if not shape_topology == None:
3864
+ # if removeCoplanarFaces == True:
3865
+ # shape_topology = Topology.RemoveCoplanarFaces(shape_topology, epsilon=0.0001)
3866
+ # except:
3867
+ # pass
3868
+ # if not shape_topology == None and storeBREP:
3869
+ # topology_dict = Dictionary.SetValuesAtKeys(topology_dict, ["brep", "brepType", "brepTypeString"], [Topology.BREPString(shape_topology), Topology.Type(shape_topology), Topology.TypeAsString(shape_topology)])
3870
+ # if not shape_topology == None and useInternalVertex == True:
3871
+ # centroid = Topology.InternalVertex(shape_topology)
3872
+ # centroid = Topology.SetDictionary(centroid, topology_dict)
3873
+ # return centroid
3874
+ # return None
3599
3875
 
3600
- for ifc_rel in ifc_relationships:
3601
- source = None
3602
- destinations = []
3603
- if ifc_rel.is_a("IfcRelConnectsPorts"):
3604
- source = ifc_rel.RelatingPort
3605
- destinations = ifc_rel.RelatedPorts
3606
- elif ifc_rel.is_a("IfcRelConnectsPortToElement"):
3607
- source = ifc_rel.RelatingPort
3608
- destinations = [ifc_rel.RelatedElement]
3609
- elif ifc_rel.is_a("IfcRelAggregates"):
3610
- source = ifc_rel.RelatingObject
3611
- destinations = ifc_rel.RelatedObjects
3612
- elif ifc_rel.is_a("IfcRelNests"):
3613
- source = ifc_rel.RelatingObject
3614
- destinations = ifc_rel.RelatedObjects
3615
- elif ifc_rel.is_a("IfcRelAssignsToGroup"):
3616
- source = ifc_rel.RelatingGroup
3617
- destinations = ifc_rel.RelatedObjects
3618
- elif ifc_rel.is_a("IfcRelConnectsPathElements"):
3619
- source = ifc_rel.RelatingElement
3620
- destinations = [ifc_rel.RelatedElement]
3621
- elif ifc_rel.is_a("IfcRelConnectsStructuralMember"):
3622
- source = ifc_rel.RelatingStructuralMember
3623
- destinations = [ifc_rel.RelatedStructuralConnection]
3624
- elif ifc_rel.is_a("IfcRelContainedInSpatialStructure"):
3625
- source = ifc_rel.RelatingStructure
3626
- destinations = ifc_rel.RelatedElements
3627
- elif ifc_rel.is_a("IfcRelFillsElement"):
3628
- source = ifc_rel.RelatingOpeningElement
3629
- destinations = [ifc_rel.RelatedBuildingElement]
3630
- elif ifc_rel.is_a("IfcRelSpaceBoundary"):
3631
- source = ifc_rel.RelatingSpace
3632
- destinations = [ifc_rel.RelatedBuildingElement]
3633
- elif ifc_rel.is_a("IfcRelVoidsElement"):
3634
- source = ifc_rel.RelatingBuildingElement
3635
- destinations = [ifc_rel.RelatedOpeningElement]
3636
- elif ifc_rel.is_a("IfcRelDefinesByProperties") or ifc_rel.is_a("IfcRelAssociatesMaterial") or ifc_rel.is_a("IfcRelDefinesByType"):
3637
- source = None
3638
- destinations = None
3639
- else:
3640
- print("Graph.ByIFCFile - Warning: The relationship", ifc_rel, "is not supported. Skipping.")
3641
- if source:
3642
- sv = vertexAtKeyValue(vertices, key="IFC_global_id", value=getattr(source, 'GlobalId', 0))
3643
- if sv:
3644
- si = Vertex.Index(sv, vertices, tolerance=tolerance)
3645
- if not si == None:
3646
- for destination in destinations:
3647
- if destination == None:
3648
- continue
3649
- ev = vertexAtKeyValue(vertices, key="IFC_global_id", value=getattr(destination, 'GlobalId', 0),)
3650
- if ev:
3651
- ei = Vertex.Index(ev, vertices, tolerance=tolerance)
3652
- if not ei == None:
3653
- if not([si,ei] in tuples or [ei,si] in tuples):
3654
- tuples.append([si,ei])
3655
- e = Edge.ByVertices([sv,ev])
3656
- d = Dictionary.ByKeysValues(["IFC_global_id", "IFC_name", "IFC_type"], [ifc_rel.id(), ifc_rel.Name, ifc_rel.is_a()])
3657
- e = Topology.SetDictionary(e, d)
3658
- edges.append(e)
3659
- return edges
3876
+ # def edgesByIFCRelationships(ifc_relationships, ifc_types, vertices):
3877
+ # tuples = []
3878
+ # edges = []
3879
+
3880
+ # for ifc_rel in ifc_relationships:
3881
+ # source = None
3882
+ # destinations = []
3883
+ # if ifc_rel.is_a("IfcRelConnectsPorts"):
3884
+ # source = ifc_rel.RelatingPort
3885
+ # destinations = ifc_rel.RelatedPorts
3886
+ # elif ifc_rel.is_a("IfcRelConnectsPortToElement"):
3887
+ # source = ifc_rel.RelatingPort
3888
+ # destinations = [ifc_rel.RelatedElement]
3889
+ # elif ifc_rel.is_a("IfcRelAggregates"):
3890
+ # source = ifc_rel.RelatingObject
3891
+ # destinations = ifc_rel.RelatedObjects
3892
+ # elif ifc_rel.is_a("IfcRelNests"):
3893
+ # source = ifc_rel.RelatingObject
3894
+ # destinations = ifc_rel.RelatedObjects
3895
+ # elif ifc_rel.is_a("IfcRelAssignsToGroup"):
3896
+ # source = ifc_rel.RelatingGroup
3897
+ # destinations = ifc_rel.RelatedObjects
3898
+ # elif ifc_rel.is_a("IfcRelConnectsPathElements"):
3899
+ # source = ifc_rel.RelatingElement
3900
+ # destinations = [ifc_rel.RelatedElement]
3901
+ # elif ifc_rel.is_a("IfcRelConnectsStructuralMember"):
3902
+ # source = ifc_rel.RelatingStructuralMember
3903
+ # destinations = [ifc_rel.RelatedStructuralConnection]
3904
+ # elif ifc_rel.is_a("IfcRelContainedInSpatialStructure"):
3905
+ # source = ifc_rel.RelatingStructure
3906
+ # destinations = ifc_rel.RelatedElements
3907
+ # elif ifc_rel.is_a("IfcRelFillsElement"):
3908
+ # source = ifc_rel.RelatingOpeningElement
3909
+ # destinations = [ifc_rel.RelatedBuildingElement]
3910
+ # elif ifc_rel.is_a("IfcRelSpaceBoundary"):
3911
+ # source = ifc_rel.RelatingSpace
3912
+ # destinations = [ifc_rel.RelatedBuildingElement]
3913
+ # elif ifc_rel.is_a("IfcRelVoidsElement"):
3914
+ # source = ifc_rel.RelatingBuildingElement
3915
+ # destinations = [ifc_rel.RelatedOpeningElement]
3916
+ # elif ifc_rel.is_a("IfcRelDefinesByProperties") or ifc_rel.is_a("IfcRelAssociatesMaterial") or ifc_rel.is_a("IfcRelDefinesByType"):
3917
+ # source = None
3918
+ # destinations = None
3919
+ # else:
3920
+ # print("Graph.ByIFCFile - Warning: The relationship", ifc_rel, "is not supported. Skipping.")
3921
+ # if source:
3922
+ # sv = vertexAtKeyValue(vertices, key="IFC_global_id", value=getattr(source, 'GlobalId', 0))
3923
+ # if sv:
3924
+ # si = Vertex.Index(sv, vertices, tolerance=tolerance)
3925
+ # if not si == None:
3926
+ # for destination in destinations:
3927
+ # if destination == None:
3928
+ # continue
3929
+ # ev = vertexAtKeyValue(vertices, key="IFC_global_id", value=getattr(destination, 'GlobalId', 0),)
3930
+ # if ev:
3931
+ # ei = Vertex.Index(ev, vertices, tolerance=tolerance)
3932
+ # if not ei == None:
3933
+ # if not([si,ei] in tuples or [ei,si] in tuples):
3934
+ # tuples.append([si,ei])
3935
+ # e = Edge.ByVertices([sv,ev])
3936
+ # d = Dictionary.ByKeysValues(["IFC_global_id", "IFC_name", "IFC_type"], [ifc_rel.id(), ifc_rel.Name, ifc_rel.is_a()])
3937
+ # e = Topology.SetDictionary(e, d)
3938
+ # edges.append(e)
3939
+ # return edges
3660
3940
 
3661
- ifc_types = IFCObjectTypes(file)
3662
- ifc_objects = IFCObjects(file, include=includeTypes, exclude=excludeTypes)
3663
- vertices = []
3664
- for ifc_object in ifc_objects:
3665
- v = vertexByIFCObject(ifc_object, ifc_types)
3666
- if v:
3667
- vertices.append(v)
3668
- if len(vertices) > 0:
3669
- ifc_relationships = IFCRelationships(file, include=includeRels, exclude=excludeRels)
3670
- edges = edgesByIFCRelationships(ifc_relationships, ifc_types, vertices)
3671
- g = Graph.ByVerticesEdges(vertices, edges)
3672
- else:
3673
- g = None
3674
- return g
3941
+ # ifc_types = IFCObjectTypes(file)
3942
+ # ifc_objects = IFCObjects(file, include=includeTypes, exclude=excludeTypes)
3943
+ # vertices = []
3944
+ # for ifc_object in ifc_objects:
3945
+ # v = vertexByIFCObject(ifc_object, ifc_types)
3946
+ # if v:
3947
+ # vertices.append(v)
3948
+ # if len(vertices) > 0:
3949
+ # ifc_relationships = IFCRelationships(file, include=includeRels, exclude=excludeRels)
3950
+ # edges = edgesByIFCRelationships(ifc_relationships, ifc_types, vertices)
3951
+ # g = Graph.ByVerticesEdges(vertices, edges)
3952
+ # else:
3953
+ # g = None
3954
+ # return g
3675
3955
 
3676
3956
  @staticmethod
3677
3957
  def ByIFCPath(path,
@@ -3766,6 +4046,380 @@ class Graph:
3766
4046
  removeCoplanarFaces=removeCoplanarFaces,
3767
4047
  xMin=xMin, yMin=yMin, zMin=zMin, xMax=xMax, yMax=yMax, zMax=zMax)
3768
4048
 
4049
+ @staticmethod
4050
+ def ByJSONDictionary(
4051
+ jsonDictionary: dict,
4052
+ xKey: str = "x",
4053
+ yKey: str = "y",
4054
+ zKey: str = "z",
4055
+ vertexIDKey: str = None,
4056
+ edgeSourceKey: str = "source",
4057
+ edgeTargetKey: str = "target",
4058
+ edgeIDKey: str = None,
4059
+ graphPropsKey: str = "properties",
4060
+ verticesKey: str = "vertices",
4061
+ edgesKey: str = "edges",
4062
+ mantissa: int = 6,
4063
+ tolerance: float = 0.0001,
4064
+ silent: bool = False,
4065
+ ):
4066
+ """
4067
+ Loads a Graph from a JSON file and attaches graph-, vertex-, and edge-level dictionaries.
4068
+
4069
+ Parameters
4070
+ ----------
4071
+ path : str
4072
+ Path to a JSON file containing:
4073
+ - graph-level properties under `graphPropsKey` (default "properties"),
4074
+ - a vertex dict under `verticesKey` (default "vertices") keyed by vertex IDs,
4075
+ - an edge dict under `edgesKey` (default "edges") keyed by edge IDs.
4076
+ xKey: str , optional
4077
+ JSON key used to read vertex's x coordinate. Default is "x".
4078
+ yKey: str , optional
4079
+ JSON key used to read vertex's y coordinate. Default is "y".
4080
+ zKey: str , optional
4081
+ JSON key used to read vertex's z coordinate. Default is "z".
4082
+ vertexIDKey : str , optional
4083
+ If not None, the vertex dictionary key under which to store the JSON vertex id. Default is "id".
4084
+ edgeSourceKey: str , optional
4085
+ JSON key used to read edge's start vertex. Default is "source".
4086
+ edgeTargetKey: str , optional
4087
+ JSON key used to read edge's end vertex. Default is "target".
4088
+ edgeIDKey : str , optional
4089
+ If not None, the edge dictionary key under which to store the JSON edge id. Default is "id".
4090
+ graphPropsKey: str , optional
4091
+ JSON key for the graph properties section. Default is "properties".
4092
+ verticesKey: str , optional
4093
+ JSON key for the vertices section. Default is "vertices".
4094
+ edgesKey: str , optional
4095
+ JSON key for the edges section. Default is "edges".
4096
+ mantissa : int , optional
4097
+ The desired length of the mantissa. Default is 6.
4098
+ tolerance : float , optional
4099
+ The desired tolerance. Default is 0.0001.
4100
+ silent : bool , optional
4101
+ If set to True, no warnings or error messages are displayed. Default is False.
4102
+
4103
+ Returns
4104
+ -------
4105
+ topologic_core.Graph
4106
+ """
4107
+ # --- Imports kept local by request ---
4108
+ import json
4109
+ import math
4110
+ from typing import Any, Iterable
4111
+
4112
+ # TopologicPy imports
4113
+ from topologicpy.Graph import Graph
4114
+ from topologicpy.Vertex import Vertex
4115
+ from topologicpy.Edge import Edge
4116
+ from topologicpy.Topology import Topology
4117
+ from topologicpy.Dictionary import Dictionary
4118
+
4119
+ # --- Helper functions kept local by request ---
4120
+ def _to_plain(value: Any) -> Any:
4121
+ "Convert numpy/pandas-ish scalars/arrays and nested containers to plain Python."
4122
+ try:
4123
+ import numpy as _np # optional
4124
+ if isinstance(value, _np.generic):
4125
+ return value.item()
4126
+ if isinstance(value, _np.ndarray):
4127
+ return [_to_plain(v) for v in value.tolist()]
4128
+ except Exception:
4129
+ pass
4130
+ if isinstance(value, (list, tuple)):
4131
+ return [_to_plain(v) for v in value]
4132
+ if isinstance(value, dict):
4133
+ return {str(k): _to_plain(v) for k, v in value.items()}
4134
+ if isinstance(value, float):
4135
+ if not math.isfinite(value):
4136
+ return 0.0
4137
+ # normalize -0.0
4138
+ return 0.0 if abs(value) < tolerance else float(value)
4139
+ return value
4140
+
4141
+ def _round_num(x: Any, m: int) -> float:
4142
+ "Safe float conversion + rounding + tolerance clamp."
4143
+ try:
4144
+ xf = float(x)
4145
+ except Exception:
4146
+ return 0.0
4147
+ if not math.isfinite(xf):
4148
+ return 0.0
4149
+ # clamp tiny values to zero to avoid -0.0 drift and floating trash
4150
+ if abs(xf) < tolerance:
4151
+ xf = 0.0
4152
+ return round(xf, max(0, int(m)))
4153
+
4154
+ def _dict_from(obj: dict, drop_keys: Iterable[str] = ()):
4155
+ "Create a Topologic Dictionary from a Python dict (optionally dropping some keys)."
4156
+ data = {k: _to_plain(v) for k, v in obj.items() if k not in drop_keys}
4157
+ if not data:
4158
+ return None
4159
+ keys = list(map(str, data.keys()))
4160
+ vals = list(data.values())
4161
+ try:
4162
+ return Dictionary.ByKeysValues(keys, vals)
4163
+ except Exception:
4164
+ # As a last resort, stringify nested types
4165
+ import json as _json
4166
+ vals2 = [_json.dumps(v) if isinstance(v, (list, dict)) else v for v in vals]
4167
+ return Dictionary.ByKeysValues(keys, vals2)
4168
+
4169
+ # --- Load JSON ---
4170
+ if not isinstance(jsonDictionary, dict):
4171
+ if not silent:
4172
+ print(f"Graph.ByJSONDictionary - Error: The input JSON Dictionary parameter is not a valid python dictionary. Returning None.")
4173
+ return None
4174
+
4175
+ gprops = jsonDictionary.get(graphPropsKey, {}) or {}
4176
+ verts = jsonDictionary.get(verticesKey, {}) or {}
4177
+ edges = jsonDictionary.get(edgesKey, {}) or {}
4178
+
4179
+ # --- Build vertices ---
4180
+ id_to_vertex = {}
4181
+ vertex_list = []
4182
+ for v_id, v_rec in verts.items():
4183
+ x = _round_num(v_rec.get(xKey, 0.0), mantissa)
4184
+ y = _round_num(v_rec.get(yKey, 0.0), mantissa)
4185
+ z = _round_num(v_rec.get(zKey, 0.0), mantissa)
4186
+ try:
4187
+ v = Vertex.ByCoordinates(x, y, z)
4188
+ except Exception as e:
4189
+ if not silent:
4190
+ print(f"Graph.ByJSONDictionary - Warning: failed to create Vertex {v_id} at ({x},{y},{z}): {e}")
4191
+ continue
4192
+
4193
+ # Attach vertex dictionary with all attributes except raw coords
4194
+ v_dict_py = dict(v_rec)
4195
+ if vertexIDKey:
4196
+ v_dict_py[vertexIDKey] = v_id
4197
+ v_dict = _dict_from(v_dict_py, drop_keys={xKey, yKey, zKey})
4198
+ if v_dict:
4199
+ v = Topology.SetDictionary(v, v_dict)
4200
+
4201
+ id_to_vertex[str(v_id)] = v
4202
+ vertex_list.append(v)
4203
+
4204
+ # --- Build edges ---
4205
+ edge_list = []
4206
+ for e_id, e_rec in edges.items():
4207
+ s_id = e_rec.get(edgeSourceKey)
4208
+ t_id = e_rec.get(edgeTargetKey)
4209
+ if s_id is None or t_id is None:
4210
+ if not silent:
4211
+ print(f"Graph.ByJSONDictionary - Warning: skipping Edge {e_id}: missing '{edgeSourceKey}' or '{edgeTargetKey}'.")
4212
+ continue
4213
+ s_id = str(s_id)
4214
+ t_id = str(t_id)
4215
+ if s_id not in id_to_vertex or t_id not in id_to_vertex:
4216
+ if not silent:
4217
+ print(f"Graph.ByJSONDictionary - Warning: skipping Edge {e_id}: unknown endpoint(s) {s_id}->{t_id}.")
4218
+ continue
4219
+ u = id_to_vertex[s_id]
4220
+ v = id_to_vertex[t_id]
4221
+ try:
4222
+ e = Edge.ByVertices(u, v)
4223
+ except Exception as ee:
4224
+ if not silent:
4225
+ print(f"Graph.ByJSONDictionary - Warning: failed to create Edge {e_id}: {ee}")
4226
+ continue
4227
+
4228
+ # Attach full edge record as dictionary (including source/target keys)
4229
+ e_dict = _dict_from(dict(e_rec), drop_keys=())
4230
+ if edgeIDKey:
4231
+ Dictionary.SetValueAtKey(e_dict, edgeIDKey, e_id)
4232
+ if e_dict:
4233
+ e = Topology.SetDictionary(e, e_dict)
4234
+ edge_list.append(e)
4235
+
4236
+ # --- Assemble graph ---
4237
+ try:
4238
+ g = Graph.ByVerticesEdges(vertex_list, edge_list)
4239
+ except Exception:
4240
+ # Fallback: create empty, then add
4241
+ g = Graph.ByVerticesEdges([], [])
4242
+ for v in vertex_list:
4243
+ try:
4244
+ g = Graph.AddVertex(g, v)
4245
+ except Exception:
4246
+ pass
4247
+ for e in edge_list:
4248
+ try:
4249
+ g = Graph.AddEdge(g, e)
4250
+ except Exception:
4251
+ pass
4252
+
4253
+ # --- Graph-level dictionary ---
4254
+ g_dict = _dict_from(dict(gprops), drop_keys=())
4255
+ if g_dict:
4256
+ g = Topology.SetDictionary(g, g_dict)
4257
+
4258
+ return g
4259
+
4260
+ @staticmethod
4261
+ def ByJSONFile(file,
4262
+ xKey: str = "x",
4263
+ yKey: str = "y",
4264
+ zKey: str = "z",
4265
+ vertexIDKey: str = "id",
4266
+ edgeSourceKey: str = "source",
4267
+ edgeTargetKey: str = "target",
4268
+ edgeIDKey: str = "id",
4269
+ graphPropsKey: str = "properties",
4270
+ verticesKey: str = "vertices",
4271
+ edgesKey: str = "edges",
4272
+ mantissa: int = 6,
4273
+ tolerance: float = 0.0001,
4274
+ silent: bool = False):
4275
+ """
4276
+ Imports the graph from a JSON file.
4277
+
4278
+ Parameters
4279
+ ----------
4280
+ file : file object
4281
+ The input JSON file.
4282
+ xKey: str , optional
4283
+ JSON key used to read vertex's x coordinate. Default is "x".
4284
+ yKey: str , optional
4285
+ JSON key used to read vertex's y coordinate. Default is "y".
4286
+ zKey: str , optional
4287
+ JSON key used to read vertex's z coordinate. Default is "z".
4288
+ vertexIDKey : str , optional
4289
+ If not None, the vertex dictionary key under which to store the JSON vertex id. Default is "id".
4290
+ edgeSourceKey: str , optional
4291
+ JSON key used to read edge's start vertex. Default is "source".
4292
+ edgeTargetKey: str , optional
4293
+ JSON key used to read edge's end vertex. Default is "target".
4294
+ edgeIDKey : str , optional
4295
+ If not None, the edge dictionary key under which to store the JSON edge id. Default is "id".
4296
+ graphPropsKey: str , optional
4297
+ JSON key for the graph properties section. Default is "properties".
4298
+ verticesKey: str , optional
4299
+ JSON key for the vertices section. Default is "vertices".
4300
+ edgesKey: str , optional
4301
+ JSON key for the edges section. Default is "edges".
4302
+ mantissa : int , optional
4303
+ The desired length of the mantissa. Default is 6.
4304
+ tolerance : float , optional
4305
+ The desired tolerance. Default is 0.0001.
4306
+ silent : bool , optional
4307
+ If set to True, no warnings or error messages are displayed. Default is False.
4308
+
4309
+ Returns
4310
+ -------
4311
+ topologic_graph
4312
+ the imported graph.
4313
+
4314
+ """
4315
+ import json
4316
+ if not file:
4317
+ if not silent:
4318
+ print("Topology.ByJSONFile - Error: the input file parameter is not a valid file. Returning None.")
4319
+ return None
4320
+ try:
4321
+ json_dict = json.load(file)
4322
+ except Exception as e:
4323
+ if not silent:
4324
+ print("Graph.ByJSONFile - Error: Could not load the JSON file: {e}. Returning None.")
4325
+ return None
4326
+ return Graph.ByJSONDictionary(json_dict,
4327
+ xKey=xKey,
4328
+ yKey=yKey,
4329
+ zKey=zKey,
4330
+ vertexIDKey=vertexIDKey,
4331
+ edgeSourceKey=edgeSourceKey,
4332
+ edgeTargetKey=edgeTargetKey,
4333
+ edgeIDKey=edgeIDKey,
4334
+ graphPropsKey=graphPropsKey,
4335
+ verticesKey=verticesKey,
4336
+ edgesKey=edgesKey,
4337
+ mantissa=mantissa,
4338
+ tolerance=tolerance,
4339
+ silent=silent)
4340
+
4341
+ @staticmethod
4342
+ def ByJSONPath(path,
4343
+ xKey: str = "x",
4344
+ yKey: str = "y",
4345
+ zKey: str = "z",
4346
+ vertexIDKey: str = "id",
4347
+ edgeSourceKey: str = "source",
4348
+ edgeTargetKey: str = "target",
4349
+ edgeIDKey: str = "id",
4350
+ graphPropsKey: str = "properties",
4351
+ verticesKey: str = "vertices",
4352
+ edgesKey: str = "edges",
4353
+ mantissa: int = 6,
4354
+ tolerance: float = 0.0001,
4355
+ silent: bool = False):
4356
+ """
4357
+ Imports the graph from a JSON file.
4358
+
4359
+ Parameters
4360
+ ----------
4361
+ path : str
4362
+ The file path to the json file.
4363
+ xKey: str , optional
4364
+ JSON key used to read vertex's x coordinate. Default is "x".
4365
+ yKey: str , optional
4366
+ JSON key used to read vertex's y coordinate. Default is "y".
4367
+ zKey: str , optional
4368
+ JSON key used to read vertex's z coordinate. Default is "z".
4369
+ vertexIDKey : str , optional
4370
+ If not None, the vertex dictionary key under which to store the JSON vertex id. Default is "id".
4371
+ edgeSourceKey: str , optional
4372
+ JSON key used to read edge's start vertex. Default is "source".
4373
+ edgeTargetKey: str , optional
4374
+ JSON key used to read edge's end vertex. Default is "target".
4375
+ edgeIDKey : str , optional
4376
+ If not None, the edge dictionary key under which to store the JSON edge id. Default is "id".
4377
+ graphPropsKey: str , optional
4378
+ JSON key for the graph properties section. Default is "properties".
4379
+ verticesKey: str , optional
4380
+ JSON key for the vertices section. Default is "vertices".
4381
+ edgesKey: str , optional
4382
+ JSON key for the edges section. Default is "edges".
4383
+ mantissa : int , optional
4384
+ The desired length of the mantissa. Default is 6.
4385
+ tolerance : float , optional
4386
+ The desired tolerance. Default is 0.0001.
4387
+ silent : bool , optional
4388
+ If set to True, no warnings or error messages are displayed. Default is False.
4389
+
4390
+ Returns
4391
+ -------
4392
+ list
4393
+ The list of imported topologies.
4394
+
4395
+ """
4396
+ import json
4397
+ if not path:
4398
+ if not silent:
4399
+ print("Graph.ByJSONPath - Error: the input path parameter is not a valid path. Returning None.")
4400
+ return None
4401
+ try:
4402
+ with open(path) as file:
4403
+ json_dict = json.load(file)
4404
+ except Exception as e:
4405
+ if not silent:
4406
+ print(f"Graph.ByJSONPath - Error: Could not load file: {e}. Returning None.")
4407
+ return None
4408
+ return Graph.ByJSONDictionary(json_dict,
4409
+ xKey=xKey,
4410
+ yKey=yKey,
4411
+ zKey=zKey,
4412
+ vertexIDKey=vertexIDKey,
4413
+ edgeSourceKey=edgeSourceKey,
4414
+ edgeTargetKey=edgeTargetKey,
4415
+ edgeIDKey=edgeIDKey,
4416
+ graphPropsKey=graphPropsKey,
4417
+ verticesKey=verticesKey,
4418
+ edgesKey=edgesKey,
4419
+ mantissa=mantissa,
4420
+ tolerance=tolerance,
4421
+ silent=silent)
4422
+
3769
4423
  @staticmethod
3770
4424
  def ByMeshData(vertices, edges, vertexDictionaries=None, edgeDictionaries=None, tolerance=0.0001):
3771
4425
  """
@@ -3825,7 +4479,7 @@ class Graph:
3825
4479
  return Graph.ByVerticesEdges(g_vertices, g_edges)
3826
4480
 
3827
4481
  @staticmethod
3828
- def ByNetworkXGraph(nxGraph, xKey="x", yKey="y", zKey="z", range=(-1, 1), mantissa: int = 6, tolerance: float = 0.0001):
4482
+ def ByNetworkXGraph(nxGraph, xKey="x", yKey="y", zKey="z", coordsKey='coords', randomRange=(-1, 1), mantissa: int = 6, tolerance: float = 0.0001):
3829
4483
  """
3830
4484
  Converts the input NetworkX graph into a topologic Graph. See http://networkx.org
3831
4485
 
@@ -3839,8 +4493,10 @@ class Graph:
3839
4493
  The dictionary key under which to find the Y-Coordinate of the vertex. Default is 'y'.
3840
4494
  zKey : str , optional
3841
4495
  The dictionary key under which to find the Z-Coordinate of the vertex. Default is 'z'.
3842
- range : tuple , optional
3843
- The range to use for position coordinates if no values are found in the dictionaries. Default is (-1,1)
4496
+ coordsKey : str , optional
4497
+ The dictionary key under which to find the list of the coordinates vertex. Default is 'coords'.
4498
+ randomRange : tuple , optional
4499
+ The range to use for random position coordinates if no values are found in the dictionaries. Default is (-1,1)
3844
4500
  mantissa : int , optional
3845
4501
  The number of decimal places to round the result to. Default is 6.
3846
4502
  tolerance : float , optional
@@ -3859,38 +4515,221 @@ class Graph:
3859
4515
 
3860
4516
  import random
3861
4517
  import numpy as np
4518
+ import math
4519
+ import torch
4520
+ from collections.abc import Mapping, Sequence
4521
+
4522
+ def _is_iterable_but_not_str(x):
4523
+ return isinstance(x, Sequence) and not isinstance(x, (str, bytes, bytearray))
4524
+
4525
+ def _to_python_scalar(x):
4526
+ """Return a plain Python scalar if x is a numpy/pandas/Decimal/torch scalar; otherwise return x."""
4527
+ # numpy scalar
4528
+ if np is not None and isinstance(x, np.generic):
4529
+ return x.item()
4530
+ # pandas NA
4531
+ if pd is not None and x is pd.NA:
4532
+ return None
4533
+ # pandas Timestamp/Timedelta
4534
+ if pd is not None and isinstance(x, (pd.Timestamp, pd.Timedelta)):
4535
+ return x.isoformat()
4536
+ # torch scalar tensor
4537
+ if torch is not None and isinstance(x, torch.Tensor) and x.dim() == 0:
4538
+ return _to_python_scalar(x.item())
4539
+ # decimal
4540
+ try:
4541
+ from decimal import Decimal
4542
+ if isinstance(x, Decimal):
4543
+ return float(x)
4544
+ except Exception:
4545
+ pass
4546
+ return x
4547
+
4548
+ def _to_python_list(x):
4549
+ """Convert arrays/series/tensors/sets/tuples to Python lists (recursively)."""
4550
+ if torch is not None and isinstance(x, torch.Tensor):
4551
+ x = x.detach().cpu().tolist()
4552
+ elif np is not None and isinstance(x, (np.ndarray,)):
4553
+ x = x.tolist()
4554
+ elif pd is not None and isinstance(x, (pd.Series, pd.Index)):
4555
+ x = x.tolist()
4556
+ elif isinstance(x, (set, tuple)):
4557
+ x = list(x)
4558
+ return x
4559
+
4560
+ def _round_number(x, mantissa):
4561
+ """Round finite floats; keep ints; sanitize NaNs/Infs to None."""
4562
+ if isinstance(x, bool): # bool is int subclass; keep as bool
4563
+ return x
4564
+ if isinstance(x, int):
4565
+ return x
4566
+ # try float conversion
4567
+ try:
4568
+ xf = float(x)
4569
+ except Exception:
4570
+ return x # not a number
4571
+ if math.isfinite(xf):
4572
+ return round(xf, mantissa)
4573
+ return None # NaN/Inf -> None
4574
+
4575
+ def clean_value(value, mantissa):
4576
+ """
4577
+ Recursively convert value into TopologicPy-friendly types:
4578
+ - numbers rounded to mantissa
4579
+ - sequences -> lists (cleaned)
4580
+ - mappings -> dicts (cleaned)
4581
+ - datetime -> isoformat
4582
+ - other objects -> str(value)
4583
+ """
4584
+ # First, normalize common library wrappers
4585
+ value = _to_python_scalar(value)
4586
+
4587
+ # Datetime from stdlib
4588
+ import datetime
4589
+ if isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
4590
+ try:
4591
+ return value.isoformat()
4592
+ except Exception:
4593
+ return str(value)
4594
+
4595
+ # Mapping (dict-like)
4596
+ if isinstance(value, Mapping):
4597
+ return {str(k): clean_value(v, mantissa) for k, v in value.items()}
4598
+
4599
+ # Sequences / arrays / tensors -> list
4600
+ if _is_iterable_but_not_str(value) or (
4601
+ (np is not None and isinstance(value, (np.ndarray,))) or
4602
+ (torch is not None and isinstance(value, torch.Tensor)) or
4603
+ (pd is not None and isinstance(value, (pd.Series, pd.Index)))
4604
+ ):
4605
+ value = _to_python_list(value)
4606
+ return [clean_value(v, mantissa) for v in value]
4607
+
4608
+ # Strings stay as-is
4609
+ if isinstance(value, (str, bytes, bytearray)):
4610
+ return value.decode() if isinstance(value, (bytes, bytearray)) else value
4611
+
4612
+ # Numbers (or things that can be safely treated as numbers)
4613
+ out = _round_number(value, mantissa)
4614
+ # If rounder didn't change type and it's still a weird object, stringify it
4615
+ if out is value and not isinstance(out, (type(None), bool, int, float, str)):
4616
+ return str(out)
4617
+ return out
4618
+
4619
+ def coerce_xyz(val, mantissa, default=0.0):
4620
+ """
4621
+ Coerce a candidate XYZ value into a float:
4622
+ - if mapping with 'x' or 'value' -> try those
4623
+ - if sequence -> use first element
4624
+ - if string -> try float
4625
+ - arrays/tensors -> first element
4626
+ - fallback to default
4627
+ """
4628
+ if val is None:
4629
+ return round(float(default), mantissa)
4630
+ # library scalars
4631
+ val = _to_python_scalar(val)
4632
+
4633
+ # Mapping with common keys
4634
+ if isinstance(val, Mapping):
4635
+ for k in ("x", "value", "val", "coord", "0"):
4636
+ if k in val:
4637
+ return coerce_xyz(val[k], mantissa, default)
4638
+ # otherwise try to take first value
4639
+ try:
4640
+ first = next(iter(val.values()))
4641
+ return coerce_xyz(first, mantissa, default)
4642
+ except Exception:
4643
+ return round(float(default), mantissa)
4644
+
4645
+ # Sequence / array / tensor
4646
+ if _is_iterable_but_not_str(val) or \
4647
+ (np is not None and isinstance(val, (np.ndarray,))) or \
4648
+ (torch is not None and isinstance(val, torch.Tensor)) or \
4649
+ (pd is not None and isinstance(val, (pd.Series, pd.Index))):
4650
+ lst = _to_python_list(val)
4651
+ if len(lst) == 0:
4652
+ return round(float(default), mantissa)
4653
+ return coerce_xyz(lst[0], mantissa, default)
4654
+
4655
+ # String
4656
+ if isinstance(val, str):
4657
+ try:
4658
+ return round(float(val), mantissa)
4659
+ except Exception:
4660
+ return round(float(default), mantissa)
3862
4661
 
3863
- # Create a mapping from NetworkX nodes to TopologicPy vertices
3864
- nx_to_topologic_vertex = {}
4662
+ # Numeric
4663
+ try:
4664
+ return _round_number(val, mantissa)
4665
+ except Exception:
4666
+ return round(float(default), mantissa)
4667
+
4668
+ # Create a mapping from NetworkX nodes to TopologicPy vertices
4669
+ nx_to_topologic_vertex = {}
3865
4670
 
3866
4671
  # Create TopologicPy vertices for each node in the NetworkX graph
3867
4672
  vertices = []
3868
4673
  for node, data in nxGraph.nodes(data=True):
3869
- # Attempt to get X, Y, Z from the node data
3870
- x = round(data.get(xKey, random.uniform(*range)), mantissa)
3871
- y = round(data.get(yKey, random.uniform(*range)), mantissa)
3872
- z = round(data.get(zKey, 0), mantissa) # If there are no Z values, this is probably a flat graph.
3873
- # Create a TopologicPy vertex with the node data dictionary
3874
- vertex = Vertex.ByCoordinates(x,y,z)
4674
+ # Clean the node dictionary
3875
4675
  cleaned_values = []
3876
- for value in data.values():
3877
- if isinstance(value, np.ndarray):
3878
- value = list(value)
3879
- cleaned_values.append(value)
3880
-
3881
- node_dict = Dictionary.ByKeysValues(list(data.keys()), cleaned_values)
4676
+ cleaned_keys = []
4677
+ for k, v in data.items():
4678
+ cleaned_keys.append(str(k))
4679
+ cleaned_values.append(clean_value(v, mantissa))
4680
+ data = dict(zip(cleaned_keys, cleaned_values))
4681
+ # Defensive defaults for coordinates
4682
+ x_raw = y_raw = z_raw = None
4683
+ try:
4684
+ x_raw = data.get(xKey, None)
4685
+ y_raw = data.get(yKey, None)
4686
+ z_raw = data.get(zKey, None)
4687
+ except Exception:
4688
+ x_raw = y_raw = z_raw = None
4689
+
4690
+ if x_raw == None:
4691
+ coords = data.get(coordsKey, None)
4692
+ if coords:
4693
+ coords = clean_value(coords, mantissa)
4694
+ if isinstance(coords, list):
4695
+ if len(coords) == 2:
4696
+ x_raw = coords[0]
4697
+ y_raw = coords[1]
4698
+ z_raw = 0
4699
+ elif len(coords) == 3:
4700
+ x_raw = coords[0]
4701
+ y_raw = coords[1]
4702
+ z_raw = coords[2]
4703
+
4704
+ # Fall back to random only if missing / invalid
4705
+ x = coerce_xyz(x_raw, mantissa, default=random.uniform(*randomRange))
4706
+ y = coerce_xyz(y_raw, mantissa, default=random.uniform(*randomRange))
4707
+ z = coerce_xyz(z_raw, mantissa, default=0.0)
4708
+
4709
+ # Create vertex
4710
+ vertex = Vertex.ByCoordinates(x, y, z)
4711
+
4712
+ # Build and attach TopologicPy dictionary
4713
+ node_dict = Dictionary.ByKeysValues(cleaned_keys, cleaned_values)
3882
4714
  vertex = Topology.SetDictionary(vertex, node_dict)
3883
- nx_to_topologic_vertex[node] = vertex
4715
+
4716
+ #nx_to_topologic_vertex[node] = vertex
3884
4717
  vertices.append(vertex)
3885
4718
 
3886
4719
  # Create TopologicPy edges for each edge in the NetworkX graph
3887
4720
  edges = []
3888
4721
  for u, v, data in nxGraph.edges(data=True):
3889
- start_vertex = nx_to_topologic_vertex[u]
3890
- end_vertex = nx_to_topologic_vertex[v]
4722
+ start_vertex = vertices[u]
4723
+ end_vertex = vertices[v]
3891
4724
 
3892
4725
  # Create a TopologicPy edge with the edge data dictionary
3893
- edge_dict = Dictionary.ByKeysValues(list(data.keys()), list(data.values()))
4726
+ # Clean the node dictionary
4727
+ cleaned_values = []
4728
+ cleaned_keys = []
4729
+ for k, v in data.items():
4730
+ cleaned_keys.append(str(k))
4731
+ cleaned_values.append(clean_value(v, mantissa))
4732
+ edge_dict = Dictionary.ByKeysValues(cleaned_keys, cleaned_values)
3894
4733
  edge = Edge.ByVertices([start_vertex, end_vertex], tolerance=tolerance)
3895
4734
  edge = Topology.SetDictionary(edge, edge_dict)
3896
4735
  edges.append(edge)
@@ -6007,6 +6846,8 @@ class Graph:
6007
6846
  return graph
6008
6847
 
6009
6848
 
6849
+
6850
+
6010
6851
  @staticmethod
6011
6852
  def ClosenessCentrality(
6012
6853
  graph,
@@ -6021,144 +6862,396 @@ class Graph:
6021
6862
  silent: bool = False
6022
6863
  ):
6023
6864
  """
6024
- Returns the closeness centrality of the input graph. The order of the returned
6025
- list matches the order of Graph.Vertices(graph).
6026
- See: https://en.wikipedia.org/wiki/Closeness_centrality
6027
-
6028
- Parameters
6029
- ----------
6030
- graph : topologic_core.Graph
6031
- The input graph.
6032
- weightKey : str , optional
6033
- If specified, this edge attribute will be used as the distance weight when
6034
- computing shortest paths. If set to a name containing "Length" or "Distance",
6035
- it will be mapped to "length".
6036
- Note: Graph.NetworkXGraph automatically provides a "length" attribute on all edges.
6037
- normalize : bool , optional
6038
- If True, the returned values are rescaled to [0, 1]. Otherwise raw values
6039
- from NetworkX (optionally using the improved formula) are returned.
6040
- nxCompatible : bool , optional
6041
- If True, use NetworkX's wf_improved scaling (Wasserman and Faust).
6042
- For single-component graphs it matches the original formula.
6043
- key : str , optional
6044
- The dictionary key under which to store the closeness centrality score.
6045
- colorKey : str , optional
6046
- The dictionary key under which to store a color derived from the score.
6047
- colorScale : str , optional
6048
- Plotly color scale name (e.g., "viridis", "plasma").
6049
- mantissa : int , optional
6050
- The number of decimal places to round the result to. Default is 6.
6051
- tolerance : float , optional
6052
- The desired tolerance. Default is 0.0001.
6053
- silent : bool , optional
6054
- If set to True, error and warning messages are suppressed. Default is False.
6055
-
6056
- Returns
6057
- -------
6058
- list[float]
6059
- Closeness centrality values for vertices in the same order as Graph.Vertices(graph).
6865
+ Optimized closeness centrality:
6866
+ - Avoids NetworkX and costly per-vertex Topologic calls.
6867
+ - Builds integer-index adjacency once from edges (undirected).
6868
+ - Unweighted: multi-source BFS (one per node).
6869
+ - Weighted: Dijkstra per node (heapq), or SciPy csgraph if available.
6870
+ - Supports 'wf_improved' scaling (nxCompatible) and optional normalization.
6060
6871
  """
6061
- import warnings
6062
- try:
6063
- import networkx as nx
6064
- except Exception as e:
6065
- warnings.warn(
6066
- f"Graph.ClosenessCentrality - Error: networkx is required but not installed ({e}). Returning None."
6067
- )
6068
- return None
6872
+ from collections import deque
6873
+ import math
6069
6874
 
6875
+ from topologicpy.Topology import Topology
6070
6876
  from topologicpy.Dictionary import Dictionary
6071
6877
  from topologicpy.Color import Color
6072
- from topologicpy.Topology import Topology
6073
6878
  from topologicpy.Helper import Helper
6879
+ from topologicpy.Vertex import Vertex
6880
+ from topologicpy.Edge import Edge
6881
+ # NOTE: We are inside Graph.*, so Graph.<...> methods are available.
6074
6882
 
6075
- # Topology.IsInstance is case-insensitive, so a single call is sufficient.
6883
+ # Validate graph
6076
6884
  if not Topology.IsInstance(graph, "graph"):
6077
6885
  if not silent:
6078
6886
  print("Graph.ClosenessCentrality - Error: The input is not a valid Graph. Returning None.")
6079
6887
  return None
6888
+
6080
6889
  vertices = Graph.Vertices(graph)
6081
- if len(vertices) == 0:
6890
+ n = len(vertices)
6891
+ if n == 0:
6082
6892
  if not silent:
6083
6893
  print("Graph.ClosenessCentrality - Warning: Graph has no vertices. Returning [].")
6084
6894
  return []
6085
6895
 
6086
- # Normalize the weight key semantics
6896
+ # Stable vertex key (prefer an 'id' in the vertex dictionary; else rounded coords)
6897
+ def vkey(v, r=9):
6898
+ d = Topology.Dictionary(v)
6899
+ vid = Dictionary.ValueAtKey(d, "id")
6900
+ if vid is not None:
6901
+ return ("id", vid)
6902
+ return ("xyz", round(Vertex.X(v), r), round(Vertex.Y(v), r), round(Vertex.Z(v), r))
6903
+
6904
+ idx_of = {vkey(v): i for i, v in enumerate(vertices)}
6905
+
6906
+ # Normalize weight key
6087
6907
  distance_attr = None
6088
6908
  if isinstance(weightKey, str) and weightKey:
6089
- if ("len" in weightKey.lower()) or ("dis" in weightKey.lower()):
6909
+ wl = weightKey.lower()
6910
+ if ("len" in wl) or ("dis" in wl):
6090
6911
  weightKey = "length"
6091
- distance_attr = weightKey
6092
-
6093
- # Build the NX graph
6094
- nx_graph = Graph.NetworkXGraph(graph)
6095
-
6096
- # Graph.NetworkXGraph automatically adds "length" to all edges.
6097
- # So if distance_attr == "length", we trust it and skip per-edge checks.
6098
- if distance_attr and distance_attr != "length":
6099
- # For any non-"length" custom attribute, verify presence; else fall back unweighted.
6100
- attr_missing = any(
6101
- (distance_attr not in data) or (data[distance_attr] is None)
6102
- for _, _, data in nx_graph.edges(data=True)
6103
- )
6104
- if attr_missing:
6105
- if not silent:
6106
- print("Graph.ClosenessCentrality - Warning: The specified edge attribute was not found on all edges. Falling back to unweighted closeness.")
6107
- distance_arg = None
6108
- else:
6109
- distance_arg = distance_attr
6110
- else:
6111
- # Use "length" directly or unweighted if distance_attr is falsy.
6112
- distance_arg = distance_attr if distance_attr else None
6912
+ distance_attr = weightKey # may be "length" or a custom key
6113
6913
 
6114
- # Compute centrality (dict keyed by NetworkX nodes)
6115
- try:
6116
- cc_dict = nx.closeness_centrality(nx_graph, distance=distance_arg, wf_improved=nxCompatible)
6117
- except Exception as e:
6118
- if not silent:
6119
- print(f"Graph.ClosenessCentrality - Error: NetworkX failed to compute centrality ({e}). Returning None.")
6120
- return None
6914
+ # Build undirected adjacency with minimal weights per edge
6915
+ # Use dict-of-dict to collapse multi-edges to minimal weight
6916
+ adj = [dict() for _ in range(n)] # adj[i][j] = weight
6917
+ edges = Graph.Edges(graph)
6121
6918
 
6122
- # NetworkX vertex ids are in the same numerice order as the list of vertices starting from 0.
6123
- raw_values = []
6124
- for i, v in enumerate(vertices):
6919
+ def edge_weight(e):
6920
+ if distance_attr == "length":
6921
+ try:
6922
+ return float(Edge.Length(e))
6923
+ except Exception:
6924
+ return 1.0
6925
+ elif distance_attr:
6926
+ try:
6927
+ d = Topology.Dictionary(e)
6928
+ w = Dictionary.ValueAtKey(d, distance_attr)
6929
+ return float(w) if (w is not None) else 1.0
6930
+ except Exception:
6931
+ return 1.0
6932
+ else:
6933
+ return 1.0
6934
+
6935
+ for e in edges:
6125
6936
  try:
6126
- raw_values.append(float(cc_dict.get(i, 0.0)))
6937
+ u = Edge.StartVertex(e)
6938
+ v = Edge.EndVertex(e)
6127
6939
  except Exception:
6128
- if not silent:
6129
- print(f,"Graph.ClosenessCentrality - Warning: Could not retrieve score for vertex {i}. Assigning a Zero (0).")
6130
- raw_values.append(0.0)
6940
+ # Fallback in odd cases
6941
+ continue
6942
+ iu = idx_of.get(vkey(u))
6943
+ iv = idx_of.get(vkey(v))
6944
+ if iu is None or iv is None or iu == iv:
6945
+ continue
6946
+ w = edge_weight(e)
6947
+ # Keep minimal weight if duplicates
6948
+ prev = adj[iu].get(iv)
6949
+ if (prev is None) or (w < prev):
6950
+ adj[iu][iv] = w
6951
+ adj[iv][iu] = w
6952
+
6953
+ # Detect weighted vs unweighted
6954
+ weighted = False
6955
+ for i in range(n):
6956
+ if any(abs(w - 1.0) > 1e-12 for w in adj[i].values()):
6957
+ weighted = True
6958
+ break
6131
6959
 
6132
- # Optional normalization ONLY once, then rounding once at the end
6133
- values_for_return = Helper.Normalize(raw_values) if normalize else raw_values
6960
+ INF = float("inf")
6961
+
6962
+ # ---- shortest paths helpers ----
6963
+ def bfs_sum(i):
6964
+ """Sum of unweighted shortest path distances from i; returns (tot, reachable)."""
6965
+ dist = [-1] * n
6966
+ q = deque([i])
6967
+ dist[i] = 0
6968
+ reachable = 1
6969
+ tot = 0
6970
+ pop = q.popleft; push = q.append
6971
+ while q:
6972
+ u = pop()
6973
+ du = dist[u]
6974
+ for v in adj[u].keys():
6975
+ if dist[v] == -1:
6976
+ dist[v] = du + 1
6977
+ reachable += 1
6978
+ tot += dist[v]
6979
+ push(v)
6980
+ return float(tot), reachable
6981
+
6982
+ def dijkstra_sum(i):
6983
+ """Sum of weighted shortest path distances from i; returns (tot, reachable)."""
6984
+ import heapq
6985
+ dist = [INF] * n
6986
+ dist[i] = 0.0
6987
+ hq = [(0.0, i)]
6988
+ push = heapq.heappush; pop = heapq.heappop
6989
+ while hq:
6990
+ du, u = pop(hq)
6991
+ if du > dist[u]:
6992
+ continue
6993
+ for v, w in adj[u].items():
6994
+ nd = du + w
6995
+ if nd < dist[v]:
6996
+ dist[v] = nd
6997
+ push(hq, (nd, v))
6998
+ # Exclude self (0.0) and unreachable (INF)
6999
+ reachable = 0
7000
+ tot = 0.0
7001
+ for d in dist:
7002
+ if d < INF:
7003
+ reachable += 1
7004
+ tot += d
7005
+ # subtract self-distance
7006
+ tot -= 0.0
7007
+ return float(tot), reachable
7008
+
7009
+ # SciPy acceleration if weighted and available
7010
+ use_scipy = False
7011
+ if weighted:
7012
+ try:
7013
+ import numpy as np
7014
+ from scipy.sparse import csr_matrix
7015
+ from scipy.sparse.csgraph import dijkstra as sp_dijkstra
7016
+ use_scipy = True
7017
+ # Build CSR once
7018
+ rows, cols, data = [], [], []
7019
+ for i in range(n):
7020
+ for j, w in adj[i].items():
7021
+ rows.append(i); cols.append(j); data.append(float(w))
7022
+ if len(data) == 0:
7023
+ use_scipy = False # empty graph; fall back
7024
+ else:
7025
+ A = csr_matrix((np.array(data), (np.array(rows), np.array(cols))), shape=(n, n))
7026
+ except Exception:
7027
+ use_scipy = False
6134
7028
 
6135
- # Values for color scaling should reflect the displayed numbers
6136
- color_values = values_for_return
7029
+ # ---- centrality computation ----
7030
+ values = [0.0] * n
7031
+ if n == 1:
7032
+ values[0] = 0.0
7033
+ else:
7034
+ if not weighted:
7035
+ for i in range(n):
7036
+ tot, reachable = bfs_sum(i)
7037
+ s = max(reachable - 1, 0)
7038
+ if tot > 0.0:
7039
+ if nxCompatible:
7040
+ # Wasserman–Faust improved scaling for disconnected graphs
7041
+ values[i] = (s / (n - 1)) * (s / tot)
7042
+ else:
7043
+ values[i] = s / tot
7044
+ else:
7045
+ values[i] = 0.0
7046
+ else:
7047
+ if use_scipy:
7048
+ # All-pairs from SciPy (fast)
7049
+ import numpy as np
7050
+ D = sp_dijkstra(A, directed=False, return_predecessors=False)
7051
+ for i in range(n):
7052
+ di = D[i]
7053
+ finite = di[np.isfinite(di)]
7054
+ # di includes self at 0; reachable count is len(finite)
7055
+ reachable = int(finite.size)
7056
+ s = max(reachable - 1, 0)
7057
+ tot = float(finite.sum()) # includes self=0
7058
+ if s > 0:
7059
+ if nxCompatible:
7060
+ values[i] = (s / (n - 1)) * (s / tot)
7061
+ else:
7062
+ values[i] = s / tot
7063
+ else:
7064
+ values[i] = 0.0
7065
+ else:
7066
+ # Per-source Dijkstra
7067
+ for i in range(n):
7068
+ tot, reachable = dijkstra_sum(i)
7069
+ s = max(reachable - 1, 0)
7070
+ if tot > 0.0:
7071
+ if nxCompatible:
7072
+ values[i] = (s / (n - 1)) * (s / tot)
7073
+ else:
7074
+ values[i] = s / tot
7075
+ else:
7076
+ values[i] = 0.0
6137
7077
 
6138
- # Single rounding at the end for return values
7078
+ # Optional normalization, round once
7079
+ out_vals = Helper.Normalize(values) if normalize else values
6139
7080
  if mantissa is not None and mantissa >= 0:
6140
- values_for_return = [round(v, mantissa) for v in values_for_return]
7081
+ out_vals = [round(v, mantissa) for v in out_vals]
6141
7082
 
6142
- # Prepare color mapping range, guarding equal-range case
6143
- if color_values:
6144
- min_value = min(color_values)
6145
- max_value = max(color_values)
7083
+ # Color mapping range (use displayed numbers)
7084
+ if out_vals:
7085
+ min_v, max_v = min(out_vals), max(out_vals)
6146
7086
  else:
6147
- min_value, max_value = 0.0, 1.0
7087
+ min_v, max_v = 0.0, 1.0
7088
+ if abs(max_v - min_v) < tolerance:
7089
+ max_v = min_v + tolerance
6148
7090
 
6149
- if abs(max_value - min_value) < tolerance:
6150
- max_value = min_value + tolerance
6151
-
6152
- # Annotate vertices with score and color
6153
- for i, value in enumerate(color_values):
7091
+ # Annotate vertices
7092
+ for i, value in enumerate(out_vals):
6154
7093
  d = Topology.Dictionary(vertices[i])
6155
7094
  color_hex = Color.AnyToHex(
6156
- Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale)
7095
+ Color.ByValueInRange(value, minValue=min_v, maxValue=max_v, colorScale=colorScale)
6157
7096
  )
6158
- d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [values_for_return[i], color_hex])
7097
+ d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color_hex])
6159
7098
  vertices[i] = Topology.SetDictionary(vertices[i], d)
6160
7099
 
6161
- return values_for_return
7100
+ return out_vals
7101
+
7102
+
7103
+ # @staticmethod
7104
+ # def ClosenessCentrality_old(
7105
+ # graph,
7106
+ # weightKey: str = "length",
7107
+ # normalize: bool = False,
7108
+ # nxCompatible: bool = True,
7109
+ # key: str = "closeness_centrality",
7110
+ # colorKey: str = "cc_color",
7111
+ # colorScale: str = "viridis",
7112
+ # mantissa: int = 6,
7113
+ # tolerance: float = 0.0001,
7114
+ # silent: bool = False
7115
+ # ):
7116
+ # """
7117
+ # Returns the closeness centrality of the input graph. The order of the returned
7118
+ # list matches the order of Graph.Vertices(graph).
7119
+ # See: https://en.wikipedia.org/wiki/Closeness_centrality
7120
+
7121
+ # Parameters
7122
+ # ----------
7123
+ # graph : topologic_core.Graph
7124
+ # The input graph.
7125
+ # weightKey : str , optional
7126
+ # If specified, this edge attribute will be used as the distance weight when
7127
+ # computing shortest paths. If set to a name containing "Length" or "Distance",
7128
+ # it will be mapped to "length".
7129
+ # Note: Graph.NetworkXGraph automatically provides a "length" attribute on all edges.
7130
+ # normalize : bool , optional
7131
+ # If True, the returned values are rescaled to [0, 1]. Otherwise raw values
7132
+ # from NetworkX (optionally using the improved formula) are returned.
7133
+ # nxCompatible : bool , optional
7134
+ # If True, use NetworkX's wf_improved scaling (Wasserman and Faust).
7135
+ # For single-component graphs it matches the original formula.
7136
+ # key : str , optional
7137
+ # The dictionary key under which to store the closeness centrality score.
7138
+ # colorKey : str , optional
7139
+ # The dictionary key under which to store a color derived from the score.
7140
+ # colorScale : str , optional
7141
+ # Plotly color scale name (e.g., "viridis", "plasma").
7142
+ # mantissa : int , optional
7143
+ # The number of decimal places to round the result to. Default is 6.
7144
+ # tolerance : float , optional
7145
+ # The desired tolerance. Default is 0.0001.
7146
+ # silent : bool , optional
7147
+ # If set to True, error and warning messages are suppressed. Default is False.
7148
+
7149
+ # Returns
7150
+ # -------
7151
+ # list[float]
7152
+ # Closeness centrality values for vertices in the same order as Graph.Vertices(graph).
7153
+ # """
7154
+ # import warnings
7155
+ # try:
7156
+ # import networkx as nx
7157
+ # except Exception as e:
7158
+ # warnings.warn(
7159
+ # f"Graph.ClosenessCentrality - Error: networkx is required but not installed ({e}). Returning None."
7160
+ # )
7161
+ # return None
7162
+
7163
+ # from topologicpy.Dictionary import Dictionary
7164
+ # from topologicpy.Color import Color
7165
+ # from topologicpy.Topology import Topology
7166
+ # from topologicpy.Helper import Helper
7167
+
7168
+ # # Topology.IsInstance is case-insensitive, so a single call is sufficient.
7169
+ # if not Topology.IsInstance(graph, "graph"):
7170
+ # if not silent:
7171
+ # print("Graph.ClosenessCentrality - Error: The input is not a valid Graph. Returning None.")
7172
+ # return None
7173
+ # vertices = Graph.Vertices(graph)
7174
+ # if len(vertices) == 0:
7175
+ # if not silent:
7176
+ # print("Graph.ClosenessCentrality - Warning: Graph has no vertices. Returning [].")
7177
+ # return []
7178
+
7179
+ # # Normalize the weight key semantics
7180
+ # distance_attr = None
7181
+ # if isinstance(weightKey, str) and weightKey:
7182
+ # if ("len" in weightKey.lower()) or ("dis" in weightKey.lower()):
7183
+ # weightKey = "length"
7184
+ # distance_attr = weightKey
7185
+
7186
+ # # Build the NX graph
7187
+ # nx_graph = Graph.NetworkXGraph(graph)
7188
+
7189
+ # # Graph.NetworkXGraph automatically adds "length" to all edges.
7190
+ # # So if distance_attr == "length", we trust it and skip per-edge checks.
7191
+ # if distance_attr and distance_attr != "length":
7192
+ # # For any non-"length" custom attribute, verify presence; else fall back unweighted.
7193
+ # attr_missing = any(
7194
+ # (distance_attr not in data) or (data[distance_attr] is None)
7195
+ # for _, _, data in nx_graph.edges(data=True)
7196
+ # )
7197
+ # if attr_missing:
7198
+ # if not silent:
7199
+ # print("Graph.ClosenessCentrality - Warning: The specified edge attribute was not found on all edges. Falling back to unweighted closeness.")
7200
+ # distance_arg = None
7201
+ # else:
7202
+ # distance_arg = distance_attr
7203
+ # else:
7204
+ # # Use "length" directly or unweighted if distance_attr is falsy.
7205
+ # distance_arg = distance_attr if distance_attr else None
7206
+
7207
+ # # Compute centrality (dict keyed by NetworkX nodes)
7208
+ # try:
7209
+ # cc_dict = nx.closeness_centrality(nx_graph, distance=distance_arg, wf_improved=nxCompatible)
7210
+ # except Exception as e:
7211
+ # if not silent:
7212
+ # print(f"Graph.ClosenessCentrality - Error: NetworkX failed to compute centrality ({e}). Returning None.")
7213
+ # return None
7214
+
7215
+ # # NetworkX vertex ids are in the same numerice order as the list of vertices starting from 0.
7216
+ # raw_values = []
7217
+ # for i, v in enumerate(vertices):
7218
+ # try:
7219
+ # raw_values.append(float(cc_dict.get(i, 0.0)))
7220
+ # except Exception:
7221
+ # if not silent:
7222
+ # print(f,"Graph.ClosenessCentrality - Warning: Could not retrieve score for vertex {i}. Assigning a Zero (0).")
7223
+ # raw_values.append(0.0)
7224
+
7225
+ # # Optional normalization ONLY once, then rounding once at the end
7226
+ # values_for_return = Helper.Normalize(raw_values) if normalize else raw_values
7227
+
7228
+ # # Values for color scaling should reflect the displayed numbers
7229
+ # color_values = values_for_return
7230
+
7231
+ # # Single rounding at the end for return values
7232
+ # if mantissa is not None and mantissa >= 0:
7233
+ # values_for_return = [round(v, mantissa) for v in values_for_return]
7234
+
7235
+ # # Prepare color mapping range, guarding equal-range case
7236
+ # if color_values:
7237
+ # min_value = min(color_values)
7238
+ # max_value = max(color_values)
7239
+ # else:
7240
+ # min_value, max_value = 0.0, 1.0
7241
+
7242
+ # if abs(max_value - min_value) < tolerance:
7243
+ # max_value = min_value + tolerance
7244
+
7245
+ # # Annotate vertices with score and color
7246
+ # for i, value in enumerate(color_values):
7247
+ # d = Topology.Dictionary(vertices[i])
7248
+ # color_hex = Color.AnyToHex(
7249
+ # Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale)
7250
+ # )
7251
+ # d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [values_for_return[i], color_hex])
7252
+ # vertices[i] = Topology.SetDictionary(vertices[i], d)
7253
+
7254
+ # return values_for_return
6162
7255
 
6163
7256
  @staticmethod
6164
7257
  def Community(graph, key: str = "partition", mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
@@ -7467,13 +8560,13 @@ class Graph:
7467
8560
  1 for validate, and 2 for test. If no key is found, the ratio of train/validate/test will be used. Default is "mask".
7468
8561
  nodeTrainRatio : float , optional
7469
8562
  The desired ratio of the node data to use for training. The number must be between 0 and 1. Default is 0.8 which means 80% of the data will be used for training.
7470
- This value is ignored if an nodeMaskKey is foud.
8563
+ This value is ignored if an nodeMaskKey is found.
7471
8564
  nodeValidateRatio : float , optional
7472
8565
  The desired ratio of the node data to use for validation. The number must be between 0 and 1. Default is 0.1 which means 10% of the data will be used for validation.
7473
- This value is ignored if an nodeMaskKey is foud.
8566
+ This value is ignored if an nodeMaskKey is found.
7474
8567
  nodeTestRatio : float , optional
7475
8568
  The desired ratio of the node data to use for testing. The number must be between 0 and 1. Default is 0.1 which means 10% of the data will be used for testing.
7476
- This value is ignored if an nodeMaskKey is foud.
8569
+ This value is ignored if an nodeMaskKey is found.
7477
8570
  mantissa : int , optional
7478
8571
  The number of decimal places to round the result to. Default is 6.
7479
8572
  tolerance : float , optional
@@ -8340,7 +9433,7 @@ class Graph:
8340
9433
  return False
8341
9434
 
8342
9435
  @staticmethod
8343
- def ExportToJSON(graph, path, verticesKey="vertices", edgesKey="edges", vertexLabelKey="", edgeLabelKey="", xKey="x", yKey="y", zKey="z", indent=4, sortKeys=False, mantissa=6, overwrite=False):
9436
+ def ExportToJSON(graph, path, propertiesKey="properties", verticesKey="vertices", edgesKey="edges", vertexLabelKey="", edgeLabelKey="", xKey="x", yKey="y", zKey="z", indent=4, sortKeys=False, mantissa=6, overwrite=False):
8344
9437
  """
8345
9438
  Exports the input graph to a JSON file.
8346
9439
 
@@ -8350,6 +9443,8 @@ class Graph:
8350
9443
  The input graph.
8351
9444
  path : str
8352
9445
  The path to the JSON file.
9446
+ propertiesKey : str , optional
9447
+ The desired key name to call graph properties. Default is "properties".
8353
9448
  verticesKey : str , optional
8354
9449
  The desired key name to call vertices. Default is "vertices".
8355
9450
  edgesKey : str , optional
@@ -8399,7 +9494,7 @@ class Graph:
8399
9494
  except:
8400
9495
  raise Exception("Graph.ExportToJSON - Error: Could not create a new file at the following location: "+path)
8401
9496
  if (f):
8402
- jsondata = Graph.JSONData(graph, verticesKey=verticesKey, edgesKey=edgesKey, vertexLabelKey=vertexLabelKey, edgeLabelKey=edgeLabelKey, xKey=xKey, yKey=yKey, zKey=zKey, mantissa=mantissa)
9497
+ jsondata = Graph.JSONData(graph, propertiesKey=propertiesKey, verticesKey=verticesKey, edgesKey=edgesKey, vertexLabelKey=vertexLabelKey, edgeLabelKey=edgeLabelKey, xKey=xKey, yKey=yKey, zKey=zKey, mantissa=mantissa)
8403
9498
  if jsondata != None:
8404
9499
  json.dump(jsondata, f, indent=indent, sort_keys=sortKeys)
8405
9500
  f.close()
@@ -9919,8 +11014,26 @@ class Graph:
9919
11014
  The created GraphViz graph.
9920
11015
  """
9921
11016
 
9922
- from graphviz import Digraph
9923
- from graphviz import Graph as Udgraph
11017
+ import os
11018
+ import warnings
11019
+
11020
+ try:
11021
+ from graphviz import Digraph
11022
+ from graphviz import Graph as Udgraph
11023
+
11024
+ except:
11025
+ print("Graph - Installing required graphviz library.")
11026
+ try:
11027
+ os.system("pip install graphviz")
11028
+ except:
11029
+ os.system("pip install graphviz --user")
11030
+ try:
11031
+ from graphviz import Digraph
11032
+ from graphviz import Graph as Udgraph
11033
+ print("Graph - graphviz library installed correctly.")
11034
+ except:
11035
+ warnings.warn("Graph - Error: Could not import graphviz.")
11036
+
9924
11037
  from topologicpy.Graph import Graph
9925
11038
  from topologicpy.Topology import Topology
9926
11039
  from topologicpy.Dictionary import Dictionary
@@ -11114,6 +12227,7 @@ class Graph:
11114
12227
 
11115
12228
  @staticmethod
11116
12229
  def JSONData(graph,
12230
+ propertiesKey: str = "properties",
11117
12231
  verticesKey: str = "vertices",
11118
12232
  edgesKey: str = "edges",
11119
12233
  vertexLabelKey: str = "",
@@ -11133,6 +12247,8 @@ class Graph:
11133
12247
  ----------
11134
12248
  graph : topologic_core.Graph
11135
12249
  The input graph.
12250
+ propertiesKey : str , optional
12251
+ The desired key name to call the graph properties. Default is "properties".
11136
12252
  verticesKey : str , optional
11137
12253
  The desired key name to call vertices. Default is "vertices".
11138
12254
  edgesKey : str , optional
@@ -11172,8 +12288,10 @@ class Graph:
11172
12288
  from topologicpy.Dictionary import Dictionary
11173
12289
  from topologicpy.Helper import Helper
11174
12290
 
12291
+ graph_d = Dictionary.PythonDictionary(Topology.Dictionary(graph))
11175
12292
  vertices = Graph.Vertices(graph)
11176
12293
  j_data = {}
12294
+ j_data[propertiesKey] = graph_d
11177
12295
  j_data[verticesKey] = {}
11178
12296
  j_data[edgesKey] = {}
11179
12297
  n = max(len(str(len(vertices))), 4)
@@ -12962,90 +14080,201 @@ class Graph:
12962
14080
  return outgoing_vertices
12963
14081
 
12964
14082
  @staticmethod
12965
- def PageRank(graph, alpha: float = 0.85, maxIterations: int = 100, normalize: bool = True, directed: bool = False, key: str = "page_rank", colorKey="pr_color", colorScale="viridis", mantissa: int = 6, tolerance: float = 0.0001):
14083
+ def PageRank(
14084
+ graph,
14085
+ alpha: float = 0.85,
14086
+ maxIterations: int = 100,
14087
+ normalize: bool = True,
14088
+ directed: bool = False,
14089
+ key: str = "page_rank",
14090
+ colorKey: str = "pr_color",
14091
+ colorScale: str = "viridis",
14092
+ mantissa: int = 6,
14093
+ tolerance: float = 1e-4
14094
+ ):
12966
14095
  """
12967
- Calculates PageRank scores for vertices in a directed graph. see https://en.wikipedia.org/wiki/PageRank.
12968
-
12969
- Parameters
12970
- ----------
12971
- graph : topologic_core.Graph
12972
- The input graph.
12973
- alpha : float , optional
12974
- The damping (dampening) factor. Default is 0.85. See https://en.wikipedia.org/wiki/PageRank.
12975
- maxIterations : int , optional
12976
- The maximum number of iterations to calculate the page rank. Default is 100.
12977
- normalize : bool , optional
12978
- If set to True, the results will be normalized from 0 to 1. Otherwise, they won't be. Default is True.
12979
- directed : bool , optional
12980
- If set to True, the graph is considered as a directed graph. Otherwise, it will be considered as an undirected graph. Default is False.
12981
- key : str , optional
12982
- The dictionary key under which to store the page_rank score. Default is "page_rank"
12983
- colorKey : str , optional
12984
- The desired dictionary key under which to store the pagerank color. Default is "pr_color".
12985
- colorScale : str , optional
12986
- The desired type of plotly color scales to use (e.g. "viridis", "plasma"). Default is "viridis". For a full list of names, see https://plotly.com/python/builtin-colorscales/.
12987
- In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
12988
- mantissa : int , optional
12989
- The desired length of the mantissa.
12990
- tolerance : float , optional
12991
- The desired tolerance. Default is 0.0001.
12992
-
12993
- Returns
12994
- -------
12995
- list
12996
- The list of page ranks for the vertices in the graph.
14096
+ PageRank with stable vertex mapping (by coordinates) so neighbors resolve correctly.
14097
+ Handles dangling nodes; uses cached neighbor lists and L1 convergence.
12997
14098
  """
12998
14099
  from topologicpy.Vertex import Vertex
12999
14100
  from topologicpy.Helper import Helper
13000
14101
  from topologicpy.Dictionary import Dictionary
13001
14102
  from topologicpy.Topology import Topology
13002
14103
  from topologicpy.Color import Color
14104
+ from topologicpy.Graph import Graph
13003
14105
 
13004
14106
  vertices = Graph.Vertices(graph)
13005
- num_vertices = len(vertices)
13006
- if num_vertices < 1:
14107
+ n = len(vertices)
14108
+ if n < 1:
13007
14109
  print("Graph.PageRank - Error: The input graph parameter has no vertices. Returning None")
13008
14110
  return None
13009
- initial_score = 1.0 / num_vertices
13010
- values = [initial_score for vertex in vertices]
14111
+
14112
+ # ---- stable vertex key (coord-based) ----
14113
+ # Use a modest rounding to be robust to tiny numerical noise.
14114
+ # If your graphs can have distinct vertices at the exact same coords,
14115
+ # switch to a stronger key (e.g., include a unique ID from the vertex dictionary).
14116
+ def vkey(v, r=9):
14117
+ return (round(Vertex.X(v), r), round(Vertex.Y(v), r), round(Vertex.Z(v), r))
14118
+
14119
+ idx_of = {vkey(v): i for i, v in enumerate(vertices)}
14120
+
14121
+ # Helper that resolves an arbitrary Topologic vertex to our index
14122
+ def to_idx(u):
14123
+ return idx_of.get(vkey(u), None)
14124
+
14125
+ # ---- build neighbor lists ONCE (by indices) ----
14126
+ if directed:
14127
+ in_neighbors = [[] for _ in range(n)]
14128
+ out_neighbors = [[] for _ in range(n)]
14129
+
14130
+ for i, v in enumerate(vertices):
14131
+ inv = Graph.IncomingVertices(graph, v, directed=True)
14132
+ onv = Graph.OutgoingVertices(graph, v, directed=True)
14133
+ # map to indices, drop misses
14134
+ in_neighbors[i] = [j for u in inv if (j := to_idx(u)) is not None]
14135
+ out_neighbors[i] = [j for u in onv if (j := to_idx(u)) is not None]
14136
+ else:
14137
+ in_neighbors = [[] for _ in range(n)]
14138
+ out_neighbors = in_neighbors # same list objects is fine; we set both below
14139
+ for i, v in enumerate(vertices):
14140
+ nbrs = Graph.AdjacentVertices(graph, v)
14141
+ idxs = [j for u in nbrs if (j := to_idx(u)) is not None]
14142
+ in_neighbors[i] = idxs
14143
+ out_neighbors = in_neighbors # undirected: in == out
14144
+
14145
+ out_degree = [len(out_neighbors[i]) for i in range(n)]
14146
+ dangling = [i for i in range(n) if out_degree[i] == 0]
14147
+
14148
+ # ---- power iteration ----
14149
+ pr = [1.0 / n] * n
14150
+ base = (1.0 - alpha) / n
14151
+
13011
14152
  for _ in range(maxIterations):
13012
- new_scores = [0 for vertex in vertices]
13013
- for i, vertex in enumerate(vertices):
13014
- incoming_score = 0
13015
- for incoming_vertex in Graph.IncomingVertices(graph, vertex, directed=directed):
13016
- if len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed)) > 0:
13017
- vi = Vertex.Index(incoming_vertex, vertices, tolerance=tolerance)
13018
- if not vi == None:
13019
- incoming_score += values[vi] / len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed))
13020
- new_scores[i] = alpha * incoming_score + (1 - alpha) / num_vertices
13021
-
13022
- # Check for convergence
13023
- if all(abs(new_scores[i] - values[i]) <= tolerance for i in range(len(vertices))):
14153
+ # Distribute dangling mass uniformly
14154
+ dangling_mass = alpha * (sum(pr[i] for i in dangling) / n) if dangling else 0.0
14155
+
14156
+ new_pr = [base + dangling_mass] * n
14157
+
14158
+ # Sum contributions from incoming neighbors j: alpha * pr[j] / out_degree[j]
14159
+ for i in range(n):
14160
+ acc = 0.0
14161
+ for j in in_neighbors[i]:
14162
+ deg = out_degree[j]
14163
+ if deg > 0:
14164
+ acc += pr[j] / deg
14165
+ new_pr[i] += alpha * acc
14166
+
14167
+ # L1 convergence
14168
+ if sum(abs(new_pr[i] - pr[i]) for i in range(n)) <= tolerance:
14169
+ pr = new_pr
13024
14170
  break
14171
+ pr = new_pr
13025
14172
 
13026
- values = new_scores
13027
- if normalize == True:
13028
- if mantissa > 0: # We cannot round numbers from 0 to 1 with a mantissa = 0.
13029
- values = [round(v, mantissa) for v in Helper.Normalize(values)]
13030
- else:
13031
- values = Helper.Normalize(values)
13032
- min_value = 0
13033
- max_value = 1
14173
+ # ---- normalize & write dictionaries ----
14174
+ if normalize:
14175
+ pr = Helper.Normalize(pr)
14176
+ if mantissa > 0:
14177
+ pr = [round(v, mantissa) for v in pr]
14178
+ min_v, max_v = 0.0, 1.0
13034
14179
  else:
13035
- min_value = min(values)
13036
- max_value = max(values)
14180
+ min_v, max_v = (min(pr), max(pr)) if n > 0 else (0.0, 0.0)
13037
14181
 
13038
- for i, value in enumerate(values):
14182
+ for i, value in enumerate(pr):
13039
14183
  d = Topology.Dictionary(vertices[i])
13040
- color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
14184
+ color = Color.AnyToHex(
14185
+ Color.ByValueInRange(value, minValue=min_v, maxValue=max_v, colorScale=colorScale)
14186
+ )
13041
14187
  d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
13042
14188
  vertices[i] = Topology.SetDictionary(vertices[i], d)
14189
+
14190
+ return pr
14191
+
14192
+
14193
+ # @staticmethod
14194
+ # def PageRank_old(graph, alpha: float = 0.85, maxIterations: int = 100, normalize: bool = True, directed: bool = False, key: str = "page_rank", colorKey="pr_color", colorScale="viridis", mantissa: int = 6, tolerance: float = 0.0001):
14195
+ # """
14196
+ # Calculates PageRank scores for vertices in a directed graph. see https://en.wikipedia.org/wiki/PageRank.
14197
+
14198
+ # Parameters
14199
+ # ----------
14200
+ # graph : topologic_core.Graph
14201
+ # The input graph.
14202
+ # alpha : float , optional
14203
+ # The damping (dampening) factor. Default is 0.85. See https://en.wikipedia.org/wiki/PageRank.
14204
+ # maxIterations : int , optional
14205
+ # The maximum number of iterations to calculate the page rank. Default is 100.
14206
+ # normalize : bool , optional
14207
+ # If set to True, the results will be normalized from 0 to 1. Otherwise, they won't be. Default is True.
14208
+ # directed : bool , optional
14209
+ # If set to True, the graph is considered as a directed graph. Otherwise, it will be considered as an undirected graph. Default is False.
14210
+ # key : str , optional
14211
+ # The dictionary key under which to store the page_rank score. Default is "page_rank"
14212
+ # colorKey : str , optional
14213
+ # The desired dictionary key under which to store the pagerank color. Default is "pr_color".
14214
+ # colorScale : str , optional
14215
+ # The desired type of plotly color scales to use (e.g. "viridis", "plasma"). Default is "viridis". For a full list of names, see https://plotly.com/python/builtin-colorscales/.
14216
+ # In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
14217
+ # mantissa : int , optional
14218
+ # The desired length of the mantissa.
14219
+ # tolerance : float , optional
14220
+ # The desired tolerance. Default is 0.0001.
14221
+
14222
+ # Returns
14223
+ # -------
14224
+ # list
14225
+ # The list of page ranks for the vertices in the graph.
14226
+ # """
14227
+ # from topologicpy.Vertex import Vertex
14228
+ # from topologicpy.Helper import Helper
14229
+ # from topologicpy.Dictionary import Dictionary
14230
+ # from topologicpy.Topology import Topology
14231
+ # from topologicpy.Color import Color
14232
+
14233
+ # vertices = Graph.Vertices(graph)
14234
+ # num_vertices = len(vertices)
14235
+ # if num_vertices < 1:
14236
+ # print("Graph.PageRank - Error: The input graph parameter has no vertices. Returning None")
14237
+ # return None
14238
+ # initial_score = 1.0 / num_vertices
14239
+ # values = [initial_score for vertex in vertices]
14240
+ # for _ in range(maxIterations):
14241
+ # new_scores = [0 for vertex in vertices]
14242
+ # for i, vertex in enumerate(vertices):
14243
+ # incoming_score = 0
14244
+ # for incoming_vertex in Graph.IncomingVertices(graph, vertex, directed=directed):
14245
+ # if len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed)) > 0:
14246
+ # vi = Vertex.Index(incoming_vertex, vertices, tolerance=tolerance)
14247
+ # if not vi == None:
14248
+ # incoming_score += values[vi] / len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed))
14249
+ # new_scores[i] = alpha * incoming_score + (1 - alpha) / num_vertices
14250
+
14251
+ # # Check for convergence
14252
+ # if all(abs(new_scores[i] - values[i]) <= tolerance for i in range(len(vertices))):
14253
+ # break
14254
+
14255
+ # values = new_scores
14256
+ # if normalize == True:
14257
+ # if mantissa > 0: # We cannot round numbers from 0 to 1 with a mantissa = 0.
14258
+ # values = [round(v, mantissa) for v in Helper.Normalize(values)]
14259
+ # else:
14260
+ # values = Helper.Normalize(values)
14261
+ # min_value = 0
14262
+ # max_value = 1
14263
+ # else:
14264
+ # min_value = min(values)
14265
+ # max_value = max(values)
14266
+
14267
+ # for i, value in enumerate(values):
14268
+ # d = Topology.Dictionary(vertices[i])
14269
+ # color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
14270
+ # d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
14271
+ # vertices[i] = Topology.SetDictionary(vertices[i], d)
13043
14272
 
13044
- for i, v in enumerate(vertices):
13045
- d = Topology.Dictionary(v)
13046
- d = Dictionary.SetValueAtKey(d, key, values[i])
13047
- v = Topology.SetDictionary(v, d)
13048
- return values
14273
+ # for i, v in enumerate(vertices):
14274
+ # d = Topology.Dictionary(v)
14275
+ # d = Dictionary.SetValueAtKey(d, key, values[i])
14276
+ # v = Topology.SetDictionary(v, d)
14277
+ # return values
13049
14278
 
13050
14279
  @staticmethod
13051
14280
  def Partition(graph, method: str = "Betweenness", n: int = 2, m: int = 10, key: str ="partition",
@@ -13607,6 +14836,54 @@ class Graph:
13607
14836
  _ = graph.RemoveEdges([edge], tolerance) # Hook to Core
13608
14837
  return graph
13609
14838
 
14839
+ @staticmethod
14840
+ def RemoveIsolatedEdges(graph, removeVertices: bool = True, tolerance: float = 0.0001, silent: bool = False):
14841
+ """
14842
+ Removes all isolated edges from the input graph.
14843
+ Isolated edges are those whose vertices are not connected to any other edges.
14844
+ That is, they have a degree of 1.
14845
+
14846
+ Parameters
14847
+ ----------
14848
+ graph : topologic_core.Graph
14849
+ The input graph.
14850
+ removeVertices : bool , optional
14851
+ If set to True, the end vertices of the edges are also removed. Default is True.
14852
+ tolerance : float , optional
14853
+ The desired tolerance. Default is 0.0001.
14854
+ silent : bool , optional
14855
+ If set to True, error and warning messages are suppressed. Default is False.
14856
+
14857
+ Returns
14858
+ -------
14859
+ topologic_core.Graph
14860
+ The input graph with all isolated vertices removed.
14861
+
14862
+ """
14863
+ from topologicpy.Topology import Topology
14864
+ from topologicpy.Edge import Edge
14865
+
14866
+
14867
+ if not Topology.IsInstance(graph, "graph"):
14868
+ if not silent:
14869
+ print("Graph.RemoveIsolatedEdges - Error: The input graph parameter is not a valid graph. Returning None.")
14870
+ return None
14871
+
14872
+ edges = Graph.Edges(graph)
14873
+ if removeVertices == True:
14874
+ for edge in edges:
14875
+ va, vb = Edge.Vertices(edge)
14876
+ if Graph.VertexDegree(graph, va, tolerance=tolerance, silent=silent) == 1 and Graph.VertexDegree(graph, vb, tolerance=tolerance, silent=silent) == 1:
14877
+ graph = Graph.RemoveEdge(graph, edge, tolerance=tolerance)
14878
+ graph = Graph.RemoveVertex(graph, va, tolerance=tolerance)
14879
+ graph = Graph.RemoveVertex(graph, vb, tolerance=tolerance)
14880
+ else:
14881
+ for edge in edges:
14882
+ va, vb = Edge.Vertices(edge)
14883
+ if Graph.VertexDegree(graph, va, tolerance=tolerance, silent=silent) == 1 and Graph.VertexDegree(graph, vb, tolerance=tolerance, silent=silent) == 1:
14884
+ graph = Graph.RemoveEdge(graph, edge, tolerance=tolerance)
14885
+ return graph
14886
+
13610
14887
  @staticmethod
13611
14888
  def RemoveIsolatedVertices(graph, tolerance=0.0001):
13612
14889
  """