topologicpy 0.8.58__py3-none-any.whl → 0.8.59__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
@@ -1901,11 +1901,28 @@ class Graph:
1901
1901
  mantissa= mantissa)
1902
1902
  return bot_graph.serialize(format=format)
1903
1903
 
1904
+
1904
1905
  @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):
1906
+ def BetweennessCentrality(
1907
+ graph,
1908
+ method: str = "vertex",
1909
+ weightKey: str = "length",
1910
+ normalize: bool = False,
1911
+ nxCompatible: bool = False,
1912
+ key: str = "betweenness_centrality",
1913
+ colorKey: str = "bc_color",
1914
+ colorScale: str = "viridis",
1915
+ mantissa: int = 6,
1916
+ tolerance: float = 0.001,
1917
+ silent: bool = False
1918
+ ):
1906
1919
  """
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
-
1920
+ 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.
1921
+ Optimized betweenness centrality (undirected) using Brandes:
1922
+ - Unweighted: O(VE) BFS per source
1923
+ - Weighted: Dijkstra-Brandes with binary heap
1924
+ - Vertex or Edge mode
1925
+ - Optional NetworkX-compatible normalization or 0..1 rescale
1909
1926
  Parameters
1910
1927
  ----------
1911
1928
  graph : topologic_core.Graph
@@ -1936,60 +1953,337 @@ class Graph:
1936
1953
  -------
1937
1954
  list
1938
1955
  The betweenness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
1939
-
1940
1956
  """
1941
- import warnings
1957
+ from collections import deque
1958
+ import math
1942
1959
 
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
-
1960
+ from topologicpy.Topology import Topology
1958
1961
  from topologicpy.Dictionary import Dictionary
1959
1962
  from topologicpy.Color import Color
1960
- from topologicpy.Topology import Topology
1961
1963
  from topologicpy.Helper import Helper
1964
+ from topologicpy.Vertex import Vertex
1965
+ from topologicpy.Edge import Edge
1966
+ # We are inside Graph.* context; Graph.<...> methods available.
1967
+
1968
+ # ---------- validate ----------
1969
+ if not Topology.IsInstance(graph, "graph"):
1970
+ if not silent:
1971
+ print("Graph.BetweennessCentrality - Error: The input is not a valid Graph. Returning None.")
1972
+ return None
1973
+
1974
+ vertices = Graph.Vertices(graph)
1975
+ n = len(vertices)
1976
+ if n == 0:
1977
+ if not silent:
1978
+ print("Graph.BetweennessCentrality - Warning: Graph has no vertices. Returning [].")
1979
+ return []
1980
+
1981
+ method_l = (method or "vertex").lower()
1982
+ compute_edges = "edge" in method_l
1962
1983
 
1963
- if weightKey:
1964
- if "len" in weightKey.lower() or "dis" in weightKey.lower():
1984
+ # ---------- stable vertex indexing ----------
1985
+ def vkey(v, r=9):
1986
+ d = Topology.Dictionary(v)
1987
+ vid = Dictionary.ValueAtKey(d, "id")
1988
+ if vid is not None:
1989
+ return ("id", vid)
1990
+ return ("xyz", round(Vertex.X(v), r), round(Vertex.Y(v), r), round(Vertex.Z(v), r))
1991
+
1992
+ idx_of = {vkey(v): i for i, v in enumerate(vertices)}
1993
+
1994
+ # ---------- weight handling ----------
1995
+ dist_attr = None
1996
+ if isinstance(weightKey, str) and weightKey:
1997
+ wl = weightKey.lower()
1998
+ if ("len" in wl) or ("dis" in wl):
1965
1999
  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)]
2000
+ dist_attr = weightKey
2001
+
2002
+ def edge_weight(e):
2003
+ if dist_attr == "length":
2004
+ try:
2005
+ return float(Edge.Length(e))
2006
+ except Exception:
2007
+ return 1.0
2008
+ elif dist_attr:
2009
+ try:
2010
+ d = Topology.Dictionary(e)
2011
+ w = Dictionary.ValueAtKey(d, dist_attr)
2012
+ return float(w) if (w is not None) else 1.0
2013
+ except Exception:
2014
+ return 1.0
1978
2015
  else:
1979
- values = Helper.Normalize(values)
1980
- min_value = 0
1981
- max_value = 1
2016
+ return 1.0
2017
+
2018
+ # ---------- build undirected adjacency (min weight on multi-edges) ----------
2019
+ edges = Graph.Edges(graph)
2020
+ # For per-edge outputs in input order:
2021
+ edge_end_idx = [] # [(iu, iv)] aligned with edges list (undirected as sorted pair)
2022
+ tmp_adj = [dict() for _ in range(n)] # temporary: dedup by neighbor with min weight
2023
+
2024
+ for e in edges:
2025
+ try:
2026
+ u = Edge.StartVertex(e)
2027
+ v = Edge.EndVertex(e)
2028
+ except Exception:
2029
+ continue
2030
+ iu = idx_of.get(vkey(u))
2031
+ iv = idx_of.get(vkey(v))
2032
+ if iu is None or iv is None or iu == iv:
2033
+ # still store mapping for return list to avoid index error
2034
+ pair = None
2035
+ else:
2036
+ w = edge_weight(e)
2037
+ # keep minimal weight for duplicates
2038
+ pu = tmp_adj[iu].get(iv)
2039
+ if (pu is None) or (w < pu):
2040
+ tmp_adj[iu][iv] = w
2041
+ tmp_adj[iv][iu] = w
2042
+ pair = (iu, iv) if iu < iv else (iv, iu)
2043
+ edge_end_idx.append(pair)
2044
+
2045
+ # finalize adjacency as list-of-tuples for fast loops
2046
+ adj = [list(neigh.items()) for neigh in tmp_adj] # adj[i] = [(j, w), ...]
2047
+ del tmp_adj
2048
+
2049
+ # detect weightedness
2050
+ weighted = False
2051
+ for i in range(n):
2052
+ if any(abs(w - 1.0) > 1e-12 for _, w in adj[i]):
2053
+ weighted = True
2054
+ break
2055
+
2056
+ # ---------- Brandes ----------
2057
+ CB_v = [0.0] * n
2058
+ CB_e = {} # key: (min_i, max_j) -> score (only if compute_edges)
2059
+
2060
+ if n > 1:
2061
+ if not weighted:
2062
+ # Unweighted BFS Brandes
2063
+ for s in range(n):
2064
+ S = []
2065
+ P = [[] for _ in range(n)]
2066
+ sigma = [0.0] * n
2067
+ sigma[s] = 1.0
2068
+ dist = [-1] * n
2069
+ dist[s] = 0
2070
+ Q = deque([s])
2071
+ pushQ, popQ = Q.append, Q.popleft
2072
+
2073
+ while Q:
2074
+ v = popQ()
2075
+ S.append(v)
2076
+ dv = dist[v]
2077
+ sv = sigma[v]
2078
+ for w, _ in adj[v]:
2079
+ if dist[w] < 0:
2080
+ dist[w] = dv + 1
2081
+ pushQ(w)
2082
+ if dist[w] == dv + 1:
2083
+ sigma[w] += sv
2084
+ P[w].append(v)
2085
+
2086
+ delta = [0.0] * n
2087
+ while S:
2088
+ w = S.pop()
2089
+ sw = sigma[w]
2090
+ dw = 1.0 + delta[w]
2091
+ for v in P[w]:
2092
+ c = (sigma[v] / sw) * dw
2093
+ delta[v] += c
2094
+ if compute_edges:
2095
+ a, b = (v, w) if v < w else (w, v)
2096
+ CB_e[a, b] = CB_e.get((a, b), 0.0) + c
2097
+ if w != s:
2098
+ CB_v[w] += delta[w]
2099
+ else:
2100
+ # Weighted Dijkstra-Brandes
2101
+ import heapq
2102
+ EPS = 1e-12
2103
+ for s in range(n):
2104
+ S = []
2105
+ P = [[] for _ in range(n)]
2106
+ sigma = [0.0] * n
2107
+ sigma[s] = 1.0
2108
+ dist = [math.inf] * n
2109
+ dist[s] = 0.0
2110
+ H = [(0.0, s)]
2111
+ pushH, popH = heapq.heappush, heapq.heappop
2112
+
2113
+ while H:
2114
+ dv, v = popH(H)
2115
+ if dv > dist[v] + EPS:
2116
+ continue
2117
+ S.append(v)
2118
+ sv = sigma[v]
2119
+ for w, wgt in adj[v]:
2120
+ nd = dv + wgt
2121
+ dw = dist[w]
2122
+ if nd + EPS < dw:
2123
+ dist[w] = nd
2124
+ sigma[w] = sv
2125
+ P[w] = [v]
2126
+ pushH(H, (nd, w))
2127
+ elif abs(nd - dw) <= EPS:
2128
+ sigma[w] += sv
2129
+ P[w].append(v)
2130
+
2131
+ delta = [0.0] * n
2132
+ while S:
2133
+ w = S.pop()
2134
+ sw = sigma[w]
2135
+ if sw == 0.0:
2136
+ continue
2137
+ dw = 1.0 + delta[w]
2138
+ for v in P[w]:
2139
+ c = (sigma[v] / sw) * dw
2140
+ delta[v] += c
2141
+ if compute_edges:
2142
+ a, b = (v, w) if v < w else (w, v)
2143
+ CB_e[a, b] = CB_e.get((a, b), 0.0) + c
2144
+ if w != s:
2145
+ CB_v[w] += delta[w]
2146
+
2147
+ # ---------- normalization ----------
2148
+ # NetworkX-compatible normalization (undirected):
2149
+ # vertices/edges factor = 2/((n-1)(n-2)) for n > 2 when normalized=True
2150
+ if nxCompatible:
2151
+ if normalize and n > 2:
2152
+ scale = 2.0 / ((n - 1) * (n - 2))
2153
+ CB_v = [v * scale for v in CB_v]
2154
+ if compute_edges:
2155
+ for k in list(CB_e.keys()):
2156
+ CB_e[k] *= scale
2157
+ # else: leave raw Brandes scores (normalized=False behavior)
2158
+ values_raw = CB_v if not compute_edges else [
2159
+ CB_e.get(tuple(sorted(pair)) if pair else None, 0.0) if pair else 0.0
2160
+ for pair in edge_end_idx
2161
+ ]
2162
+ values_for_return = values_raw
1982
2163
  else:
