topologicpy 0.8.57__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.
1962
1967
 
1963
- if weightKey:
1964
- if "len" in weightKey.lower() or "dis" in weightKey.lower():
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
1983
+
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
6367
+ distance_attr = weightKey # may be "length" or a custom key
6368
+
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)
6373
+
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
6108
6387
  else:
6109
- distance_arg = distance_attr
6388
+ return 1.0
6389
+
6390
+ for e in edges:
6391
+ try:
6392
+ u = Edge.StartVertex(e)
6393
+ v = Edge.EndVertex(e)
6394
+ except Exception:
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
6414
+
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
6483
+
6484
+ # ---- centrality computation ----
6485
+ values = [0.0] * n
6486
+ if n == 1:
6487
+ values[0] = 0.0
6110
6488
  else:
6111
- # Use "length" directly or unweighted if distance_attr is falsy.
6112
- distance_arg = distance_attr if distance_attr else None
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
6532
+
6533
+ # Optional normalization, round once
6534
+ out_vals = Helper.Normalize(values) if normalize else values
6535
+ if mantissa is not None and mantissa >= 0:
6536
+ out_vals = [round(v, mantissa) for v in out_vals]
6537
+
6538
+ # Color mapping range (use displayed numbers)
6539
+ if out_vals:
6540
+ min_v, max_v = min(out_vals), max(out_vals)
6541
+ else:
6542
+ min_v, max_v = 0.0, 1.0
6543
+ if abs(max_v - min_v) < tolerance:
6544
+ max_v = min_v + tolerance
6545
+
6546
+ # Annotate vertices
6547
+ for i, value in enumerate(out_vals):
6548
+ d = Topology.Dictionary(vertices[i])
6549
+ color_hex = Color.AnyToHex(
6550
+ Color.ByValueInRange(value, minValue=min_v, maxValue=max_v, colorScale=colorScale)
6551
+ )
6552
+ d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color_hex])
6553
+ vertices[i] = Topology.SetDictionary(vertices[i], d)
6554
+
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 []
6113
6633
 
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
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
6121
6669
 
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):
6125
- try:
6126
- raw_values.append(float(cc_dict.get(i, 0.0)))
6127
- 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)
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)
6131
6679
 
6132
- # Optional normalization ONLY once, then rounding once at the end
6133
- values_for_return = Helper.Normalize(raw_values) if normalize else raw_values
6680
+ # # Optional normalization ONLY once, then rounding once at the end
6681
+ # values_for_return = Helper.Normalize(raw_values) if normalize else raw_values
6134
6682
 
6135
- # Values for color scaling should reflect the displayed numbers
6136
- color_values = values_for_return
6683
+ # # Values for color scaling should reflect the displayed numbers
6684
+ # color_values = values_for_return
6137
6685
 
6138
- # Single rounding at the end for return values
6139
- if mantissa is not None and mantissa >= 0:
6140
- values_for_return = [round(v, mantissa) for v in values_for_return]
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]
6141
6689
 
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)
6146
- else:
6147
- min_value, max_value = 0.0, 1.0
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
6148
6696
 
6149
- if abs(max_value - min_value) < tolerance:
6150
- max_value = min_value + tolerance
6697
+ # if abs(max_value - min_value) < tolerance:
6698
+ # max_value = min_value + tolerance
6151
6699
 
6152
- # Annotate vertices with score and color
6153
- for i, value in enumerate(color_values):
6154
- d = Topology.Dictionary(vertices[i])
6155
- color_hex = Color.AnyToHex(
6156
- Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale)
6157
- )
6158
- d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [values_for_return[i], color_hex])
6159
- vertices[i] = Topology.SetDictionary(vertices[i], d)
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)
6160
6708
 
6161
- return values_for_return
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):
@@ -6929,44 +7477,146 @@ class Graph:
6929
7477
  return graph.Edge(vertexA, vertexB, tolerance) # Hook to Core
6930
7478
 
6931
7479
  @staticmethod
