topologicpy 0.8.59__py3-none-any.whl → 0.8.71__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/BVH.py +478 -211
- topologicpy/Cell.py +5 -2
- topologicpy/Dictionary.py +17 -6
- topologicpy/Face.py +23 -50
- topologicpy/Graph.py +660 -42
- topologicpy/Kuzu.py +1 -1
- topologicpy/Shell.py +50 -14
- topologicpy/Topology.py +138 -108
- topologicpy/Vertex.py +333 -99
- topologicpy/version.py +1 -1
- {topologicpy-0.8.59.dist-info → topologicpy-0.8.71.dist-info}/METADATA +1 -1
- {topologicpy-0.8.59.dist-info → topologicpy-0.8.71.dist-info}/RECORD +15 -15
- {topologicpy-0.8.59.dist-info → topologicpy-0.8.71.dist-info}/WHEEL +0 -0
- {topologicpy-0.8.59.dist-info → topologicpy-0.8.71.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.59.dist-info → topologicpy-0.8.71.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
|
|
@@ -4060,6 +4046,380 @@ class Graph:
|
|
4060
4046
|
removeCoplanarFaces=removeCoplanarFaces,
|
4061
4047
|
xMin=xMin, yMin=yMin, zMin=zMin, xMax=xMax, yMax=yMax, zMax=zMax)
|
4062
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
|
+
|
4063
4423
|
@staticmethod
|
4064
4424
|
def ByMeshData(vertices, edges, vertexDictionaries=None, edgeDictionaries=None, tolerance=0.0001):
|
4065
4425
|
"""
|
@@ -4119,7 +4479,7 @@ class Graph:
|
|
4119
4479
|
return Graph.ByVerticesEdges(g_vertices, g_edges)
|
4120
4480
|
|
4121
4481
|
@staticmethod
|
4122
|
-
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):
|
4123
4483
|
"""
|
4124
4484
|
Converts the input NetworkX graph into a topologic Graph. See http://networkx.org
|
4125
4485
|
|
@@ -4133,8 +4493,10 @@ class Graph:
|
|
4133
4493
|
The dictionary key under which to find the Y-Coordinate of the vertex. Default is 'y'.
|
4134
4494
|
zKey : str , optional
|
4135
4495
|
The dictionary key under which to find the Z-Coordinate of the vertex. Default is 'z'.
|
4136
|
-
|
4137
|
-
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)
|
4138
4500
|
mantissa : int , optional
|
4139
4501
|
The number of decimal places to round the result to. Default is 6.
|
4140
4502
|
tolerance : float , optional
|
@@ -4153,38 +4515,221 @@ class Graph:
|
|
4153
4515
|
|
4154
4516
|
import random
|
4155
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)
|
4661
|
+
|
4662
|
+
# Numeric
|
4663
|
+
try:
|
4664
|
+
return _round_number(val, mantissa)
|
4665
|
+
except Exception:
|
4666
|
+
return round(float(default), mantissa)
|
4156
4667
|
|
4157
|
-
|
4158
|
-
|
4668
|
+
# Create a mapping from NetworkX nodes to TopologicPy vertices
|
4669
|
+
nx_to_topologic_vertex = {}
|
4159
4670
|
|
4160
4671
|
# Create TopologicPy vertices for each node in the NetworkX graph
|
4161
4672
|
vertices = []
|
4162
4673
|
for node, data in nxGraph.nodes(data=True):
|
4163
|
-
#
|
4164
|
-
x = round(data.get(xKey, random.uniform(*range)), mantissa)
|
4165
|
-
y = round(data.get(yKey, random.uniform(*range)), mantissa)
|
4166
|
-
z = round(data.get(zKey, 0), mantissa) # If there are no Z values, this is probably a flat graph.
|
4167
|
-
# Create a TopologicPy vertex with the node data dictionary
|
4168
|
-
vertex = Vertex.ByCoordinates(x,y,z)
|
4674
|
+
# Clean the node dictionary
|
4169
4675
|
cleaned_values = []
|
4170
|
-
|
4171
|
-
|
4172
|
-
|
4173
|
-
cleaned_values.append(
|
4174
|
-
|
4175
|
-
|
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)
|
4176
4714
|
vertex = Topology.SetDictionary(vertex, node_dict)
|
4177
|
-
|
4715
|
+
|
4716
|
+
#nx_to_topologic_vertex[node] = vertex
|
4178
4717
|
vertices.append(vertex)
|
4179
4718
|
|
4180
4719
|
# Create TopologicPy edges for each edge in the NetworkX graph
|
4181
4720
|
edges = []
|
4182
4721
|
for u, v, data in nxGraph.edges(data=True):
|
4183
|
-
start_vertex =
|
4184
|
-
end_vertex =
|
4722
|
+
start_vertex = vertices[u]
|
4723
|
+
end_vertex = vertices[v]
|
4185
4724
|
|
4186
4725
|
# Create a TopologicPy edge with the edge data dictionary
|
4187
|
-
|
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)
|
4188
4733
|
edge = Edge.ByVertices([start_vertex, end_vertex], tolerance=tolerance)
|
4189
4734
|
edge = Topology.SetDictionary(edge, edge_dict)
|
4190
4735
|
edges.append(edge)
|
@@ -8015,13 +8560,13 @@ class Graph:
|
|
8015
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".
|
8016
8561
|
nodeTrainRatio : float , optional
|
8017
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.
|
8018
|
-
This value is ignored if an nodeMaskKey is
|
8563
|
+
This value is ignored if an nodeMaskKey is found.
|
8019
8564
|
nodeValidateRatio : float , optional
|
8020
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.
|
8021
|
-
This value is ignored if an nodeMaskKey is
|
8566
|
+
This value is ignored if an nodeMaskKey is found.
|
8022
8567
|
nodeTestRatio : float , optional
|
8023
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.
|
8024
|
-
This value is ignored if an nodeMaskKey is
|
8569
|
+
This value is ignored if an nodeMaskKey is found.
|
8025
8570
|
mantissa : int , optional
|
8026
8571
|
The number of decimal places to round the result to. Default is 6.
|
8027
8572
|
tolerance : float , optional
|
@@ -8888,7 +9433,7 @@ class Graph:
|
|
8888
9433
|
return False
|
8889
9434
|
|
8890
9435
|
@staticmethod
|
8891
|
-
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):
|
8892
9437
|
"""
|
8893
9438
|
Exports the input graph to a JSON file.
|
8894
9439
|
|
@@ -8898,6 +9443,8 @@ class Graph:
|
|
8898
9443
|
The input graph.
|
8899
9444
|
path : str
|
8900
9445
|
The path to the JSON file.
|
9446
|
+
propertiesKey : str , optional
|
9447
|
+
The desired key name to call graph properties. Default is "properties".
|
8901
9448
|
verticesKey : str , optional
|
8902
9449
|
The desired key name to call vertices. Default is "vertices".
|
8903
9450
|
edgesKey : str , optional
|
@@ -8947,7 +9494,7 @@ class Graph:
|
|
8947
9494
|
except:
|
8948
9495
|
raise Exception("Graph.ExportToJSON - Error: Could not create a new file at the following location: "+path)
|
8949
9496
|
if (f):
|
8950
|
-
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)
|
8951
9498
|
if jsondata != None:
|
8952
9499
|
json.dump(jsondata, f, indent=indent, sort_keys=sortKeys)
|
8953
9500
|
f.close()
|
@@ -10467,8 +11014,26 @@ class Graph:
|
|
10467
11014
|
The created GraphViz graph.
|
10468
11015
|
"""
|
10469
11016
|
|
10470
|
-
|
10471
|
-
|
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
|
+
|
10472
11037
|
from topologicpy.Graph import Graph
|
10473
11038
|
from topologicpy.Topology import Topology
|
10474
11039
|
from topologicpy.Dictionary import Dictionary
|
@@ -11662,6 +12227,7 @@ class Graph:
|
|
11662
12227
|
|
11663
12228
|
@staticmethod
|
11664
12229
|
def JSONData(graph,
|
12230
|
+
propertiesKey: str = "properties",
|
11665
12231
|
verticesKey: str = "vertices",
|
11666
12232
|
edgesKey: str = "edges",
|
11667
12233
|
vertexLabelKey: str = "",
|
@@ -11681,6 +12247,8 @@ class Graph:
|
|
11681
12247
|
----------
|
11682
12248
|
graph : topologic_core.Graph
|
11683
12249
|
The input graph.
|
12250
|
+
propertiesKey : str , optional
|
12251
|
+
The desired key name to call the graph properties. Default is "properties".
|
11684
12252
|
verticesKey : str , optional
|
11685
12253
|
The desired key name to call vertices. Default is "vertices".
|
11686
12254
|
edgesKey : str , optional
|
@@ -11720,8 +12288,10 @@ class Graph:
|
|
11720
12288
|
from topologicpy.Dictionary import Dictionary
|
11721
12289
|
from topologicpy.Helper import Helper
|
11722
12290
|
|
12291
|
+
graph_d = Dictionary.PythonDictionary(Topology.Dictionary(graph))
|
11723
12292
|
vertices = Graph.Vertices(graph)
|
11724
12293
|
j_data = {}
|
12294
|
+
j_data[propertiesKey] = graph_d
|
11725
12295
|
j_data[verticesKey] = {}
|
11726
12296
|
j_data[edgesKey] = {}
|
11727
12297
|
n = max(len(str(len(vertices))), 4)
|
@@ -14266,6 +14836,54 @@ class Graph:
|
|
14266
14836
|
_ = graph.RemoveEdges([edge], tolerance) # Hook to Core
|
14267
14837
|
return graph
|
14268
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
|
+
|
14269
14887
|
@staticmethod
|
14270
14888
|
def RemoveIsolatedVertices(graph, tolerance=0.0001):
|
14271
14889
|
"""
|