1983
- min_value = min(values)
1984
- max_value = max(values)
2164
+ # Rescale to [0,1] regardless of theoretical normalization
2165
+ values_raw = CB_v if not compute_edges else [
2166
+ CB_e.get(tuple(sorted(pair)) if pair else None, 0.0) if pair else 0.0
2167
+ for pair in edge_end_idx
2168
+ ]
2169
+ values_for_return = Helper.Normalize(values_raw)
1985
2170
 
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)
2171
+ # rounding once
2172
+ if mantissa is not None and mantissa >= 0:
2173
+ values_for_return = [round(v, mantissa) for v in values_for_return]
1991
2174
 
1992
- return values
2175
+ # ---------- color mapping ----------
2176
+ if values_for_return:
2177
+ min_v, max_v = min(values_for_return), max(values_for_return)
2178
+ else:
2179
+ min_v, max_v = 0.0, 1.0
2180
+ if abs(max_v - min_v) < tolerance:
2181
+ max_v = min_v + tolerance
2182
+
2183
+ # annotate (vertices or edges) in input order
2184
+ if compute_edges:
2185
+ elems = edges
2186
+ else:
2187
+ elems = vertices
2188
+ for i, value in enumerate(values_for_return):
2189
+ d = Topology.Dictionary(elems[i])
2190
+ color_hex = Color.AnyToHex(
2191
+ Color.ByValueInRange(value, minValue=min_v, maxValue=max_v, colorScale=colorScale)
2192
+ )
2193
+ d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color_hex])
2194
+ elems[i] = Topology.SetDictionary(elems[i], d)
2195
+
2196
+ return values_for_return
2197
+
2198
+ # @staticmethod
2199
+ # 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):
2200
+ # """
2201
+ # 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.
2202
+
2203
+ # Parameters
2204
+ # ----------
2205
+ # graph : topologic_core.Graph
2206
+ # The input graph.
2207
+ # method : str , optional
2208
+ # The method of computing the betweenness centrality. The options are "vertex" or "edge". Default is "vertex".
2209
+ # weightKey : str , optional
2210
+ # If specified, the value in the connected edges' dictionary specified by the weightKey string will be aggregated to calculate
2211
+ # the shortest path. If a numeric value cannot be retrieved from an edge, a value of 1 is used instead.
2212
+ # 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.
2213
+ # normalize : bool , optional
2214
+ # If set to True, the values are normalized to be in the range 0 to 1. Otherwise they are not. Default is False.
2215
+ # nxCompatible : bool , optional
2216
+ # 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.
2217
+ # key : str , optional
2218
+ # The desired dictionary key under which to store the betweenness centrality score. Default is "betweenness_centrality".
2219
+ # colorKey : str , optional
2220
+ # The desired dictionary key under which to store the betweenness centrality color. Default is "betweenness_centrality".
2221
+ # colorScale : str , optional
2222
+ # 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/.
2223
+ # In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
2224
+ # mantissa : int , optional
2225
+ # The number of decimal places to round the result to. Default is 6.
2226
+ # tolerance : float , optional
2227
+ # The desired tolerance. Default is 0.0001.
2228
+
2229
+ # Returns
2230
+ # -------
2231
+ # list
2232
+ # The betweenness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
2233
+
2234
+ # """
2235
+ # import warnings
2236
+
2237
+ # try:
2238
+ # import networkx as nx
2239
+ # except:
2240
+ # print("Graph.BetwennessCentrality - Information: Installing required networkx library.")
2241
+ # try:
2242
+ # os.system("pip install networkx")
2243
+ # except:
2244
+ # os.system("pip install networkx --user")
2245
+ # try:
2246
+ # import networkx as nx
2247
+ # print("Graph.BetwennessCentrality - Infromation: networkx library installed correctly.")
2248
+ # except:
2249
+ # warnings.warn("Graph.BetwennessCentrality - Error: Could not import networkx. Please try to install networkx manually. Returning None.")
2250
+ # return None
2251
+
2252
+ # from topologicpy.Dictionary import Dictionary
2253
+ # from topologicpy.Color import Color
2254
+ # from topologicpy.Topology import Topology
2255
+ # from topologicpy.Helper import Helper
2256
+
2257
+ # if weightKey:
2258
+ # if "len" in weightKey.lower() or "dis" in weightKey.lower():
2259
+ # weightKey = "length"
2260
+ # nx_graph = Graph.NetworkXGraph(graph)
2261
+ # if "vert" in method.lower():
2262
+ # elements = Graph.Vertices(graph)
2263
+ # elements_dict = nx.betweenness_centrality(nx_graph, normalized=normalize, weight=weightKey)
2264
+ # values = [round(value, mantissa) for value in list(elements_dict.values())]
2265
+ # else:
2266
+ # elements = Graph.Edges(graph)
2267
+ # elements_dict = nx.edge_betweenness_centrality(nx_graph, normalized=normalize, weight=weightKey)
2268
+ # values = [round(value, mantissa) for value in list(elements_dict.values())]
2269
+ # if nxCompatible == False:
2270
+ # if mantissa > 0: # We cannot have values in the range 0 to 1 with a mantissa < 1
2271
+ # values = [round(v, mantissa) for v in Helper.Normalize(values)]
2272
+ # else:
2273
+ # values = Helper.Normalize(values)
2274
+ # min_value = 0
2275
+ # max_value = 1
2276
+ # else:
2277
+ # min_value = min(values)
2278
+ # max_value = max(values)
2279
+
2280
+ # for i, value in enumerate(values):
2281
+ # d = Topology.Dictionary(elements[i])
2282
+ # color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
2283
+ # d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
2284
+ # elements[i] = Topology.SetDictionary(elements[i], d)
2285
+
2286
+ # return values
1993
2287
 
1994
2288
  @staticmethod
1995
2289
  def BetweennessPartition(graph, n=2, m=10, key="partition", tolerance=0.0001, silent=False):
@@ -3246,432 +3540,432 @@ class Graph:
3246
3540
  edges = get_edges(relationships, vertices)
3247
3541
  return Graph.ByVerticesEdges(vertices, edges)