6932
- def Edges(graph, vertices=None, tolerance=0.0001):
7480
+ def Edges(
7481
+ graph,
7482
+ vertices: list = None,
7483
+ strict: bool = False,
7484
+ sortBy: str = None,
7485
+ reverse: bool = False,
7486
+ silent: bool = False,
7487
+ tolerance: float = 0.0001
7488
+ ) -> list: # list[topologic_core.Edge]
6933
7489
  """
6934
- Returns the edges found in the input graph. If the input list of vertices is specified, this method returns the edges connected to this list of vertices. Otherwise, it returns all graph edges.
7490
+ Returns the list of edges from `graph` whose endpoints match the given `vertices`
7491
+ according to the `strict` rule.
7492
+
7493
+ If `strict` is True, both endpoints of an edge must be in `vertices`.
7494
+ If `strict` is False, at least one endpoint must be in `vertices`.
6935
7495
 
6936
7496
  Parameters
6937
7497
  ----------
6938
- graph : topologic_core.Graph
7498
+ graph : topologicpy.Graph
6939
7499
  The input graph.
6940
- vertices : list , optional
6941
- An optional list of vertices to restrict the returned list of edges only to those connected to this list.
7500
+ vertices : list[topologicpy.Vertex]
7501
+ The list of vertices to test membership against.
7502
+ strict : bool, optional
7503
+ If set to True, require both endpoints to be in `vertices`. Otherwise,
7504
+ require at least one endpoint to be in `vertices`. Default is False.
7505
+ sortBy : str , optional
7506
+ The dictionary key to use for sorting the returned edges. Special strings include "length" and "distance" to sort by the length of the edge. Default is None.
7507
+ reverse : bool , optional
7508
+ If set to True, the sorted list is reversed. This has no effect if the sortBy parameter is not set. Default is False.
7509
+ silent : bool, optional
7510
+ Isilent : bool, optional
7511
+ If set to True, all errors and warnings are suppressed. Default is False.
6942
7512
  tolerance : float , optional
6943
7513
  The desired tolerance. Default is 0.0001.
6944
7514
 
6945
7515
  Returns
6946
7516
  -------
6947
- list
6948
- The list of edges in the graph.
7517
+ list[topologic_core.Edge]
7518
+ The list of matching edges from the original graph (not recreated).
6949
7519
 
6950
7520
  """
7521
+ from topologicpy.Vertex import Vertex
6951
7522
  from topologicpy.Topology import Topology
7523
+ from topologicpy.Edge import Edge
7524
+ from topologicpy.Helper import Helper
7525
+ from topologicpy.Dictionary import Dictionary
7526
+
7527
+ def sort_edges(edges, sortBy, reverse):
7528
+ if not sortBy is None:
7529
+ if "length" in sortBy.lower() or "dist" in sortBy.lower():
7530
+ edge_values = [Edge.Length(e) for e in edges]
7531
+ else:
7532
+ edge_values = [Dictionary.ValueAtKey(Topology.Dictionary(e), sortBy, "0") for e in edges]
7533
+ edges = Helper.Sort(edges, edge_values)
7534
+ if reverse:
7535
+ edges.reverse()
7536
+ return edges
6952
7537
 
6953
7538
  if not Topology.IsInstance(graph, "Graph"):
6954
- print("Graph.Edges - Error: The input graph is not a valid graph. Returning None.")
6955
- return None
7539
+ if not silent:
7540
+ print("Graph.InducedEdges - Error: The input 'graph' is not a valid Graph. Returning [].")
7541
+ return []
7542
+
7543
+ graph_edges = []
7544
+ _ = graph.Edges(graph_edges, tolerance) # Hook to Core
7545
+ graph_edges = list(dict.fromkeys(graph_edges)) # remove duplicates
7546
+
7547
+ if not graph_edges:
7548
+ return []
6956
7549
  if not vertices:
