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