3248
3542
 
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.
3543
+ # @staticmethod
3544
+ # def ByIFCFile_old(file,
3545
+ # includeTypes: list = [],
3546
+ # excludeTypes: list = [],
3547
+ # includeRels: list = [],
3548
+ # excludeRels: list = [],
3549
+ # transferDictionaries: bool = False,
3550
+ # useInternalVertex: bool = False,
3551
+ # storeBREP: bool = False,
3552
+ # removeCoplanarFaces: bool = False,
3553
+ # xMin: float = -0.5, yMin: float = -0.5, zMin: float = -0.5,
3554
+ # xMax: float = 0.5, yMax: float = 0.5, zMax: float = 0.5,
3555
+ # tolerance: float = 0.0001):
3556
+ # """
3557
+ # Create a Graph from an IFC file. This code is partially based on code from Bruno Postle.
3264
3558
 
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.
3299
-
3300
- Returns
3301
- -------
3302
- topologic_core.Graph
3303
- The created graph.
3559
+ # Parameters
3560
+ # ----------
3561
+ # file : file
3562
+ # The input IFC file
3563
+ # includeTypes : list , optional
3564
+ # A list of IFC object types to include in the graph. Default is [] which means all object types are included.
3565
+ # excludeTypes : list , optional
3566
+ # A list of IFC object types to exclude from the graph. Default is [] which mean no object type is excluded.
3567
+ # includeRels : list , optional
3568
+ # A list of IFC relationship types to include in the graph. Default is [] which means all relationship types are included.
3569
+ # excludeRels : list , optional
3570
+ # A list of IFC relationship types to exclude from the graph. Default is [] which mean no relationship type is excluded.
3571
+ # transferDictionaries : bool , optional
3572
+ # If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. Default is False.
3573
+ # useInternalVertex : bool , optional
3574
+ # If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. Default is False.
3575
+ # storeBREP : bool , optional
3576
+ # If set to True, store the BRep of the subtopology in its representative vertex. Default is False.
3577
+ # removeCoplanarFaces : bool , optional
3578
+ # If set to True, coplanar faces are removed. Otherwise they are not. Default is False.
3579
+ # xMin : float, optional
3580
+ # The desired minimum value to assign for a vertex's X coordinate. Default is -0.5.
3581
+ # yMin : float, optional
3582
+ # The desired minimum value to assign for a vertex's Y coordinate. Default is -0.5.
3583
+ # zMin : float, optional
3584
+ # The desired minimum value to assign for a vertex's Z coordinate. Default is -0.5.
3585
+ # xMax : float, optional
3586
+ # The desired maximum value to assign for a vertex's X coordinate. Default is 0.5.
3587
+ # yMax : float, optional
3588
+ # The desired maximum value to assign for a vertex's Y coordinate. Default is 0.5.
3589
+ # zMax : float, optional
3590
+ # The desired maximum value to assign for a vertex's Z coordinate. Default is 0.5.
3591
+ # tolerance : float , optional
3592
+ # The desired tolerance. Default is 0.0001.
3304
3593
 
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
3594
+ # Returns
3595
+ # -------
3596
+ # topologic_core.Graph
3597
+ # The created graph.
3333
3598
 
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
3599
+ # """
3600
+ # from topologicpy.Topology import Topology
3601
+ # from topologicpy.Vertex import Vertex
3602
+ # from topologicpy.Edge import Edge
3603
+ # from topologicpy.Graph import Graph
3604
+ # from topologicpy.Dictionary import Dictionary
3605
+ # try:
3606
+ # import ifcopenshell
3607
+ # import ifcopenshell.util.placement
3608
+ # import ifcopenshell.util.element
3609
+ # import ifcopenshell.util.shape
3610
+ # import ifcopenshell.geom
3611
+ # except:
3612
+ # print("Graph.ByIFCFile - Warning: Installing required ifcopenshell library.")
3613
+ # try:
3614
+ # os.system("pip install ifcopenshell")
3615
+ # except:
3616
+ # os.system("pip install ifcopenshell --user")
3617
+ # try:
3618
+ # import ifcopenshell
3619
+ # import ifcopenshell.util.placement
3620
+ # import ifcopenshell.util.element
3621
+ # import ifcopenshell.util.shape
3622
+ # import ifcopenshell.geom
3623
+ # print("Graph.ByIFCFile - Warning: ifcopenshell library installed correctly.")
3624
+ # except:
3625
+ # warnings.warn("Graph.ByIFCFile - Error: Could not import ifcopenshell. Please try to install ifcopenshell manually. Returning None.")
3626
+ # return None
3627
+
3628
+ # import random
3629
+
3630
+ # def vertexAtKeyValue(vertices, key, value):
3631
+ # for v in vertices:
3632
+ # d = Topology.Dictionary(v)
3633
+ # d_value = Dictionary.ValueAtKey(d, key)
3634
+ # if value == d_value:
3635
+ # return v
3636
+ # return None
3385
3637
 
3386
- def get_psets(entity):
3387
- # Initialize the PSET dictionary for this entity
3388
- psets = {}
3638
+ # def IFCObjects(ifc_file, include=[], exclude=[]):
3639
+ # include = [s.lower() for s in include]
3640
+ # exclude = [s.lower() for s in exclude]
3641
+ # all_objects = ifc_file.by_type('IfcProduct')
3642
+ # return_objects = []
3643
+ # for obj in all_objects:
3644
+ # is_a = obj.is_a().lower()
3645
+ # if is_a in exclude:
3646
+ # continue
3647
+ # if is_a in include or len(include) == 0:
3648
+ # return_objects.append(obj)
3649
+ # return return_objects
3650
+
3651
+ # def IFCObjectTypes(ifc_file):
3652
+ # products = IFCObjects(ifc_file)
3653
+ # obj_types = []
3654
+ # for product in products:
3655
+ # obj_types.append(product.is_a())
3656
+ # obj_types = list(set(obj_types))
3657
+ # obj_types.sort()
3658
+ # return obj_types
3659
+
3660
+ # def IFCRelationshipTypes(ifc_file):
3661
+ # rel_types = [ifc_rel.is_a() for ifc_rel in ifc_file.by_type("IfcRelationship")]
3662
+ # rel_types = list(set(rel_types))
3663
+ # rel_types.sort()
3664
+ # return rel_types
3665
+
3666
+ # def IFCRelationships(ifc_file, include=[], exclude=[]):
3667
+ # include = [s.lower() for s in include]
3668
+ # exclude = [s.lower() for s in exclude]
3669
+ # rel_types = [ifc_rel.is_a() for ifc_rel in ifc_file.by_type("IfcRelationship")]
3670
+ # rel_types = list(set(rel_types))
3671
+ # relationships = []
3672
+ # for ifc_rel in ifc_file.by_type("IfcRelationship"):
3673
+ # rel_type = ifc_rel.is_a().lower()
3674
+ # if rel_type in exclude:
3675
+ # continue
3676
+ # if rel_type in include or len(include) == 0:
3677
+ # relationships.append(ifc_rel)
3678
+ # return relationships
3679
+
3680
+ # def get_psets(entity):
3681
+ # # Initialize the PSET dictionary for this entity
3682
+ # psets = {}
3389
3683
 
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.")
3684
+ # # Check if the entity has a GlobalId
3685
+ # if not hasattr(entity, 'GlobalId'):
3686
+ # raise ValueError("The provided entity does not have a GlobalId.")
3393
3687
 
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
3688
+ # # Get the property sets related to this entity
3689
+ # for definition in entity.IsDefinedBy:
3690
+ # if definition.is_a('IfcRelDefinesByProperties'):
3691
+ # property_set = definition.RelatingPropertyDefinition
3398
3692
 
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
3693
+ # # Check if it is a property set
3694
+ # if not property_set == None:
3695
+ # if property_set.is_a('IfcPropertySet'):
3696
+ # pset_name = "IFC_"+property_set.Name
3403
3697
 
3404
- # Dictionary to hold individual properties
3405
- properties = {}
3698
+ # # Dictionary to hold individual properties
3699
+ # properties = {}
3406
3700
 
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
3701
+ # # Iterate over the properties in the PSET
3702
+ # for prop in property_set.HasProperties:
3703
+ # if prop.is_a('IfcPropertySingleValue'):
3704
+ # # Get the property name and value
3705
+ # prop_name = "IFC_"+prop.Name
3706
+ # prop_value = prop.NominalValue.wrappedValue if prop.NominalValue else None
3707
+ # properties[prop_name] = prop_value
3414
3708
 
3415
- # Add this PSET to the dictionary for this entity
3416
- psets[pset_name] = properties
3417
- return psets
3709
+ # # Add this PSET to the dictionary for this entity
3710
+ # psets[pset_name] = properties
3711
+ # return psets
3418
3712
 