6957
- edges = []
6958
- _ = graph.Edges(edges, tolerance) # Hook to Core
6959
- if not edges:
6960
- return []
6961
- return list(dict.fromkeys(edges)) # remove duplicates
6962
- else:
6963
- vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
6964
- if len(vertices) < 1:
6965
- print("Graph.Edges - Error: The input list of vertices does not contain any valid vertices. Returning None.")
6966
- return None
6967
- edges = []
6968
- _ = graph.Edges(vertices, tolerance, edges) # Hook to Core
6969
- return list(dict.fromkeys(edges)) # remove duplicates
7550
+ graph_edges = sort_edges(graph_edges, sortBy, reverse)
7551
+ return graph_edges
7552
+
7553
+ if not isinstance(vertices, list):
7554
+ if not silent:
7555
+ print("Graph.Edges - Error: The input 'vertices' is not a list. Returning [].")
7556
+ return []
7557
+
7558
+ valid_vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
7559
+ if not valid_vertices:
7560
+ if not silent:
7561
+ print("Graph.Edges - Warning: No valid vertices provided. Returning [].")
7562
+ return []
7563
+
7564
+ return_edges = []
7565
+ for e in graph_edges:
7566
+ sv = Edge.StartVertex(e)
7567
+ ev = Edge.EndVertex(e)
7568
+
7569
+ in_start = not Vertex.Index(sv, valid_vertices) is None
7570
+ in_end = not Vertex.Index(ev, valid_vertices) is None
7571
+ if strict:
7572
+ if in_start and in_end:
7573
+ return_edges.append(e)
7574
+ else:
7575
+ if in_start or in_end:
7576
+ return_edges.append(e)
7577
+
7578
+ return_edges = sort_edges(return_edges, sortBy, reverse)
7579
+ return return_edges
7580
+
7581
+ # @staticmethod
7582
+ # def Edges(graph, vertices=None, tolerance=0.0001):
7583
+ # """
7584
+ # Returns the edges found in the input graph. If the input list of vertices is specified, this method returns the edges connected to this list of vertices. Otherwise, it returns all graph edges.
7585
+
7586
+ # Parameters
7587
+ # ----------
7588
+ # graph : topologic_core.Graph
7589
+ # The input graph.
7590
+ # vertices : list , optional
7591
+ # An optional list of vertices to restrict the returned list of edges only to those connected to this list.
7592
+ # tolerance : float , optional
7593
+ # The desired tolerance. Default is 0.0001.
7594
+
7595
+ # Returns
7596
+ # -------
7597
+ # list
7598
+ # The list of edges in the graph.
7599
+
7600
+ # """
7601
+ # from topologicpy.Topology import Topology
7602
+
7603
+ # if not Topology.IsInstance(graph, "Graph"):
7604
+ # print("Graph.Edges - Error: The input graph is not a valid graph. Returning None.")
7605
+ # return None
7606
+ # if not vertices:
7607
+ # edges = []
7608
+ # _ = graph.Edges(edges, tolerance) # Hook to Core
7609
+ # if not edges:
7610
+ # return []
7611
+ # return list(dict.fromkeys(edges)) # remove duplicates
7612
+ # else:
7613
+ # vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
7614
+ # if len(vertices) < 1:
7615
+ # print("Graph.Edges - Error: The input list of vertices does not contain any valid vertices. Returning None.")
7616
+ # return None
7617
+ # edges = []
7618
+ # _ = graph.Edges(vertices, tolerance, edges) # Hook to Core
7619
+ # return list(dict.fromkeys(edges)) # remove duplicates
6970
7620
 
6971
7621
  @staticmethod
