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 +1633 -659
- topologicpy/Kuzu.py +495 -134
- topologicpy/Plotly.py +9 -7
- topologicpy/Topology.py +1 -0
- topologicpy/version.py +1 -1
- {topologicpy-0.8.57.dist-info → topologicpy-0.8.59.dist-info}/METADATA +1 -1
- {topologicpy-0.8.57.dist-info → topologicpy-0.8.59.dist-info}/RECORD +10 -10
- {topologicpy-0.8.57.dist-info → topologicpy-0.8.59.dist-info}/WHEEL +0 -0
- {topologicpy-0.8.57.dist-info → topologicpy-0.8.59.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.57.dist-info → topologicpy-0.8.59.dist-info}/top_level.txt +0 -0
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(
|
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
|
-
|
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
|
1957
|
+
from collections import deque
|
1958
|
+
import math
|
1942
1959
|
|
1943
|
-
|
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
|
-
|
1964
|
-
|
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
|
-
|
1967
|
-
|
1968
|
-
|
1969
|
-
|
1970
|
-
|
1971
|
-
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
1975
|
-
|
1976
|
-
|
1977
|
-
|
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
|
-
|
1980
|
-
|
1981
|
-
|
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
|
-
|
1984
|
-
|
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
|
-
|
1987
|
-
|
1988
|
-
|
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
|
-
|
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
|
-
|
3252
|
-
|
3253
|
-
|
3254
|
-
|
3255
|
-
|
3256
|
-
|
3257
|
-
|
3258
|
-
|
3259
|
-
|
3260
|
-
|
3261
|
-
|
3262
|
-
|
3263
|
-
|
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
|
-
|
3266
|
-
|
3267
|
-
|
3268
|
-
|
3269
|
-
|
3270
|
-
|
3271
|
-
|
3272
|
-
|
3273
|
-
|
3274
|
-
|
3275
|
-
|
3276
|
-
|
3277
|
-
|
3278
|
-
|
3279
|
-
|
3280
|
-
|
3281
|
-
|
3282
|
-
|
3283
|
-
|
3284
|
-
|
3285
|
-
|
3286
|
-
|
3287
|
-
|
3288
|
-
|
3289
|
-
|
3290
|
-
|
3291
|
-
|
3292
|
-
|
3293
|
-
|
3294
|
-
|
3295
|
-
|
3296
|
-
|
3297
|
-
|
3298
|
-
|
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
|
-
|
3307
|
-
|
3308
|
-
|
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
|
-
|
3335
|
-
|
3336
|
-
|
3337
|
-
|
3338
|
-
|
3339
|
-
|
3340
|
-
|
3341
|
-
|
3342
|
-
|
3343
|
-
|
3344
|
-
|
3345
|
-
|
3346
|
-
|
3347
|
-
|
3348
|
-
|
3349
|
-
|
3350
|
-
|
3351
|
-
|
3352
|
-
|
3353
|
-
|
3354
|
-
|
3355
|
-
|
3356
|
-
|
3357
|
-
|
3358
|
-
|
3359
|
-
|
3360
|
-
|
3361
|
-
|
3362
|
-
|
3363
|
-
|
3364
|
-
|
3365
|
-
|
3366
|
-
|
3367
|
-
|
3368
|
-
|
3369
|
-
|
3370
|
-
|
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
|
-
|
3387
|
-
|
3388
|
-
|
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
|
-
|
3391
|
-
|
3392
|
-
|
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
|
-
|
3395
|
-
|
3396
|
-
|
3397
|
-
|
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
|
-
|
3400
|
-
|
3401
|
-
|
3402
|
-
|
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
|
-
|
3405
|
-
|
3698
|
+
# # Dictionary to hold individual properties
|
3699
|
+
# properties = {}
|
3406
3700
|
|
3407
|
-
|
3408
|
-
|
3409
|
-
|
3410
|
-
|
3411
|
-
|
3412
|
-
|
3413
|
-
|
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
|
-
|
3416
|
-
|
3417
|
-
|
3709
|
+
# # Add this PSET to the dictionary for this entity
|
3710
|
+
# psets[pset_name] = properties
|
3711
|
+
# return psets
|
3418
3712
|
|
3419
|
-
|
3420
|
-
|
3713
|
+
# def get_color_transparency_material(entity):
|
3714
|
+
# import random
|
3421
3715
|
|
3422
|
-
|
3423
|
-
|
3424
|
-
|
3425
|
-
|
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
|
-
|
3428
|
-
|
3429
|
-
|
3430
|
-
|
3431
|
-
|
3432
|
-
|
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
|
-
|
3435
|
-
|
3436
|
-
|
3437
|
-
|
3438
|
-
|
3439
|
-
|
3440
|
-
|
3441
|
-
|
3442
|
-
|
3443
|
-
|
3444
|
-
|
3445
|
-
|
3446
|
-
|
3447
|
-
|
3448
|
-
|
3449
|
-
|
3450
|
-
|
3451
|
-
|
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
|
-
|
3454
|
-
|
3455
|
-
|
3456
|
-
|
3457
|
-
|
3458
|
-
|
3459
|
-
|
3460
|
-
|
3461
|
-
|
3462
|
-
|
3463
|
-
|
3464
|
-
|
3465
|
-
|
3466
|
-
|
3467
|
-
|
3468
|
-
|
3469
|
-
|
3470
|
-
|
3471
|
-
|
3472
|
-
|
3473
|
-
|
3474
|
-
|
3475
|
-
|
3476
|
-
|
3477
|
-
|
3478
|
-
|
3479
|
-
|
3480
|
-
|
3481
|
-
|
3482
|
-
|
3483
|
-
|
3484
|
-
|
3485
|
-
|
3486
|
-
|
3487
|
-
|
3488
|
-
|
3489
|
-
|
3490
|
-
|
3491
|
-
|
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
|
-
|
3494
|
-
|
3495
|
-
|
3496
|
-
|
3497
|
-
|
3498
|
-
|
3499
|
-
|
3500
|
-
|
3501
|
-
|
3502
|
-
|
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
|
-
|
3798
|
+
# return color, default_transparency, material_list
|
3505
3799
|
|
3506
|
-
|
3507
|
-
|
3508
|
-
|
3509
|
-
|
3510
|
-
|
3511
|
-
|
3512
|
-
|
3513
|
-
|
3514
|
-
|
3515
|
-
|
3516
|
-
|
3517
|
-
|
3518
|
-
|
3519
|
-
|
3520
|
-
|
3521
|
-
|
3522
|
-
|
3523
|
-
|
3524
|
-
|
3525
|
-
|
3526
|
-
|
3527
|
-
|
3528
|
-
|
3529
|
-
|
3530
|
-
|
3531
|
-
|
3532
|
-
|
3533
|
-
|
3534
|
-
|
3535
|
-
|
3536
|
-
|
3537
|
-
|
3538
|
-
|
3539
|
-
|
3540
|
-
|
3541
|
-
|
3542
|
-
|
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
|
-
|
3545
|
-
|
3546
|
-
|
3547
|
-
|
3548
|
-
|
3549
|
-
|
3550
|
-
|
3551
|
-
|
3552
|
-
|
3553
|
-
|
3554
|
-
|
3555
|
-
|
3556
|
-
|
3557
|
-
|
3558
|
-
|
3559
|
-
|
3560
|
-
|
3561
|
-
|
3562
|
-
|
3563
|
-
|
3564
|
-
|
3565
|
-
|
3566
|
-
|
3567
|
-
|
3568
|
-
|
3569
|
-
|
3570
|
-
|
3571
|
-
|
3572
|
-
|
3573
|
-
|
3574
|
-
|
3575
|
-
|
3576
|
-
|
3577
|
-
|
3578
|
-
|
3579
|
-
|
3580
|
-
|
3581
|
-
|
3582
|
-
|
3583
|
-
|
3584
|
-
|
3585
|
-
|
3586
|
-
|
3587
|
-
|
3588
|
-
|
3589
|
-
|
3590
|
-
|
3591
|
-
|
3592
|
-
|
3593
|
-
|
3594
|
-
|
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
|
-
|
3601
|
-
|
3602
|
-
|
3603
|
-
|
3604
|
-
|
3605
|
-
|
3606
|
-
|
3607
|
-
|
3608
|
-
|
3609
|
-
|
3610
|
-
|
3611
|
-
|
3612
|
-
|
3613
|
-
|
3614
|
-
|
3615
|
-
|
3616
|
-
|
3617
|
-
|
3618
|
-
|
3619
|
-
|
3620
|
-
|
3621
|
-
|
3622
|
-
|
3623
|
-
|
3624
|
-
|
3625
|
-
|
3626
|
-
|
3627
|
-
|
3628
|
-
|
3629
|
-
|
3630
|
-
|
3631
|
-
|
3632
|
-
|
3633
|
-
|
3634
|
-
|
3635
|
-
|
3636
|
-
|
3637
|
-
|
3638
|
-
|
3639
|
-
|
3640
|
-
|
3641
|
-
|
3642
|
-
|
3643
|
-
|
3644
|
-
|
3645
|
-
|
3646
|
-
|
3647
|
-
|
3648
|
-
|
3649
|
-
|
3650
|
-
|
3651
|
-
|
3652
|
-
|
3653
|
-
|
3654
|
-
|
3655
|
-
|
3656
|
-
|
3657
|
-
|
3658
|
-
|
3659
|
-
|
3660
|
-
|
3661
|
-
|
3662
|
-
|
3663
|
-
|
3664
|
-
|
3665
|
-
|
3666
|
-
|
3667
|
-
|
3668
|
-
|
3669
|
-
|
3670
|
-
|
3671
|
-
|
3672
|
-
|
3673
|
-
|
3674
|
-
|
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
|
-
|
6025
|
-
|
6026
|
-
|
6027
|
-
|
6028
|
-
|
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
|
6062
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
|
6364
|
+
wl = weightKey.lower()
|
6365
|
+
if ("len" in wl) or ("dis" in wl):
|
6090
6366
|
weightKey = "length"
|
6091
|
-
distance_attr = weightKey
|
6092
|
-
|
6093
|
-
# Build
|
6094
|
-
|
6095
|
-
|
6096
|
-
|
6097
|
-
|
6098
|
-
|
6099
|
-
|
6100
|
-
|
6101
|
-
|
6102
|
-
|
6103
|
-
|
6104
|
-
|
6105
|
-
|
6106
|
-
|
6107
|
-
|
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
|
-
|
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
|
-
|
6112
|
-
|
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
|
-
|
6115
|
-
|
6116
|
-
|
6117
|
-
|
6118
|
-
|
6119
|
-
|
6120
|
-
|
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
|
-
|
6123
|
-
|
6124
|
-
|
6125
|
-
|
6126
|
-
|
6127
|
-
|
6128
|
-
|
6129
|
-
|
6130
|
-
|
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
|
-
|
6133
|
-
|
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
|
-
|
6136
|
-
|
6683
|
+
# # Values for color scaling should reflect the displayed numbers
|
6684
|
+
# color_values = values_for_return
|
6137
6685
|
|
6138
|
-
|
6139
|
-
|
6140
|
-
|
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
|
-
|
6143
|
-
|
6144
|
-
|
6145
|
-
|
6146
|
-
|
6147
|
-
|
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
|
-
|
6150
|
-
|
6697
|
+
# if abs(max_value - min_value) < tolerance:
|
6698
|
+
# max_value = min_value + tolerance
|
6151
6699
|
|
6152
|
-
|
6153
|
-
|
6154
|
-
|
6155
|
-
|
6156
|
-
|
6157
|
-
|
6158
|
-
|
6159
|
-
|
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
|
-
|
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(
|
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
|
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 :
|
7498
|
+
graph : topologicpy.Graph
|
6939
7499
|
The input graph.
|
6940
|
-
vertices : list
|
6941
|
-
|
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
|
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
|
-
|
6955
|
-
|
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
|
-
|
6958
|
-
|
6959
|
-
|
6960
|
-
|
6961
|
-
|
6962
|
-
|
6963
|
-
|
6964
|
-
|
6965
|
-
|
6966
|
-
|
6967
|
-
|
6968
|
-
|
6969
|
-
|
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(
|
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
|
-
|
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
|
-
|
12694
|
-
if
|
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
|
-
|
12698
|
-
|
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
|
-
|
12701
|
-
for i
|
12702
|
-
|
12703
|
-
|
12704
|
-
|
12705
|
-
|
12706
|
-
|
12707
|
-
|
12708
|
-
|
12709
|
-
|
12710
|
-
|
12711
|
-
|
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
|
-
|
12715
|
-
if normalize
|
12716
|
-
|
12717
|
-
|
12718
|
-
|
12719
|
-
|
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
|
-
|
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(
|
13612
|
+
for i, value in enumerate(pr):
|
12727
13613
|
d = Topology.Dictionary(vertices[i])
|
12728
|
-
color = Color.AnyToHex(
|
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
|
-
|
12733
|
-
|
12734
|
-
|
12735
|
-
|
12736
|
-
|
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,
|
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
|
-
|
14279
|
-
|
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
|
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
|
14303
|
-
|
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,
|
14307
|
-
|
14308
|
-
vertices = Helper.Sort(vertices,
|
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
|