3419
- def get_color_transparency_material(entity):
3420
- import random
3713
+ # def get_color_transparency_material(entity):
3714
+ # import random
3421
3715
 
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
3716
+ # # Set default Material Name and ID
3717
+ # material_list = []
3718
+ # # Set default transparency based on entity type or material
3719
+ # default_transparency = 0.0
3426
3720
 
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
3721
+ # # Check if the entity is an opening or made of glass
3722
+ # is_a = entity.is_a().lower()
3723
+ # if "opening" in is_a or "window" in is_a or "door" in is_a or "space" in is_a:
3724
+ # default_transparency = 0.7
3725
+ # elif "space" in is_a:
3726
+ # default_transparency = 0.8
3433
3727
 
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
3728
+ # # Check if the entity has constituent materials (e.g., glass)
3729
+ # else:
3730
+ # # Check for associated materials (ConstituentMaterial or direct material assignment)
3731
+ # materials_checked = False
3732
+ # if hasattr(entity, 'HasAssociations'):
3733
+ # for rel in entity.HasAssociations:
3734
+ # if rel.is_a('IfcRelAssociatesMaterial'):
3735
+ # material = rel.RelatingMaterial
3736
+ # if material.is_a('IfcMaterial') and 'glass' in material.Name.lower():
3737
+ # default_transparency = 0.5
3738
+ # materials_checked = True
3739
+ # elif material.is_a('IfcMaterialLayerSetUsage'):
3740
+ # material_layers = material.ForLayerSet.MaterialLayers
3741
+ # for layer in material_layers:
3742
+ # material_list.append(layer.Material.Name)
3743
+ # if 'glass' in layer.Material.Name.lower():
3744
+ # default_transparency = 0.5
3745
+ # materials_checked = True
3452
3746
 
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
3747
+ # # Check for ConstituentMaterial if available
3748
+ # if hasattr(entity, 'HasAssociations') and not materials_checked:
3749
+ # for rel in entity.HasAssociations:
3750
+ # if rel.is_a('IfcRelAssociatesMaterial'):
3751
+ # material = rel.RelatingMaterial
3752
+ # if material.is_a('IfcMaterialConstituentSet'):
3753
+ # for constituent in material.MaterialConstituents:
3754
+ # material_list.append(constituent.Material.Name)
3755
+ # if 'glass' in constituent.Material.Name.lower():
3756
+ # default_transparency = 0.5
3757
+ # materials_checked = True
3758
+
3759
+ # # Check if the entity has ShapeAspects with associated materials or styles
3760
+ # if hasattr(entity, 'HasShapeAspects') and not materials_checked:
3761
+ # for shape_aspect in entity.HasShapeAspects:
3762
+ # if hasattr(shape_aspect, 'StyledByItem') and shape_aspect.StyledByItem:
3763
+ # for styled_item in shape_aspect.StyledByItem:
3764
+ # for style in styled_item.Styles:
3765
+ # if style.is_a('IfcSurfaceStyle'):
3766
+ # for surface_style in style.Styles:
3767
+ # if surface_style.is_a('IfcSurfaceStyleRendering'):
3768
+ # transparency = getattr(surface_style, 'Transparency', default_transparency)
3769
+ # if transparency > 0:
3770
+ # default_transparency = transparency
3771
+
3772
+ # # Try to get the actual color and transparency if defined
3773
+ # if hasattr(entity, 'Representation') and entity.Representation:
3774
+ # for rep in entity.Representation.Representations:
3775
+ # for item in rep.Items:
3776
+ # if hasattr(item, 'StyledByItem') and item.StyledByItem:
3777
+ # for styled_item in item.StyledByItem:
3778
+ # if hasattr(styled_item, 'Styles'):
3779
+ # for style in styled_item.Styles:
3780
+ # if style.is_a('IfcSurfaceStyle'):
3781
+ # for surface_style in style.Styles:
3782
+ # if surface_style.is_a('IfcSurfaceStyleRendering'):
3783
+ # color = surface_style.SurfaceColour
3784
+ # transparency = getattr(surface_style, 'Transparency', default_transparency)
3785
+ # return (color.Red*255, color.Green*255, color.Blue*255), transparency, material_list
3492
3786
 
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())
3787
+ # # If no color is defined, return a consistent random color based on the entity type
3788
+ # if "wall" in is_a:
3789
+ # color = (175, 175, 175)
3790
+ # elif "slab" in is_a:
3791
+ # color = (200, 200, 200)
3792
+ # elif "space" in is_a:
3793
+ # color = (250, 250, 250)
3794
+ # else:
3795
+ # random.seed(hash(is_a))
3796
+ # color = (random.random(), random.random(), random.random())
3503
3797
 