6972
7622
  def EigenVectorCentrality(graph, normalize: bool = False, key: str = "eigen_vector_centrality", colorKey: str = "evc_color", colorScale: str = "viridis", mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
@@ -8412,6 +9062,57 @@ class Graph:
8412
9062
  edge = Topology.SetDictionary(edge, d)
8413
9063
  return graph
8414
9064
 
9065
+
9066
+ @staticmethod
9067
+ def InducedSubgraph(graph, vertices: list = None, strict: bool = False, silent: bool = False, tolerance: float = 0.0001):
9068
+ """
9069
+ Returns the subgraph whose edges are connected to the given `vertices`
9070
+ according to the `strict` rule. Isolated vertices are included as-is.
9071
+
9072
+ If `strict` is True, both endpoints of an edge must be in `vertices`.
9073
+ If `strict` is False, at least one endpoint must be in `vertices`.
9074
+
9075
+ Parameters
9076
+ ----------
9077
+ graph : topologicpy.Graph
9078
+ The input graph.
9079
+ vertices : list[topologicpy.Vertex]
9080
+ The list of vertices to test membership against.
9081
+ strict : bool, optional
9082
+ If set to True, require both endpoints to be in `vertices`. Otherwise,
9083
+ require at least one endpoint to be in `vertices`. Default is False.
9084
+ silent : bool, optional
9085
+ Isilent : bool, optional
9086
+ If set to True, all errors and warnings are suppressed. Default is False
9087
+ tolerance : float , optional
9088
+ The desired tolerance. Default is 0.0001.
9089
+
9090
+ Returns
9091
+ -------
9092
+ list[topologic_core.Edge]
9093
+ The list of matching edges from the original graph (not recreated).
9094
+
9095
+ """
9096
+ from topologicpy.Topology import Topology
9097
+
9098
+ if not Topology.IsInstance(graph, "Graph"):
9099
+ if not silent:
9100
+ print("Graph.InducedSubgraph - Error: The input graph parameter is not a valid graph. Returning None.")
9101
+
9102
+ if not isinstance(vertices, list):
9103
+ if not silent:
9104
+ print("Graph.InducedSubgraph - Error: The input 'vertices' is not a list. Returning None.")
9105
+ return None
9106
+
9107
+ valid_vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
9108
+ if not valid_vertices:
9109
+ if not silent:
9110
+ print("Graph.InducedSubgraph - Warning: No valid vertices provided. Returning None.")
9111
+ return None
9112
+ connected_vertices = [v for v in valid_vertices if Graph.VertexDegree(graph, v) > 0]
9113
+ edges = Graph.Edges(graph, connected_vertices, strict=strict, tolerance=tolerance)
9114
+ return Graph.ByVerticesEdges(valid_vertices, edges)
9115
+
8415
9116
  @staticmethod
8416
9117
  def IsEmpty(graph, silent: bool = False):
8417
9118
  """
@@ -11215,7 +11916,166 @@ class Graph:
11215
11916
  print(f'Graph.Kernel - Error: Unsupported method "{method}". '
11216
11917
  f'Supported methods are "WL" and "Hopper". Returning None.')
11217
11918
  return None
11218
-
11919
+
11920
+ @staticmethod
11921
+ def KHopsSubgraph(
11922
+ graph,
11923
+ vertices: list,
11924
+ k: int = 1,
11925
+ direction: str = "both",
11926
+ silent: bool = False,
11927
+ ):
11928
+ """
11929
+ Returns a subgraph consisting of the k-hop neighborhood around the input list of seed vertices.
11930
+
11931
+ Parameters
11932
+ ----------
11933
+ graph : topologicpy.Graph
11934
+ The input graph.
11935
+ vertices : list
11936
+ The input list of seed vertices.
11937
+ k : int, optional
11938
+ Number of hops. Default is 1.
11939
+ direction : str, optional
11940
+ 'both', 'out', or 'in'. Default 'both'.
11941
+ silent : bool, optional
11942
+ Suppress warnings/errors. Default False.
11943
+
11944
+ Returns
11945
+ -------
11946
+ topologicpy.Graph or None
11947
+ The resulting subgraph, or None on error.
11948
+ """
11949
+ from topologicpy.Vertex import Vertex
11950
+ from topologicpy.Edge import Edge
11951
+ from topologicpy.Graph import Graph
11952
+ from topologicpy.Topology import Topology
11953
+ from topologicpy.Dictionary import Dictionary
11954
+
11955
+ # ---- validate inputs ----
11956
+ if not Topology.IsInstance(graph, "graph"):
11957
+ if not silent:
11958
+ print("Graph.KHopsSubgraph - Error: The input graph parameter is not a valid graph. Returning None.")
11959
+ return None
11960
+
11961
+ if not isinstance(vertices, list):
11962
+ if not silent:
11963
+ print("Graph.KHopsSubgraph - Error: The input vertices parameter is not a valid list. Returning None.")
11964
+ return None
11965
+
11966
+ graph_vertices = Graph.Vertices(graph)
11967
+ if not graph_vertices:
11968
+ if not silent:
11969
+ print("Graph.KHopsSubgraph - Error: The input graph does not contain any vertices. Returning None.")
11970
+ return None
11971
+
11972
+ # Keep only valid vertex objects
11973
+ seed_vertices = [v for v in vertices if Topology.IsInstance(v, "vertex")]
11974
+ if not seed_vertices:
11975
+ if not silent:
11976
+ print("Graph.KHopsSubgraph - Error: The input vertices list does not contain any valid vertices. Returning None.")
11977
+ return None
11978
+
11979
+ # ---- map seeds to vertex indices (prefer identity; fallback to list.index) ----
11980
+ id_to_index = {Topology.UUID(v): i for i, v in enumerate(graph_vertices)}
11981
+ seed_indices = []
11982
+ for sv in seed_vertices:
11983
+ idx = id_to_index.get(Topology.UUID(sv))
11984
+ if idx is None:
11985
+ try:
11986
+ idx = graph_vertices.index(sv) # fallback if same object not used
11987
+ except ValueError:
11988
+ idx = None
11989
+ if idx is not None:
11990
+ seed_indices.append(idx)
11991
+
11992
+ if not seed_indices:
11993
+ if not silent:
11994
+ print("Graph.KHopsSubgraph - Error: None of the seed vertices are found in the graph. Returning None.")
11995
+ return None
11996
+
11997
+ # ---- get mesh data (index-based edge list) ----
11998
+ # Expect: mesh_data["vertices"] (list), mesh_data["edges"] (list of [a, b] indices)
11999
+ mesh_data = Graph.MeshData(graph)
12000
+ edges_idx = mesh_data.get("edges") or []
12001
+ # Compute number of vertices robustly
12002
+ n_verts = len(mesh_data.get("vertices") or graph_vertices)
12003
+
12004
+ # ---- build adjacency (directed; BFS respects 'direction') ----
12005
+ adj_out = {i: set() for i in range(n_verts)}
12006
+ adj_in = {i: set() for i in range(n_verts)}
12007
+ for (a, b) in edges_idx:
12008
+ if 0 <= a < n_verts and 0 <= b < n_verts:
12009
+ adj_out[a].add(b)
12010
+ adj_in[b].add(a)
12011
+
12012
+ # ---- BFS up to k hops ----
12013
+ dir_norm = (direction or "both").lower()
12014
+ if dir_norm not in ("both", "out", "in"):
12015
+ dir_norm = "both"
12016
+
12017
+ visited = set(seed_indices)
12018
+ frontier = set(seed_indices)
12019
+ for _ in range(max(0, int(k))):
12020
+ nxt = set()
12021
+ for v in frontier:
12022
+ if dir_norm in ("both", "out"):
12023
+ nxt |= adj_out.get(v, set())
12024
+ if dir_norm in ("both", "in"):
12025
+ nxt |= adj_in.get(v, set())
12026
+ nxt -= visited
12027
+ if not nxt:
12028
+ break
12029
+ visited |= nxt
12030
+ frontier = nxt
12031
+
12032
+ if not visited:
12033
+ if not silent:
12034
+ print("Graph.KHopsSubgraph - Warning: No vertices found within the specified k hops. Returning None.")
12035
+ return None
12036
+
12037
+ # ---- assemble subgraph ----
12038
+ # Vertices: actual TopologicPy Vertex objects
12039
+ sub_vertex_indices = sorted(visited)
12040
+ sub_vertices = [graph_vertices[i] for i in sub_vertex_indices]
12041
+
12042
+ # Edges: include only those whose endpoints are both in the subgraph
12043
+ sub_index_set = set(sub_vertex_indices)
12044
+ # Map from global index -> actual Vertex object for edge reconstruction
12045
+ idx_to_vertex = {i: graph_vertices[i] for i in sub_vertex_indices}
12046
+
12047
+ sub_edges = []
12048
+ for (a, b) in edges_idx:
12049
+ if a in sub_index_set and b in sub_index_set:
12050
+ # Recreate edge to ensure it references the subgraph vertices
12051
+ ea = idx_to_vertex[a]
12052
+ eb = idx_to_vertex[b]
12053
+ try:
12054
+ e = Edge.ByStartVertexEndVertex(ea, eb)
12055
+ except Exception:
12056
+ # If creation fails, skip this edge
12057
+ continue
12058
+ # Preserve edge label if present
12059
+ try:
12060
+ # Find original edge and copy its dictionary if possible
12061
+ # (best-effort; safe if Graph.Edges aligns with edges_idx order)
12062
+ # Otherwise, leave edge as-is.
12063
+ pass
12064
+ except Exception:
12065
+ pass
12066
+ sub_edges.append(e)
12067
+
12068
+ try:
12069
+ return Graph.ByVerticesEdges(sub_vertices, sub_edges)
12070
+ except Exception:
12071
+ # As a fallback, some environments accept edges alone
12072
+ try:
12073
+ return Graph.ByEdges(sub_edges)
12074
+ except Exception:
12075
+ if not silent:
12076
+ print("Graph.KHopsSubgraph - Error: Failed to construct the subgraph. Returning None.")
12077
+ return None
12078
+
11219
12079
  @staticmethod
11220
12080
  def Laplacian(graph, silent: bool = False, normalized: bool = False):
11221
12081
  """
@@ -12650,90 +13510,201 @@ class Graph:
12650
13510
  return outgoing_vertices
12651
13511
 
12652
13512
  @staticmethod
12653
- 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
+ ):
12654
13525
  """
12655
- Calculates PageRank scores for vertices in a directed graph. see https://en.wikipedia.org/wiki/PageRank.
12656
-
12657
- Parameters
12658
- ----------
12659
- graph : topologic_core.Graph
12660
- The input graph.
12661
- alpha : float , optional
12662
- The damping (dampening) factor. Default is 0.85. See https://en.wikipedia.org/wiki/PageRank.
12663
- maxIterations : int , optional
12664
- The maximum number of iterations to calculate the page rank. Default is 100.
12665
- normalize : bool , optional
12666
- If set to True, the results will be normalized from 0 to 1. Otherwise, they won't be. Default is True.
12667
- directed : bool , optional
12668
- If set to True, the graph is considered as a directed graph. Otherwise, it will be considered as an undirected graph. Default is False.
12669
- key : str , optional
12670
- The dictionary key under which to store the page_rank score. Default is "page_rank"
12671
- colorKey : str , optional
12672
- The desired dictionary key under which to store the pagerank color. Default is "pr_color".
12673
- colorScale : str , optional
12674
- 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/.
12675
- In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
12676
- mantissa : int , optional
12677
- The desired length of the mantissa.
12678
- tolerance : float , optional
12679
- The desired tolerance. Default is 0.0001.
12680
-
12681
- Returns
12682
- -------
12683
- list
12684
- 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.
12685
13528
  """
12686
13529
  from topologicpy.Vertex import Vertex
12687
13530
  from topologicpy.Helper import Helper
12688
13531
  from topologicpy.Dictionary import Dictionary
12689
13532
  from topologicpy.Topology import Topology
12690
13533
  from topologicpy.Color import Color
13534
+ from topologicpy.Graph import Graph
12691
13535
 
12692
13536
  vertices = Graph.Vertices(graph)
12693
- num_vertices = len(vertices)
12694
- if num_vertices < 1:
13537
+ n = len(vertices)
13538
+ if n < 1:
12695
13539
  print("Graph.PageRank - Error: The input graph parameter has no vertices. Returning None")
12696
13540
  return None
12697
- initial_score = 1.0 / num_vertices
12698
- 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
+
12699
13582
  for _ in range(maxIterations):
12700
- new_scores = [0 for vertex in vertices]
12701
- for i, vertex in enumerate(vertices):
12702
- incoming_score = 0
12703
- for incoming_vertex in Graph.IncomingVertices(graph, vertex, directed=directed):
12704
- if len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed)) > 0:
12705
- vi = Vertex.Index(incoming_vertex, vertices, tolerance=tolerance)
12706
- if not vi == None:
12707
- incoming_score += values[vi] / len(Graph.IncomingVertices(graph, incoming_vertex, directed=directed))
12708
- new_scores[i] = alpha * incoming_score + (1 - alpha) / num_vertices
12709
-
12710
- # Check for convergence
12711
- 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
12712
13600
  break