3504
- return color, default_transparency, material_list
3798
+ # return color, default_transparency, material_list
3505
3799
 
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
3528
-
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)
3800
+ # def vertexByIFCObject(ifc_object, object_types, restrict=False):
3801
+ # settings = ifcopenshell.geom.settings()
3802
+ # settings.set(settings.USE_WORLD_COORDS,True)
3803
+ # try:
3804
+ # shape = ifcopenshell.geom.create_shape(settings, ifc_object)
3805
+ # except:
3806
+ # shape = None
3807
+ # if shape or restrict == False: #Only add vertices of entities that have 3D geometries.
3808
+ # obj_id = ifc_object.id()
3809
+ # psets = ifcopenshell.util.element.get_psets(ifc_object)
3810
+ # obj_type = ifc_object.is_a()
3811
+ # obj_type_id = object_types.index(obj_type)
3812
+ # name = "Untitled"
3813
+ # LongName = "Untitled"
3814
+ # try:
3815
+ # name = ifc_object.Name
3816
+ # except:
3817
+ # name = "Untitled"
3818
+ # try:
3819
+ # LongName = ifc_object.LongName
3820
+ # except:
3821
+ # LongName = name
3822
+
3823
+ # if name == None:
3824
+ # name = "Untitled"
3825
+ # if LongName == None:
3826
+ # LongName = "Untitled"
3827
+ # label = str(obj_id)+" "+LongName+" ("+obj_type+" "+str(obj_type_id)+")"
3828
+ # try:
3829
+ # grouped_verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
3830
+ # vertices = [Vertex.ByCoordinates(list(coords)) for coords in grouped_verts]
3831
+ # centroid = Vertex.Centroid(vertices)
3832
+ # except:
3833
+ # x = random.uniform(xMin,xMax)
3834
+ # y = random.uniform(yMin,yMax)
3835
+ # z = random.uniform(zMin,zMax)
3836
+ # centroid = Vertex.ByCoordinates(x, y, z)
3543
3837
 
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 = []
3838
+ # # Store relevant information
3839
+ # if transferDictionaries == True:
3840
+ # color, transparency, material_list = get_color_transparency_material(ifc_object)
3841
+ # if color == None:
3842
+ # color = "white"
3843
+ # if transparency == None:
3844
+ # transparency = 0
3845
+ # entity_dict = {
3846
+ # "TOPOLOGIC_id": str(Topology.UUID(centroid)),
3847
+ # "TOPOLOGIC_name": getattr(ifc_object, 'Name', "Untitled"),
3848
+ # "TOPOLOGIC_type": Topology.TypeAsString(centroid),
3849
+ # "TOPOLOGIC_color": color,
3850
+ # "TOPOLOGIC_opacity": 1.0 - transparency,
3851
+ # "IFC_global_id": getattr(ifc_object, 'GlobalId', 0),
3852
+ # "IFC_name": getattr(ifc_object, 'Name', "Untitled"),
3853
+ # "IFC_type": ifc_object.is_a(),
3854
+ # "IFC_material_list": material_list,
3855
+ # }
3856
+ # topology_dict = Dictionary.ByPythonDictionary(entity_dict)
3857
+ # # Get PSETs dictionary
3858
+ # pset_python_dict = get_psets(ifc_object)
3859
+ # pset_dict = Dictionary.ByPythonDictionary(pset_python_dict)
3860
+ # topology_dict = Dictionary.ByMergedDictionaries([topology_dict, pset_dict])
3861
+ # if storeBREP == True or useInternalVertex == True:
3862
+ # shape_topology = None
3863
+ # if hasattr(ifc_object, "Representation") and ifc_object.Representation:
3864
+ # for rep in ifc_object.Representation.Representations:
3865
+ # if rep.is_a("IfcShapeRepresentation"):
3866
+ # try:
3867
+ # # Generate the geometry for this entity
3868
+ # shape = ifcopenshell.geom.create_shape(settings, ifc_object)
3869
+ # # Get grouped vertices and grouped faces
3870
+ # grouped_verts = shape.geometry.verts
3871
+ # verts = [ [grouped_verts[i], grouped_verts[i + 1], grouped_verts[i + 2]] for i in range(0, len(grouped_verts), 3)]
3872
+ # grouped_edges = shape.geometry.edges
3873
+ # edges = [[grouped_edges[i], grouped_edges[i + 1]] for i in range(0, len(grouped_edges), 2)]
3874
+ # grouped_faces = shape.geometry.faces
3875
+ # faces = [ [grouped_faces[i], grouped_faces[i + 1], grouped_faces[i + 2]] for i in range(0, len(grouped_faces), 3)]
3876
+ # shape_topology = Topology.ByGeometry(verts, edges, faces, silent=True)
3877
+ # if not shape_topology == None:
3878
+ # if removeCoplanarFaces == True:
3879
+ # shape_topology = Topology.RemoveCoplanarFaces(shape_topology, epsilon=0.0001)
3880
+ # except:
3881
+ # pass
3882
+ # if not shape_topology == None and storeBREP:
3883
+ # topology_dict = Dictionary.SetValuesAtKeys(topology_dict, ["brep", "brepType", "brepTypeString"], [Topology.BREPString(shape_topology), Topology.Type(shape_topology), Topology.TypeAsString(shape_topology)])
3884
+ # if not shape_topology == None and useInternalVertex == True:
3885
+ # centroid = Topology.InternalVertex(shape_topology)
3886
+ # centroid = Topology.SetDictionary(centroid, topology_dict)
3887
+ # return centroid
3888
+ # return None
3599
3889
 
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
3660
-
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
3890
+ # def edgesByIFCRelationships(ifc_relationships, ifc_types, vertices):
3891
+ # tuples = []
3892
+ # edges = []
3893
+
3894
+ # for ifc_rel in ifc_relationships:
3895
+ # source = None
3896
+ # destinations = []
3897
+ # if ifc_rel.is_a("IfcRelConnectsPorts"):
3898
+ # source = ifc_rel.RelatingPort
3899
+ # destinations = ifc_rel.RelatedPorts
3900
+ # elif ifc_rel.is_a("IfcRelConnectsPortToElement"):
3901
+ # source = ifc_rel.RelatingPort
3902
+ # destinations = [ifc_rel.RelatedElement]
3903
+ # elif ifc_rel.is_a("IfcRelAggregates"):
3904
+ # source = ifc_rel.RelatingObject
3905
+ # destinations = ifc_rel.RelatedObjects
3906
+ # elif ifc_rel.is_a("IfcRelNests"):
3907
+ # source = ifc_rel.RelatingObject
3908
+ # destinations = ifc_rel.RelatedObjects
3909
+ # elif ifc_rel.is_a("IfcRelAssignsToGroup"):
3910
+ # source = ifc_rel.RelatingGroup
3911
+ # destinations = ifc_rel.RelatedObjects
3912
+ # elif ifc_rel.is_a("IfcRelConnectsPathElements"):
3913
+ # source = ifc_rel.RelatingElement
3914
+ # destinations = [ifc_rel.RelatedElement]
3915
+ # elif ifc_rel.is_a("IfcRelConnectsStructuralMember"):
3916
+ # source = ifc_rel.RelatingStructuralMember
3917
+ # destinations = [ifc_rel.RelatedStructuralConnection]
3918
+ # elif ifc_rel.is_a("IfcRelContainedInSpatialStructure"):
3919
+ # source = ifc_rel.RelatingStructure
3920
+ # destinations = ifc_rel.RelatedElements
3921
+ # elif ifc_rel.is_a("IfcRelFillsElement"):
3922
+ # source = ifc_rel.RelatingOpeningElement
3923
+ # destinations = [ifc_rel.RelatedBuildingElement]
3924
+ # elif ifc_rel.is_a("IfcRelSpaceBoundary"):
3925
+ # source = ifc_rel.RelatingSpace
3926
+ # destinations = [ifc_rel.RelatedBuildingElement]
3927
+ # elif ifc_rel.is_a("IfcRelVoidsElement"):
3928
+ # source = ifc_rel.RelatingBuildingElement
3929
+ # destinations = [ifc_rel.RelatedOpeningElement]
3930
+ # elif ifc_rel.is_a("IfcRelDefinesByProperties") or ifc_rel.is_a("IfcRelAssociatesMaterial") or ifc_rel.is_a("IfcRelDefinesByType"):
3931
+ # source = None
3932
+ # destinations = None
3933
+ # else:
3934
+ # print("Graph.ByIFCFile - Warning: The relationship", ifc_rel, "is not supported. Skipping.")
3935
+ # if source:
3936
+ # sv = vertexAtKeyValue(vertices, key="IFC_global_id", value=getattr(source, 'GlobalId', 0))
3937
+ # if sv:
3938
+ # si = Vertex.Index(sv, vertices, tolerance=tolerance)
3939
+ # if not si == None:
3940
+ # for destination in destinations:
3941
+ # if destination == None:
3942
+ # continue
3943
+ # ev = vertexAtKeyValue(vertices, key="IFC_global_id", value=getattr(destination, 'GlobalId', 0),)
3944
+ # if ev:
3945
+ # ei = Vertex.Index(ev, vertices, tolerance=tolerance)
3946
+ # if not ei == None:
3947
+ # if not([si,ei] in tuples or [ei,si] in tuples):
3948
+ # tuples.append([si,ei])
3949
+ # e = Edge.ByVertices([sv,ev])
3950
+ # d = Dictionary.ByKeysValues(["IFC_global_id", "IFC_name", "IFC_type"], [ifc_rel.id(), ifc_rel.Name, ifc_rel.is_a()])
3951
+ # e = Topology.SetDictionary(e, d)
3952
+ # edges.append(e)
3953
+ # return edges
3954
+
3955
+ # ifc_types = IFCObjectTypes(file)
3956
+ # ifc_objects = IFCObjects(file, include=includeTypes, exclude=excludeTypes)
3957
+ # vertices = []
3958
+ # for ifc_object in ifc_objects:
3959
+ # v = vertexByIFCObject(ifc_object, ifc_types)
3960
+ # if v:
3961
+ # vertices.append(v)
3962
+ # if len(vertices) > 0:
3963
+ # ifc_relationships = IFCRelationships(file, include=includeRels, exclude=excludeRels)
3964
+ # edges = edgesByIFCRelationships(ifc_relationships, ifc_types, vertices)
3965
+ # g = Graph.ByVerticesEdges(vertices, edges)
3966
+ # else:
3967
+ # g = None
3968
+ # return g
3675
3969
 
3676
3970
  @staticmethod
3677
3971
  def ByIFCPath(path,
@@ -6007,6 +6301,8 @@ class Graph:
6007
6301
  return graph
6008
6302
 
6009
6303
 
6304
+
6305
+
6010
6306
  @staticmethod
6011
6307
  def ClosenessCentrality(
6012
6308
  graph,
@@ -6021,144 +6317,396 @@ class Graph:
6021
6317
  silent: bool = False
6022
6318
  ):
6023
6319
  """
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).
6320
+ Optimized closeness centrality:
6321
+ - Avoids NetworkX and costly per-vertex Topologic calls.
6322
+ - Builds integer-index adjacency once from edges (undirected).
6323
+ - Unweighted: multi-source BFS (one per node).
6324
+ - Weighted: Dijkstra per node (heapq), or SciPy csgraph if available.
6325
+ - Supports 'wf_improved' scaling (nxCompatible) and optional normalization.
6060
6326
  """
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
6327
+ from collections import deque
6328
+ import math
6069
6329
 
6330
+ from topologicpy.Topology import Topology
6070
6331
  from topologicpy.Dictionary import Dictionary
6071
6332
  from topologicpy.Color import Color
6072
- from topologicpy.Topology import Topology
6073
6333
  from topologicpy.Helper import Helper
6334
+ from topologicpy.Vertex import Vertex
6335
+ from topologicpy.Edge import Edge
6336
+ # NOTE: We are inside Graph.*, so Graph.<...> methods are available.
6074
6337
 
6075
- # Topology.IsInstance is case-insensitive, so a single call is sufficient.
6338
+ # Validate graph
6076
6339
  if not Topology.IsInstance(graph, "graph"):
6077
6340
  if not silent:
6078
6341
  print("Graph.ClosenessCentrality - Error: The input is not a valid Graph. Returning None.")
6079
6342
  return None
6343
+
6080
6344
  vertices = Graph.Vertices(graph)
6081
- if len(vertices) == 0:
6345
+ n = len(vertices)
6346
+ if n == 0:
6082
6347
  if not silent:
6083
6348
  print("Graph.ClosenessCentrality - Warning: Graph has no vertices. Returning [].")
6084
6349
  return []
6085
6350
 
6086
- # Normalize the weight key semantics
6351
+ # Stable vertex key (prefer an 'id' in the vertex dictionary; else rounded coords)
6352
+ def vkey(v, r=9):
6353
+ d = Topology.Dictionary(v)
6354
+ vid = Dictionary.ValueAtKey(d, "id")
6355
+ if vid is not None:
6356
+ return ("id", vid)
6357
+ return ("xyz", round(Vertex.X(v), r), round(Vertex.Y(v), r), round(Vertex.Z(v), r))
6358
+
6359
+ idx_of = {vkey(v): i for i, v in enumerate(vertices)}
6360
+
6361
+ # Normalize weight key
6087
6362
  distance_attr = None
6088
6363
  if isinstance(weightKey, str) and weightKey:
6089
- if ("len" in weightKey.lower()) or ("dis" in weightKey.lower()):
6364
+ wl = weightKey.lower()
6365
+ if ("len" in wl) or ("dis" in wl):
6090
6366
  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
6367
+ distance_attr = weightKey # may be "length" or a custom key
6113
6368
 
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
6369
+ # Build undirected adjacency with minimal weights per edge
6370
+ # Use dict-of-dict to collapse multi-edges to minimal weight
6371
+ adj = [dict() for _ in range(n)] # adj[i][j] = weight
6372
+ edges = Graph.Edges(graph)
6121
6373
 
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):
6374
+ def edge_weight(e):
6375
+ if distance_attr == "length":
6376
+ try:
6377
+ return float(Edge.Length(e))
6378
+ except Exception:
6379
+ return 1.0
6380
+ elif distance_attr:
6381
+ try:
6382
+ d = Topology.Dictionary(e)
6383
+ w = Dictionary.ValueAtKey(d, distance_attr)
6384
+ return float(w) if (w is not None) else 1.0
6385
+ except Exception:
6386
+ return 1.0
6387
+ else:
6388
+ return 1.0
6389
+
6390
+ for e in edges:
6125
6391
  try:
6126
- raw_values.append(float(cc_dict.get(i, 0.0)))
6392
+ u = Edge.StartVertex(e)
6393
+ v = Edge.EndVertex(e)
6127
6394
  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)
6395
+ # Fallback in odd cases
6396
+ continue
6397
+ iu = idx_of.get(vkey(u))
6398
+ iv = idx_of.get(vkey(v))
6399
+ if iu is None or iv is None or iu == iv:
6400
+ continue
6401
+ w = edge_weight(e)
6402
+ # Keep minimal weight if duplicates
6403
+ prev = adj[iu].get(iv)
6404
+ if (prev is None) or (w < prev):
6405
+ adj[iu][iv] = w
6406
+ adj[iv][iu] = w
6407
+
6408
+ # Detect weighted vs unweighted
6409
+ weighted = False
6410
+ for i in range(n):
6411
+ if any(abs(w - 1.0) > 1e-12 for w in adj[i].values()):
6412
+ weighted = True
6413
+ break
6131
6414
 
6132
- # Optional normalization ONLY once, then rounding once at the end
6133
- values_for_return = Helper.Normalize(raw_values) if normalize else raw_values
6415
+ INF = float("inf")
6416
+
6417
+ # ---- shortest paths helpers ----
6418
+ def bfs_sum(i):
6419
+ """Sum of unweighted shortest path distances from i; returns (tot, reachable)."""
6420
+ dist = [-1] * n
6421
+ q = deque([i])
6422
+ dist[i] = 0
6423
+ reachable = 1
6424
+ tot = 0
6425
+ pop = q.popleft; push = q.append
6426
+ while q:
6427
+ u = pop()
6428
+ du = dist[u]
6429
+ for v in adj[u].keys():
6430
+ if dist[v] == -1:
6431
+ dist[v] = du + 1
6432
+ reachable += 1
6433
+ tot += dist[v]
6434
+ push(v)
6435
+ return float(tot), reachable
6436
+
6437
+ def dijkstra_sum(i):
6438
+ """Sum of weighted shortest path distances from i; returns (tot, reachable)."""
6439
+ import heapq
6440
+ dist = [INF] * n
6441
+ dist[i] = 0.0
6442
+ hq = [(0.0, i)]
6443
+ push = heapq.heappush; pop = heapq.heappop
6444
+ while hq:
6445
+ du, u = pop(hq)
6446
+ if du > dist[u]:
6447
+ continue
6448
+ for v, w in adj[u].items():
6449
+ nd = du + w
6450
+ if nd < dist[v]:
6451
+ dist[v] = nd
6452
+ push(hq, (nd, v))
6453
+ # Exclude self (0.0) and unreachable (INF)
6454
+ reachable = 0
6455
+ tot = 0.0
6456
+ for d in dist:
6457
+ if d < INF:
6458
+ reachable += 1
6459
+ tot += d
6460
+ # subtract self-distance
6461
+ tot -= 0.0
6462
+ return float(tot), reachable
6463
+
6464
+ # SciPy acceleration if weighted and available
6465
+ use_scipy = False
6466
+ if weighted:
6467
+ try:
6468
+ import numpy as np
6469
+ from scipy.sparse import csr_matrix
6470
+ from scipy.sparse.csgraph import dijkstra as sp_dijkstra
6471
+ use_scipy = True
6472
+ # Build CSR once
6473
+ rows, cols, data = [], [], []
6474
+ for i in range(n):
6475
+ for j, w in adj[i].items():
6476
+ rows.append(i); cols.append(j); data.append(float(w))
6477
+ if len(data) == 0:
6478
+ use_scipy = False # empty graph; fall back
6479
+ else:
6480
+ A = csr_matrix((np.array(data), (np.array(rows), np.array(cols))), shape=(n, n))
6481
+ except Exception:
6482
+ use_scipy = False
6134
6483
 
6135
- # Values for color scaling should reflect the displayed numbers
6136
- color_values = values_for_return
6484
+ # ---- centrality computation ----
6485
+ values = [0.0] * n
6486
+ if n == 1:
6487
+ values[0] = 0.0
6488
+ else:
6489
+ if not weighted:
6490
+ for i in range(n):
6491
+ tot, reachable = bfs_sum(i)
6492
+ s = max(reachable - 1, 0)
6493
+ if tot > 0.0:
6494
+ if nxCompatible:
6495
+ # Wasserman–Faust improved scaling for disconnected graphs
6496
+ values[i] = (s / (n - 1)) * (s / tot)
6497
+ else:
6498
+ values[i] = s / tot
6499
+ else:
6500
+ values[i] = 0.0
6501
+ else:
6502
+ if use_scipy:
6503
+ # All-pairs from SciPy (fast)
6504
+ import numpy as np
6505
+ D = sp_dijkstra(A, directed=False, return_predecessors=False)
6506
+ for i in range(n):
6507
+ di = D[i]
6508
+ finite = di[np.isfinite(di)]
6509
+ # di includes self at 0; reachable count is len(finite)
6510
+ reachable = int(finite.size)
6511
+ s = max(reachable - 1, 0)
6512
+ tot = float(finite.sum()) # includes self=0
6513
+ if s > 0:
6514
+ if nxCompatible:
6515
+ values[i] = (s / (n - 1)) * (s / tot)
6516
+ else:
6517
+ values[i] = s / tot
6518
+ else:
6519
+ values[i] = 0.0
6520
+ else:
6521
+ # Per-source Dijkstra
6522
+ for i in range(n):
6523
+ tot, reachable = dijkstra_sum(i)
6524
+ s = max(reachable - 1, 0)
6525
+ if tot > 0.0:
6526
+ if nxCompatible:
6527
+ values[i] = (s / (n - 1)) * (s / tot)
6528
+ else:
6529
+ values[i] = s / tot
6530
+ else:
6531
+ values[i] = 0.0
6137
6532
 
6138
- # Single rounding at the end for return values
6533
+ # Optional normalization, round once
6534
+ out_vals = Helper.Normalize(values) if normalize else values
6139
6535
  if mantissa is not None and mantissa >= 0:
6140
- values_for_return = [round(v, mantissa) for v in values_for_return]
6536
+ out_vals = [round(v, mantissa) for v in out_vals]
6141
6537
 
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)
6538
+ # Color mapping range (use displayed numbers)
6539
+ if out_vals:
6540
+ min_v, max_v = min(out_vals), max(out_vals)
6146
6541
  else:
6147
- min_value, max_value = 0.0, 1.0
6542
+ min_v, max_v = 0.0, 1.0
6543
+ if abs(max_v - min_v) < tolerance:
6544
+ max_v = min_v + tolerance
6148
6545
 
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):
6546
+ # Annotate vertices
6547
+ for i, value in enumerate(out_vals):
6154
6548
  d = Topology.Dictionary(vertices[i])
6155
6549
  color_hex = Color.AnyToHex(
6156
- Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale)
6550
+ Color.ByValueInRange(value, minValue=min_v, maxValue=max_v, colorScale=colorScale)
6157
6551
  )
6158
- d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [values_for_return[i], color_hex])
6552
+ d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color_hex])
6159
6553
  vertices[i] = Topology.SetDictionary(vertices[i], d)
6160
6554
 