13601
+ pr = new_pr
12713
13602
 
12714
- values = new_scores
12715
- if normalize == True:
12716
- if mantissa > 0: # We cannot round numbers from 0 to 1 with a mantissa = 0.
12717
- values = [round(v, mantissa) for v in Helper.Normalize(values)]
12718
- else:
12719
- values = Helper.Normalize(values)
12720
- min_value = 0
12721
- 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
12722
13609
  else:
12723
- min_value = min(values)
12724
- max_value = max(values)
13610
+ min_v, max_v = (min(pr), max(pr)) if n > 0 else (0.0, 0.0)
12725
13611
 
12726
- for i, value in enumerate(values):
13612
+ for i, value in enumerate(pr):
12727
13613
  d = Topology.Dictionary(vertices[i])
12728
- 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
+ )
12729
13617
  d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
12730
13618
  vertices[i] = Topology.SetDictionary(vertices[i], d)
12731
-
12732
- for i, v in enumerate(vertices):
12733
- d = Topology.Dictionary(v)
12734
- d = Dictionary.SetValueAtKey(d, key, values[i])
12735
- v = Topology.SetDictionary(v, d)
12736
- 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
12737
13708
 
12738
13709
  @staticmethod
12739
13710
  def Partition(graph, method: str = "Betweenness", n: int = 2, m: int = 10, key: str ="partition",
@@ -14267,7 +15238,7 @@ class Graph:
14267
15238
  return round(degree, mantissa)
14268
15239
 
14269
15240
  @staticmethod
14270
- def Vertices(graph, vertexKey=None, reverse=False):
15241
+ def Vertices(graph, sortBy=None, reverse=False):
14271
15242
  """
14272
15243
  Returns the list of vertices in the input graph.
14273
15244
 
@@ -14275,11 +15246,14 @@ class Graph:
14275
15246
  ----------
14276
15247
  graph : topologic_core.Graph
14277
15248
  The input graph.
14278
- vertexKey : str , optional
14279
- If set, the returned list of vertices is sorted according to the dicitonary values stored under this key. Default is None.
15249
+ sortBy : str , optional
15250
+ The dictionary key to use for sorting the returned edges. Special strings include "length" and "distance" to sort by the length of the edge. Default is None.
14280
15251
  reverse : bool , optional
14281
- If set to True, the vertices are sorted in reverse order (only if vertexKey is set). Otherwise, they are not. Default is False.
14282
-
15252
+ If set to True, the sorted list is reversed. This has no effect if the sortBy parameter is not set. Default is False.
15253
+ silent : bool, optional
15254
+ Isilent : bool, optional
15255
+ If set to True, all errors and warnings are suppressed. Default is False.
15256
+
14283
15257
  Returns
14284
15258
  -------
14285
15259
  list
@@ -14299,13 +15273,13 @@ class Graph:
14299
15273
  _ = graph.Vertices(vertices) # Hook to Core
14300
15274
  except:
14301
15275
  vertices = []
14302
- if not vertexKey == None:
14303
- sorting_values = []
15276
+ if not sortBy == None:
15277
+ vertex_values = []
14304
15278
  for v in vertices:
14305
15279
  d = Topology.Dictionary(v)
14306
- value = Dictionary.ValueAtKey(d, vertexKey)
14307
- sorting_values.append(value)
14308
- vertices = Helper.Sort(vertices, sorting_values)
15280
+ value = str(Dictionary.ValueAtKey(d, sortBy, "0"))
15281
+ vertex_values.append(value)
15282
+ vertices = Helper.Sort(vertices, vertex_values)
14309
15283
  if reverse == True:
14310
15284
  vertices.reverse()
14311
15285
  return vertices