6161
- return values_for_return
6555
+ return out_vals
6556
+
6557
+
6558
+ # @staticmethod
6559
+ # def ClosenessCentrality_old(
6560
+ # graph,
6561
+ # weightKey: str = "length",
6562
+ # normalize: bool = False,
6563
+ # nxCompatible: bool = True,
6564
+ # key: str = "closeness_centrality",
6565
+ # colorKey: str = "cc_color",
6566
+ # colorScale: str = "viridis",
6567
+ # mantissa: int = 6,
6568
+ # tolerance: float = 0.0001,
6569
+ # silent: bool = False
6570
+ # ):
6571
+ # """
6572
+ # Returns the closeness centrality of the input graph. The order of the returned
6573
+ # list matches the order of Graph.Vertices(graph).
6574
+ # See: https://en.wikipedia.org/wiki/Closeness_centrality
6575
+
6576
+ # Parameters
6577
+ # ----------
6578
+ # graph : topologic_core.Graph
6579
+ # The input graph.
6580
+ # weightKey : str , optional
6581
+ # If specified, this edge attribute will be used as the distance weight when
6582
+ # computing shortest paths. If set to a name containing "Length" or "Distance",
6583
+ # it will be mapped to "length".
6584
+ # Note: Graph.NetworkXGraph automatically provides a "length" attribute on all edges.
6585
+ # normalize : bool , optional
6586
+ # If True, the returned values are rescaled to [0, 1]. Otherwise raw values
6587
+ # from NetworkX (optionally using the improved formula) are returned.
6588
+ # nxCompatible : bool , optional
6589
+ # If True, use NetworkX's wf_improved scaling (Wasserman and Faust).
6590
+ # For single-component graphs it matches the original formula.
6591
+ # key : str , optional
6592
+ # The dictionary key under which to store the closeness centrality score.
6593
+ # colorKey : str , optional
6594
+ # The dictionary key under which to store a color derived from the score.
6595
+ # colorScale : str , optional
6596
+ # Plotly color scale name (e.g., "viridis", "plasma").
6597
+ # mantissa : int , optional
6598
+ # The number of decimal places to round the result to. Default is 6.
6599
+ # tolerance : float , optional
6600
+ # The desired tolerance. Default is 0.0001.
6601
+ # silent : bool , optional
6602
+ # If set to True, error and warning messages are suppressed. Default is False.
6603
+
6604
+ # Returns
6605
+ # -------
6606
+ # list[float]
6607
+ # Closeness centrality values for vertices in the same order as Graph.Vertices(graph).
6608
+ # """
6609
+ # import warnings
6610
+ # try:
6611
+ # import networkx as nx
6612
+ # except Exception as e:
6613
+ # warnings.warn(
6614
+ # f"Graph.ClosenessCentrality - Error: networkx is required but not installed ({e}). Returning None."
6615
+ # )
6616
+ # return None
6617
+
6618
+ # from topologicpy.Dictionary import Dictionary
6619
+ # from topologicpy.Color import Color
6620
+ # from topologicpy.Topology import Topology
6621
+ # from topologicpy.Helper import Helper
6622
+
6623
+ # # Topology.IsInstance is case-insensitive, so a single call is sufficient.
6624
+ # if not Topology.IsInstance(graph, "graph"):
6625
+ # if not silent:
6626
+ # print("Graph.ClosenessCentrality - Error: The input is not a valid Graph. Returning None.")
6627
+ # return None
6628
+ # vertices = Graph.Vertices(graph)
6629
+ # if len(vertices) == 0:
6630
+ # if not silent:
6631
+ # print("Graph.ClosenessCentrality - Warning: Graph has no vertices. Returning [].")
6632
+ # return []
6633
+
6634
+ # # Normalize the weight key semantics
6635
+ # distance_attr = None
6636
+ # if isinstance(weightKey, str) and weightKey:
6637
+ # if ("len" in weightKey.lower()) or ("dis" in weightKey.lower()):
6638
+ # weightKey = "length"
6639
+ # distance_attr = weightKey
6640
+
6641
+ # # Build the NX graph
6642
+ # nx_graph = Graph.NetworkXGraph(graph)
6643
+
6644
+ # # Graph.NetworkXGraph automatically adds "length" to all edges.
6645
+ # # So if distance_attr == "length", we trust it and skip per-edge checks.
6646
+ # if distance_attr and distance_attr != "length":
6647
+ # # For any non-"length" custom attribute, verify presence; else fall back unweighted.
6648
+ # attr_missing = any(
6649
+ # (distance_attr not in data) or (data[distance_attr] is None)
6650
+ # for _, _, data in nx_graph.edges(data=True)
6651
+ # )
6652
+ # if attr_missing:
6653
+ # if not silent:
6654
+ # print("Graph.ClosenessCentrality - Warning: The specified edge attribute was not found on all edges. Falling back to unweighted closeness.")
6655
+ # distance_arg = None
6656
+ # else:
6657
+ # distance_arg = distance_attr
6658
+ # else:
6659
+ # # Use "length" directly or unweighted if distance_attr is falsy.
6660
+ # distance_arg = distance_attr if distance_attr else None
6661
+
6662
+ # # Compute centrality (dict keyed by NetworkX nodes)
6663
+ # try:
6664
+ # cc_dict = nx.closeness_centrality(nx_graph, distance=distance_arg, wf_improved=nxCompatible)
6665
+ # except Exception as e:
6666
+ # if not silent:
6667
+ # print(f"Graph.ClosenessCentrality - Error: NetworkX failed to compute centrality ({e}). Returning None.")
6668
+ # return None
6669
+
6670
+ # # NetworkX vertex ids are in the same numerice order as the list of vertices starting from 0.
6671
+ # raw_values = []
6672
+ # for i, v in enumerate(vertices):
6673
+ # try:
6674
+ # raw_values.append(float(cc_dict.get(i, 0.0)))
6675
+ # except Exception:
6676
+ # if not silent:
6677
+ # print(f,"Graph.ClosenessCentrality - Warning: Could not retrieve score for vertex {i}. Assigning a Zero (0).")
6678
+ # raw_values.append(0.0)
6679
+
6680
+ # # Optional normalization ONLY once, then rounding once at the end
6681
+ # values_for_return = Helper.Normalize(raw_values) if normalize else raw_values
6682
+
6683
+ # # Values for color scaling should reflect the displayed numbers
6684
+ # color_values = values_for_return
6685
+
6686
+ # # Single rounding at the end for return values
6687
+ # if mantissa is not None and mantissa >= 0:
6688
+ # values_for_return = [round(v, mantissa) for v in values_for_return]
6689
+
6690
+ # # Prepare color mapping range, guarding equal-range case
6691
+ # if color_values:
6692
+ # min_value = min(color_values)
6693
+ # max_value = max(color_values)
6694
+ # else:
6695
+ # min_value, max_value = 0.0, 1.0
6696
+
6697
+ # if abs(max_value - min_value) < tolerance:
6698
+ # max_value = min_value + tolerance
6699
+
6700
+ # # Annotate vertices with score and color
6701
+ # for i, value in enumerate(color_values):
6702
+ # d = Topology.Dictionary(vertices[i])
6703
+ # color_hex = Color.AnyToHex(
6704
+ # Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale)
6705
+ # )
6706
+ # d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [values_for_return[i], color_hex])
6707
+ # vertices[i] = Topology.SetDictionary(vertices[i], d)
6708
+
6709
+ # return values_for_return
6162
6710
 
6163
6711
  @staticmethod
6164
6712
  def Community(graph, key: str = "partition", mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
@@ -12962,90 +13510,201 @@ class Graph:
12962
13510
  return outgoing_vertices
12963
13511
 
12964
13512
  @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):
13513
+ def PageRank(
13514
+ graph,
13515
+ alpha: float = 0.85,
13516
+ maxIterations: int = 100,
13517
+ normalize: bool = True,
13518
+ directed: bool = False,
13519
+ key: str = "page_rank",
13520
+ colorKey: str = "pr_color",
13521
+ colorScale: str = "viridis",
13522
+ mantissa: int = 6,
13523
+ tolerance: float = 1e-4
13524
+ ):
12966
13525
  """
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.
13526
+ PageRank with stable vertex mapping (by coordinates) so neighbors resolve correctly.
13527
+ Handles dangling nodes; uses cached neighbor lists and L1 convergence.
12997
13528
  """
12998
13529
  from topologicpy.Vertex import Vertex
12999
13530
  from topologicpy.Helper import Helper
13000
13531
  from topologicpy.Dictionary import Dictionary
13001
13532
  from topologicpy.Topology import Topology
13002
13533
  from topologicpy.Color import Color
13534
+ from topologicpy.Graph import Graph
13003
13535
 
13004
13536
  vertices = Graph.Vertices(graph)
13005
- num_vertices = len(vertices)
13006
- if num_vertices < 1:
13537
+ n = len(vertices)
13538
+ if n < 1:
13007
13539
  print("Graph.PageRank - Error: The input graph parameter has no vertices. Returning None")
13008
13540
  return None
13009
- initial_score = 1.0 / num_vertices
13010
- values = [initial_score for vertex in vertices]
13541
+
13542
+ # ---- stable vertex key (coord-based) ----
13543
+ # Use a modest rounding to be robust to tiny numerical noise.
13544
+ # If your graphs can have distinct vertices at the exact same coords,
13545
+ # switch to a stronger key (e.g., include a unique ID from the vertex dictionary).
13546
+ def vkey(v, r=9):
13547
+ return (round(Vertex.X(v), r), round(Vertex.Y(v), r), round(Vertex.Z(v), r))
13548
+
13549
+ idx_of = {vkey(v): i for i, v in enumerate(vertices)}
13550
+
13551
+ # Helper that resolves an arbitrary Topologic vertex to our index
13552
+ def to_idx(u):
13553
+ return idx_of.get(vkey(u), None)
13554
+
13555
+ # ---- build neighbor lists ONCE (by indices) ----
13556
+ if directed:
13557
+ in_neighbors = [[] for _ in range(n)]
13558
+ out_neighbors = [[] for _ in range(n)]
13559
+
13560
+ for i, v in enumerate(vertices):
13561
+ inv = Graph.IncomingVertices(graph, v, directed=True)
13562
+ onv = Graph.OutgoingVertices(graph, v, directed=True)
13563
+ # map to indices, drop misses
13564
+ in_neighbors[i] = [j for u in inv if (j := to_idx(u)) is not None]
13565
+ out_neighbors[i] = [j for u in onv if (j := to_idx(u)) is not None]
13566
+ else:
13567
+ in_neighbors = [[] for _ in range(n)]
13568
+ out_neighbors = in_neighbors # same list objects is fine; we set both below
13569
+ for i, v in enumerate(vertices):
13570
+ nbrs = Graph.AdjacentVertices(graph, v)
13571
+ idxs = [j for u in nbrs if (j := to_idx(u)) is not None]
13572
+ in_neighbors[i] = idxs
13573
+ out_neighbors = in_neighbors # undirected: in == out
13574
+
13575
+ out_degree = [len(out_neighbors[i]) for i in range(n)]
13576
+ dangling = [i for i in range(n) if out_degree[i] == 0]
13577
+
13578
+ # ---- power iteration ----
13579
+ pr = [1.0 / n] * n
13580
+ base = (1.0 - alpha) / n
13581
+
13011
13582
  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))):
13583
+ # Distribute dangling mass uniformly
13584
+ dangling_mass = alpha * (sum(pr[i] for i in dangling) / n) if dangling else 0.0
13585
+
13586
+ new_pr = [base + dangling_mass] * n
13587
+
13588
+ # Sum contributions from incoming neighbors j: alpha * pr[j] / out_degree[j]
13589
+ for i in range(n):
13590
+ acc = 0.0
13591
+ for j in in_neighbors[i]:
13592
+ deg = out_degree[j]
13593
+ if deg > 0:
13594
+ acc += pr[j] / deg
13595
+ new_pr[i] += alpha * acc
13596
+
13597
+ # L1 convergence
13598
+ if sum(abs(new_pr[i] - pr[i]) for i in range(n)) <= tolerance:
13599
+ pr = new_pr
13024
13600
  break
13601
+ pr = new_pr
13025
13602
 
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
13603
+ # ---- normalize & write dictionaries ----
13604
+ if normalize:
13605
+ pr = Helper.Normalize(pr)
13606
+ if mantissa > 0:
13607
+ pr = [round(v, mantissa) for v in pr]
13608
+ min_v, max_v = 0.0, 1.0
13034
13609
  else:
13035
- min_value = min(values)
13036
- max_value = max(values)
13610
+ min_v, max_v = (min(pr), max(pr)) if n > 0 else (0.0, 0.0)
13037
13611
 
13038
- for i, value in enumerate(values):
13612
+ for i, value in enumerate(pr):
13039
13613
  d = Topology.Dictionary(vertices[i])
13040
- color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
13614
+ color = Color.AnyToHex(
13615
+ Color.ByValueInRange(value, minValue=min_v, maxValue=max_v, colorScale=colorScale)
13616
+ )
13041
13617
  d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
13042
13618
  vertices[i] = Topology.SetDictionary(vertices[i], d)
13043
-
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
13619
+
13620
+ return pr
13621
+
13622
+
13623
+ # @staticmethod
13624
+ # 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):
13625
+ # """
13626
+ # Calculates PageRank scores for vertices in a directed graph. see https://en.wikipedia.org/wiki/PageRank.
13627
+
13628
+ # Parameters
13629
+ # ----------
13630
+ # graph : topologic_core.Graph
13631
+ # The input graph.
13632
+ # alpha : float , optional
13633
+ # The damping (dampening) factor. Default is 0.85. See https://en.wikipedia.org/wiki/PageRank.
13634
+ # maxIterations : int , optional
13635
+ # The maximum number of iterations to calculate the page rank. Default is 100.
13636
+ # normalize : bool , optional
13637
+ # If set to True, the results will be normalized from 0 to 1. Otherwise, they won't be. Default is True.
13638
+ # directed : bool , optional
13639
+ # If set to True, the graph is considered as a directed graph. Otherwise, it will be considered as an undirected graph. Default is False.
13640
+ # key : str , optional
13641
+ # The dictionary key under which to store the page_rank score. Default is "page_rank"
13642
+ # colorKey : str , optional
13643
+ # The desired dictionary key under which to store the pagerank color. Default is "pr_color".
13644
+ # colorScale : str , optional
13645
+ # 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/.
13646
+ # In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
13647
+ # mantissa : int , optional
13648
+ # The desired length of the mantissa.
13649
+ # tolerance : float , optional
13650
+ # The desired tolerance. Default is 0.0001.
13651
+
13652
+ # Returns
13653
+ # -------
13654
+ # list
13655
+ # The list of page ranks for the vertices in the graph.
13656
+ # """
13657
+ # from topologicpy.Vertex import Vertex
13658
+ # from topologicpy.Helper import Helper
13659
+ # from topologicpy.Dictionary import Dictionary
13660
+ # from topologicpy.Topology import Topology
13661
+ # from topologicpy.Color import Color
13662
+
13663
+ # vertices = Graph.Vertices(graph)
13664
+ # num_vertices = len(vertices)
13665
+ # if num_vertices < 1:
13666
+ # print("Graph.PageRank - Error: The input graph parameter has no vertices. Returning None")
13667
+ # return None
13668
+ # initial_score = 1.0 / num_vertices
13669
+ # values = [initial_score for vertex in vertices]
13670
+ # for _ in range(maxIterations):
13671
+ # new_scores = [0 for vertex in vertices]
13672
+ # for i, vertex in enumerate(vertices):
13673
+ # incoming_score = 0
13674
+ # for incoming_vertex in Graph.IncomingVertices(graph, vertex, directed=directed):
13675
+ # if len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed)) > 0:
13676
+ # vi = Vertex.Index(incoming_vertex, vertices, tolerance=tolerance)
13677
+ # if not vi == None:
13678
+ # incoming_score += values[vi] / len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed))
13679
+ # new_scores[i] = alpha * incoming_score + (1 - alpha) / num_vertices
13680
+
13681
+ # # Check for convergence
13682
+ # if all(abs(new_scores[i] - values[i]) <= tolerance for i in range(len(vertices))):
13683
+ # break
13684
+
13685
+ # values = new_scores
13686
+ # if normalize == True:
13687
+ # if mantissa > 0: # We cannot round numbers from 0 to 1 with a mantissa = 0.
13688
+ # values = [round(v, mantissa) for v in Helper.Normalize(values)]
13689
+ # else:
13690
+ # values = Helper.Normalize(values)
13691
+ # min_value = 0
13692
+ # max_value = 1
13693
+ # else:
13694
+ # min_value = min(values)
13695
+ # max_value = max(values)
13696
+
13697
+ # for i, value in enumerate(values):
13698
+ # d = Topology.Dictionary(vertices[i])
13699
+ # color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
13700
+ # d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
13701
+ # vertices[i] = Topology.SetDictionary(vertices[i], d)
13702
+
13703
+ # for i, v in enumerate(vertices):
13704
+ # d = Topology.Dictionary(v)
13705
+ # d = Dictionary.SetValueAtKey(d, key, values[i])
13706
+ # v = Topology.SetDictionary(v, d)
13707
+ # return values
13049
13708
 
13050
13709
  @staticmethod
13051
13710
  def Partition(graph, method: str = "Betweenness", n: int = 2, m: int = 10, key: str ="partition",