topologicpy 0.8.55__py3-none-any.whl → 0.8.57__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- topologicpy/Graph.py +220 -3
- topologicpy/Kuzu.py +589 -0
- topologicpy/Vertex.py +78 -48
- topologicpy/version.py +1 -1
- {topologicpy-0.8.55.dist-info → topologicpy-0.8.57.dist-info}/METADATA +1 -1
- {topologicpy-0.8.55.dist-info → topologicpy-0.8.57.dist-info}/RECORD +9 -8
- {topologicpy-0.8.55.dist-info → topologicpy-0.8.57.dist-info}/WHEEL +0 -0
- {topologicpy-0.8.55.dist-info → topologicpy-0.8.57.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.55.dist-info → topologicpy-0.8.57.dist-info}/top_level.txt +0 -0
topologicpy/Graph.py
CHANGED
@@ -448,6 +448,67 @@ class Graph:
|
|
448
448
|
new_graph = Graph.ByVerticesEdges(graph_vertices, graph_edges)
|
449
449
|
return new_graph
|
450
450
|
|
451
|
+
@staticmethod
|
452
|
+
def AddEdgeByIndex(graph, index: list = None, dictionary = None, silent: bool = False):
|
453
|
+
"""
|
454
|
+
Creates an edge in the input Graph by connecting the two vertices specified by their indices (e.g., [5, 6] connects the 4th and 6th vertices).
|
455
|
+
|
456
|
+
Parameters
|
457
|
+
----------
|
458
|
+
graph : topologic_core.Graph
|
459
|
+
The input graph.
|
460
|
+
index : list or tuple
|
461
|
+
The input list of vertex indices (e.g. [4, 6]).
|
462
|
+
dictionary : topologic_core.Dictionary , optional
|
463
|
+
The input edge dictionary.
|
464
|
+
silent : bool , optional
|
465
|
+
If set to True, error and warning messages are suppressed. Default is False.
|
466
|
+
|
467
|
+
Returns
|
468
|
+
-------
|
469
|
+
topologic_core.Graph
|
470
|
+
The input graph with the input edge added to it.
|
471
|
+
|
472
|
+
"""
|
473
|
+
from topologicpy.Edge import Edge
|
474
|
+
from topologicpy.Topology import Topology
|
475
|
+
|
476
|
+
if not Topology.IsInstance(graph, "Graph"):
|
477
|
+
if not silent:
|
478
|
+
print("Graph.AddEdgeIndex - Error: The input graph parameter is not a valid graph. Returning None.")
|
479
|
+
return None
|
480
|
+
if dictionary:
|
481
|
+
if not Topology.IsInstance(dictionary, "Dictionary"):
|
482
|
+
if not silent:
|
483
|
+
print("Graph.AddEdgeIndex - Error: The input dictionary parameter is not a valid dictionary. Returning None.")
|
484
|
+
return None
|
485
|
+
if not isinstance(index, list):
|
486
|
+
if not silent:
|
487
|
+
print("Graph.AddEdgeIndex - Error: The input index parameter is not a valid list. Returning None.")
|
488
|
+
return None
|
489
|
+
index = [x for x in index if isinstance(x, int)]
|
490
|
+
if not len(index) == 2:
|
491
|
+
if not silent:
|
492
|
+
print("Graph.AddEdgeIndex - Error: The input index parameter should only contain two integer numbers. Returning None.")
|
493
|
+
return None
|
494
|
+
vertices = Graph.Vertices(graph)
|
495
|
+
n = len(vertices)
|
496
|
+
if index[0] < 0 or index[0] > n-1:
|
497
|
+
if not silent:
|
498
|
+
print("Graph.AddEdgeIndex - Error: The first integer in the input index parameter does not exist in the input graph. Returning None.")
|
499
|
+
return None
|
500
|
+
if index[1] < 0 or index[1] > n-1:
|
501
|
+
if not silent:
|
502
|
+
print("Graph.AddEdgeIndex - Error: The second integer in the input index parameter does not exist in the input graph. Returning None.")
|
503
|
+
return None
|
504
|
+
sv = vertices[index[0]]
|
505
|
+
ev = vertices[index[1]]
|
506
|
+
edge = Edge.ByVertices(sv, ev)
|
507
|
+
if dictionary:
|
508
|
+
edge = Topology.SetDictionary(edge, dictionary)
|
509
|
+
graph = Graph.AddEdge(graph,edge)
|
510
|
+
return graph
|
511
|
+
|
451
512
|
@staticmethod
|
452
513
|
def AddVertex(graph, vertex, tolerance: float = 0.0001, silent: bool = False):
|
453
514
|
"""
|
@@ -2835,6 +2896,158 @@ class Graph:
|
|
2835
2896
|
graphs.append(Graph.ByVerticesEdges(vertices, edges))
|
2836
2897
|
return {'graphs':graphs, 'labels':labels}
|
2837
2898
|
|
2899
|
+
@staticmethod
|
2900
|
+
def ByDictionaries(graphDictionary, vertexDictionaries, edgeDictionaries, vertexKey: str = None, edgeKey: str = None, silent: bool = False, tolerance: float = 0.0001):
|
2901
|
+
"""
|
2902
|
+
Creates a graph from input python dictionaries.
|
2903
|
+
|
2904
|
+
Rules:
|
2905
|
+
All vertex dictionaries must contain at least the vertexKey.
|
2906
|
+
All edge dictionaries must contain at least the edgeKey.
|
2907
|
+
The edgeKey must be a tuple or list of two str values.
|
2908
|
+
x,y,z coordinates are optional. However, if a vertex dictionary contains x,y,z coordinates then all vertex dictionaries must contain x,y,z coordinates.
|
2909
|
+
If vertex dictionaries contain x,y,z coordinates they must not overlap and be separated by a distance greater than tolerance.
|
2910
|
+
Keys and values are case sensitive.
|
2911
|
+
x,y,z keys, if present must be lowercase.
|
2912
|
+
|
2913
|
+
Example:
|
2914
|
+
graphDictionary = {"name": "Small Apartment", "location": "123 Main Street"}
|
2915
|
+
vertexDictionaries = [
|
2916
|
+
{"name":"Entry", "type":"Circulation", "x":1, "y":4, "z":0, "area":5},
|
2917
|
+
{"name":"Living Room", "type":"Living Room", "x":3, "y":4 , "z":0, "area":24},
|
2918
|
+
{"name":"Dining Room", "type":"Dining Room", "x":5, "y":2, "z":0, "area":18},
|
2919
|
+
{"name":"Kitchen", "type":"Kitchen", "x":1, "y":2, "z":0, "area":15},
|
2920
|
+
{"name":"Bathroom", "type":"Bathroom", "x":3, "y":6, "z":0, "area":9},
|
2921
|
+
{"name":"Bedroom", "type":"Bedroom", "x":5, "y":4, "z":0, "area":16}
|
2922
|
+
]
|
2923
|
+
edgeDictionaries = [
|
2924
|
+
{"connects": ("Entry","Living Room"), "relationship": "adjacent_to"},
|
2925
|
+
{"connects": ("Living Room","Kitchen"), "relationship": "adjacent_to"},
|
2926
|
+
{"connects": ("Dining Room","Kitchen"), "relationship": "adjacent_to"},
|
2927
|
+
{"connects": ("Living Room","Dining Room"), "relationship": "adjacent_to"},
|
2928
|
+
{"connects": ("Living Room","Bedroom"), "relationship": "adjacent_to"},
|
2929
|
+
{"connects": ("Living Room","Bathroom"), "relationship": "adjacent_to"}
|
2930
|
+
]
|
2931
|
+
vertexKey = "name"
|
2932
|
+
edgeKey = "connects"
|
2933
|
+
|
2934
|
+
Parameters
|
2935
|
+
----------
|
2936
|
+
graphDictionary : dict
|
2937
|
+
The python dictionary to associate with the resulting graph
|
2938
|
+
vertexDictionaries : list
|
2939
|
+
The input list of vertex dictionaries. These must contain the vertexKey. X,Y,Z coordinates are optional.
|
2940
|
+
edgeDictionaries : list
|
2941
|
+
The input list of edge dictionaries. These must have the edgeKey to specify the two vertices they connect (by using the vertexKey)
|
2942
|
+
vertexKey: str
|
2943
|
+
The vertex key used to identify which vertices and edge connects.
|
2944
|
+
edgeKey: str
|
2945
|
+
The edge key under which the pair of vertex keys are listed as a tuple or list.
|
2946
|
+
tolerance: float , optional
|
2947
|
+
The desired tolerance. The default is 0.0001
|
2948
|
+
silent : bool , optional
|
2949
|
+
If set to True, error and warning messages are suppressed. Default is False.
|
2950
|
+
|
2951
|
+
Returns
|
2952
|
+
-------
|
2953
|
+
topologic_core.Graph
|
2954
|
+
The resulting graph
|
2955
|
+
|
2956
|
+
"""
|
2957
|
+
from topologicpy.Vertex import Vertex
|
2958
|
+
from topologicpy.Edge import Edge
|
2959
|
+
from topologicpy.Cluster import Cluster
|
2960
|
+
from topologicpy.Topology import Topology
|
2961
|
+
from topologicpy.Dictionary import Dictionary
|
2962
|
+
|
2963
|
+
def _set_dict(obj, kv: dict):
|
2964
|
+
keys = list(kv.keys())
|
2965
|
+
vals = list(kv.values())
|
2966
|
+
d = Dictionary.ByKeysValues(keys, vals)
|
2967
|
+
Topology.SetDictionary(obj, d)
|
2968
|
+
return obj
|
2969
|
+
|
2970
|
+
def _vertex(vertexDictionary, vertices, vertexKey, tolerance=0.0001, silent=False):
|
2971
|
+
x = vertexDictionary.get("x", 0)
|
2972
|
+
y = vertexDictionary.get("y", 0)
|
2973
|
+
z = vertexDictionary.get("z", 0)
|
2974
|
+
v = Vertex.ByCoordinates(x, y, z)
|
2975
|
+
v = _set_dict(v, vertexDictionary)
|
2976
|
+
if "x" in vertexDictionary.keys(): # Check for overlap only if coords are given.
|
2977
|
+
if len(vertices) > 0:
|
2978
|
+
nv = Vertex.NearestVertex(v, Cluster.ByTopologies(vertices))
|
2979
|
+
d = Topology.Dictionary(nv)
|
2980
|
+
nv_name = Dictionary.ValueAtKey(d, vertexKey, "Unknown")
|
2981
|
+
if Vertex.Distance(v, nv) < tolerance:
|
2982
|
+
if not silent:
|
2983
|
+
v_name = vertexDictionary[vertexKey]
|
2984
|
+
print(f"Graph.ByDictionaries - Warning: Vertices {v_name} and {nv_name} overlap.")
|
2985
|
+
return v
|
2986
|
+
|
2987
|
+
|
2988
|
+
if graphDictionary:
|
2989
|
+
if not isinstance(graphDictionary, dict):
|
2990
|
+
if not silent:
|
2991
|
+
print("Graph.ByDictionaries - Error: The input graphDictionary parameter is not a valid python dictionary. Returning None.")
|
2992
|
+
return None
|
2993
|
+
|
2994
|
+
if not isinstance(vertexDictionaries, list):
|
2995
|
+
if not silent:
|
2996
|
+
print("Graph.ByDictionaries - Error: The input vertexDictionaries parameter is not a valid list. Returning None.")
|
2997
|
+
return None
|
2998
|
+
|
2999
|
+
if not isinstance(edgeDictionaries, list):
|
3000
|
+
if not silent:
|
3001
|
+
print("Graph.ByDictionaries - Error: The input edgeDictionaries parameter is not a valid list. Returning None.")
|
3002
|
+
return None
|
3003
|
+
|
3004
|
+
name_to_vertex = {}
|
3005
|
+
vertices = []
|
3006
|
+
for vd in vertexDictionaries:
|
3007
|
+
v = _vertex(vd, vertices, vertexKey=vertexKey, tolerance=tolerance, silent=silent)
|
3008
|
+
if v:
|
3009
|
+
vertices.append(v)
|
3010
|
+
|
3011
|
+
# If coordinates are not present, make sure you separate the vertices to allow edges to be created.
|
3012
|
+
if "x" not in vertexDictionaries[0].keys():
|
3013
|
+
vertices = Vertex.Separate(vertices, minDistance=max(1, tolerance))
|
3014
|
+
|
3015
|
+
for i, v in enumerate(vertices):
|
3016
|
+
vd = vertexDictionaries[i]
|
3017
|
+
name_to_vertex[vd[vertexKey]] = v
|
3018
|
+
|
3019
|
+
# Create adjacency edges (undirected: one edge per pair)
|
3020
|
+
edges = []
|
3021
|
+
for d in edgeDictionaries:
|
3022
|
+
a, b = d[edgeKey]
|
3023
|
+
va = name_to_vertex.get(a, None)
|
3024
|
+
vb = name_to_vertex.get(b, None)
|
3025
|
+
if not va and not vb:
|
3026
|
+
if not silent:
|
3027
|
+
print(f"Graph.ByDictionaries - Warning: vertices '{a}' and '{b}' are missing. Could not create an edge between them.")
|
3028
|
+
continue
|
3029
|
+
if not va:
|
3030
|
+
if not silent:
|
3031
|
+
print(f"Graph.ByDictionaries - Warning: vertex '{a}' is missing. Could not create an edge between '{a}' and '{b}'.")
|
3032
|
+
continue
|
3033
|
+
if not vb:
|
3034
|
+
if not silent:
|
3035
|
+
print(f"Graph.ByDictionaries - Warning: vertex '{b}' is missing. Could not create an edge between '{a}' and '{b}'.")
|
3036
|
+
continue
|
3037
|
+
e = Edge.ByStartVertexEndVertex(va, vb, silent=True)
|
3038
|
+
if not e:
|
3039
|
+
if not silent:
|
3040
|
+
print(f"Graph.ByDictionaries - Warning: Could not create an edge between '{a}' and '{b}'. Check if the distance betwen '{a}' and '{b}' is kess than the input tolerance.")
|
3041
|
+
continue
|
3042
|
+
edges.append(e)
|
3043
|
+
# Build graph
|
3044
|
+
g = Graph.ByVerticesEdges(vertices, edges)
|
3045
|
+
|
3046
|
+
# Attach graph-level metadata
|
3047
|
+
if graphDictionary:
|
3048
|
+
_set_dict(g, graphDictionary)
|
3049
|
+
return g
|
3050
|
+
|
2838
3051
|
@staticmethod
|
2839
3052
|
def ByIFCFile(file,
|
2840
3053
|
includeTypes: list = [],
|
@@ -9117,8 +9330,6 @@ class Graph:
|
|
9117
9330
|
|
9118
9331
|
return pos
|
9119
9332
|
|
9120
|
-
|
9121
|
-
|
9122
9333
|
def radial_layout_2d(edge_list, root_index=0):
|
9123
9334
|
import numpy as np
|
9124
9335
|
from collections import deque
|
@@ -9270,9 +9481,15 @@ class Graph:
|
|
9270
9481
|
|
9271
9482
|
if not Topology.IsInstance(graph, "Graph"):
|
9272
9483
|
if not silent:
|
9273
|
-
print("Graph.
|
9484
|
+
print("Graph.Reshape - Error: The input graph is not a valid topologic graph. Returning None.")
|
9274
9485
|
return None
|
9275
9486
|
|
9487
|
+
vertices = Graph.Vertices(graph)
|
9488
|
+
if len(vertices) < 2:
|
9489
|
+
if not silent:
|
9490
|
+
print("Graph.Reshape - Warning: The graph has less than two vertices. It cannot be rehsaped. Returning the original input graph.")
|
9491
|
+
return graph
|
9492
|
+
|
9276
9493
|
if 'circ' in shape.lower():
|
9277
9494
|
return circle_layout_2d(graph, radius=size/2, sides=sides)
|
9278
9495
|
elif 'lin' in shape.lower():
|
topologicpy/Kuzu.py
ADDED
@@ -0,0 +1,589 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import threading, contextlib, time, json
|
3
|
+
from typing import Dict, Any, List, Optional
|
4
|
+
|
5
|
+
|
6
|
+
# Optional TopologicPy imports (make this file safe to import without TopologicPy)
|
7
|
+
from topologicpy.Graph import Graph
|
8
|
+
from topologicpy.Vertex import Vertex
|
9
|
+
from topologicpy.Edge import Edge
|
10
|
+
from topologicpy.Dictionary import Dictionary
|
11
|
+
from topologicpy.Topology import Topology
|
12
|
+
|
13
|
+
import os
|
14
|
+
import warnings
|
15
|
+
|
16
|
+
try:
|
17
|
+
import kuzu
|
18
|
+
except:
|
19
|
+
print("Kuzu - Installing required kuzu library.")
|
20
|
+
try:
|
21
|
+
os.system("pip install kuzu")
|
22
|
+
except:
|
23
|
+
os.system("pip install kuzu --user")
|
24
|
+
try:
|
25
|
+
import kuzu
|
26
|
+
except:
|
27
|
+
warnings.warn("Kuzu - Error: Could not import Kuzu.")
|
28
|
+
kuzu = None
|
29
|
+
|
30
|
+
|
31
|
+
class _DBCache:
|
32
|
+
"""
|
33
|
+
One kuzu.Database per path. Thread-safe and process-local.
|
34
|
+
"""
|
35
|
+
def __init__(self):
|
36
|
+
self._lock = threading.RLock()
|
37
|
+
self._cache: Dict[str, "kuzu.Database"] = {}
|
38
|
+
|
39
|
+
def get(self, path: str) -> "kuzu.Database":
|
40
|
+
if kuzu is None:
|
41
|
+
raise "Kuzu - Error: Kuzu is not available"
|
42
|
+
with self._lock:
|
43
|
+
db = self._cache.get(path)
|
44
|
+
if db is None:
|
45
|
+
db = kuzu.Database(path)
|
46
|
+
self._cache[path] = db
|
47
|
+
return db
|
48
|
+
|
49
|
+
class _WriteGate:
|
50
|
+
"""
|
51
|
+
Serialize writes to avoid IO lock contention.
|
52
|
+
"""
|
53
|
+
def __init__(self):
|
54
|
+
self._lock = threading.RLock()
|
55
|
+
|
56
|
+
@contextlib.contextmanager
|
57
|
+
def hold(self):
|
58
|
+
with self._lock:
|
59
|
+
yield
|
60
|
+
|
61
|
+
_db_cache = _DBCache()
|
62
|
+
_write_gate = _WriteGate()
|
63
|
+
|
64
|
+
class _ConnectionPool:
|
65
|
+
"""
|
66
|
+
Per-thread kuzu.Connection pool bound to a Database instance.
|
67
|
+
"""
|
68
|
+
def __init__(self, db: "kuzu.Database"):
|
69
|
+
self.db = db
|
70
|
+
self._local = threading.local()
|
71
|
+
|
72
|
+
def _ensure(self) -> "kuzu.Connection":
|
73
|
+
if not hasattr(self._local, "conn"):
|
74
|
+
self._local.conn = kuzu.Connection(self.db)
|
75
|
+
return self._local.conn
|
76
|
+
|
77
|
+
@contextlib.contextmanager
|
78
|
+
def connection(self, write: bool = False, retries: int = 5, backoff: float = 0.15):
|
79
|
+
conn = self._ensure()
|
80
|
+
if not write:
|
81
|
+
yield conn
|
82
|
+
return
|
83
|
+
# Serialize writes and retry transient failures
|
84
|
+
with _write_gate.hold():
|
85
|
+
attempt = 0
|
86
|
+
while True:
|
87
|
+
try:
|
88
|
+
yield conn
|
89
|
+
break
|
90
|
+
except Exception as e:
|
91
|
+
attempt += 1
|
92
|
+
if attempt > retries:
|
93
|
+
raise f"Kuzu write failed after {retries} retries: {e}"
|
94
|
+
time.sleep(backoff * attempt)
|
95
|
+
|
96
|
+
class _Mgr:
|
97
|
+
"""
|
98
|
+
Lightweight facade (per-db-path) providing read/write execution and schema bootstrap.
|
99
|
+
"""
|
100
|
+
def __init__(self, db_path: str):
|
101
|
+
self.db_path = db_path
|
102
|
+
self._db = _db_cache.get(db_path)
|
103
|
+
self._pool = _ConnectionPool(self._db)
|
104
|
+
|
105
|
+
@contextlib.contextmanager
|
106
|
+
def read(self):
|
107
|
+
with self._pool.connection(write=False) as c:
|
108
|
+
yield c
|
109
|
+
|
110
|
+
@contextlib.contextmanager
|
111
|
+
def write(self):
|
112
|
+
with self._pool.connection(write=True) as c:
|
113
|
+
yield c
|
114
|
+
|
115
|
+
def exec(self, query: str, params: Optional[dict] = None, write: bool = False):
|
116
|
+
with (self.write() if write else self.read()) as c:
|
117
|
+
with c.execute(query, parameters=params or {}) as res:
|
118
|
+
try:
|
119
|
+
return res.rows_as_dict().get_all()
|
120
|
+
except Exception:
|
121
|
+
return None
|
122
|
+
|
123
|
+
def ensure_schema(self):
|
124
|
+
# Node tables
|
125
|
+
self.exec("""
|
126
|
+
CREATE NODE TABLE IF NOT EXISTS GraphCard(
|
127
|
+
id STRING,
|
128
|
+
label STRING,
|
129
|
+
num_nodes INT64,
|
130
|
+
num_edges INT64,
|
131
|
+
props STRING,
|
132
|
+
PRIMARY KEY(id)
|
133
|
+
);
|
134
|
+
""", write=True)
|
135
|
+
self.exec("""
|
136
|
+
CREATE NODE TABLE IF NOT EXISTS Vertex(
|
137
|
+
id STRING,
|
138
|
+
graph_id STRING,
|
139
|
+
label STRING,
|
140
|
+
x DOUBLE,
|
141
|
+
y DOUBLE,
|
142
|
+
z DOUBLE,
|
143
|
+
props STRING,
|
144
|
+
PRIMARY KEY(id)
|
145
|
+
);
|
146
|
+
""", write=True)
|
147
|
+
|
148
|
+
# Relationship tables
|
149
|
+
self.exec("""
|
150
|
+
CREATE REL TABLE IF NOT EXISTS CONNECT(FROM Vertex TO Vertex, label STRING, props STRING);
|
151
|
+
""", write=True)
|
152
|
+
|
153
|
+
# Figure out later if we need sessions and steps
|
154
|
+
# self.exec("""
|
155
|
+
# CREATE NODE TABLE IF NOT EXISTS Session(
|
156
|
+
# id STRING,
|
157
|
+
# title STRING,
|
158
|
+
# created_at STRING,
|
159
|
+
# PRIMARY KEY(id)
|
160
|
+
# );
|
161
|
+
# """, write=True)
|
162
|
+
# self.exec("""
|
163
|
+
# CREATE NODE TABLE IF NOT EXISTS Step(
|
164
|
+
# id STRING,
|
165
|
+
# session_id STRING,
|
166
|
+
# idx INT64,
|
167
|
+
# action STRING,
|
168
|
+
# ok BOOL,
|
169
|
+
# message STRING,
|
170
|
+
# snapshot_before STRING,
|
171
|
+
# snapshot_after STRING,
|
172
|
+
# evidence STRING,
|
173
|
+
# created_at STRING,
|
174
|
+
# PRIMARY KEY(id)
|
175
|
+
# );
|
176
|
+
# """, write=True)
|
177
|
+
# self.exec("CREATE REL TABLE IF NOT EXISTS SessionHasStep(FROM Session TO Step);", write=True)
|
178
|
+
|
179
|
+
|
180
|
+
class Kuzu:
|
181
|
+
"""
|
182
|
+
TopologicPy-style class of static methods for Kùzu integration.
|
183
|
+
|
184
|
+
Notes
|
185
|
+
-----
|
186
|
+
- All methods are *static* to match TopologicPy's style.
|
187
|
+
- Graph persistence:
|
188
|
+
* Vertices: stored in `Vertex` with (id, graph_id, label, props JSON)
|
189
|
+
* Edges: stored as `CONNECT` relations a->b with label + props JSON
|
190
|
+
* We assume undirected design intent; only one CONNECT is stored (a->b),
|
191
|
+
but TopologicPy Graph treats edges as undirected by default.
|
192
|
+
"""
|
193
|
+
|
194
|
+
# ---------- Core (DB + Connection + Schema) ----------
|
195
|
+
@staticmethod
|
196
|
+
def EnsureSchema(db_path: str, silent: bool = False) -> bool:
|
197
|
+
"""
|
198
|
+
Ensures the required Kùzu schema exists in the database at `db_path`.
|
199
|
+
|
200
|
+
Parameters
|
201
|
+
----------
|
202
|
+
db_path : str
|
203
|
+
Path to the Kùzu database directory. It will be created if it does not exist.
|
204
|
+
silent : bool , optional
|
205
|
+
If True, suppresses error messages. Default is False.
|
206
|
+
|
207
|
+
Returns
|
208
|
+
-------
|
209
|
+
bool
|
210
|
+
True if successful, False otherwise.
|
211
|
+
"""
|
212
|
+
try:
|
213
|
+
mgr = _Mgr(db_path)
|
214
|
+
mgr.ensure_schema()
|
215
|
+
return True
|
216
|
+
except Exception as e:
|
217
|
+
if not silent:
|
218
|
+
print(f"Kuzu.EnsureSchema - Error: {e}")
|
219
|
+
return False
|
220
|
+
|
221
|
+
@staticmethod
|
222
|
+
def Database(db_path: str):
|
223
|
+
"""
|
224
|
+
Returns the underlying `kuzu.Database` instance for `db_path`.
|
225
|
+
"""
|
226
|
+
return _db_cache.get(db_path)
|
227
|
+
|
228
|
+
@staticmethod
|
229
|
+
def Connection(db_path: str):
|
230
|
+
"""
|
231
|
+
Returns a `kuzu.Connection` bound to the database at `db_path`.
|
232
|
+
"""
|
233
|
+
mgr = _Mgr(db_path)
|
234
|
+
with mgr.read() as c:
|
235
|
+
return c # Note: returns a live connection (do not use across threads)
|
236
|
+
|
237
|
+
# ---------- Graph <-> DB Conversion ----------
|
238
|
+
|
239
|
+
@staticmethod
|
240
|
+
def UpsertGraph(db_path: str,
|
241
|
+
graph,
|
242
|
+
graphIDKey: Optional[str] = None,
|
243
|
+
vertexIDKey: Optional[str] = None,
|
244
|
+
vertexLabelKey: Optional[str] = None,
|
245
|
+
mantissa: int = 6,
|
246
|
+
silent: bool = False) -> str:
|
247
|
+
"""
|
248
|
+
Upserts (deletes prior + inserts new) a TopologicPy graph and its GraphCard.
|
249
|
+
|
250
|
+
Parameters
|
251
|
+
----------
|
252
|
+
db_path : str
|
253
|
+
Kùzu database path.
|
254
|
+
graph : topologicpy.Graph
|
255
|
+
The input TopologicPy graph.
|
256
|
+
graphIDKey : str , optional
|
257
|
+
The graph dictionary key under which the graph ID is stored. If None, a UUID is generated.
|
258
|
+
title, domain, geo, time_start, time_end, summary : str , optional
|
259
|
+
Optional metadata for GraphCard.
|
260
|
+
silent : bool , optional
|
261
|
+
If True, suppresses error messages. Default is False.
|
262
|
+
|
263
|
+
Returns
|
264
|
+
-------
|
265
|
+
str
|
266
|
+
The graph_id used.
|
267
|
+
"""
|
268
|
+
from topologicpy.Topology import Topology
|
269
|
+
from topologicpy.Dictionary import Dictionary
|
270
|
+
d = Topology.Dictionary(graph)
|
271
|
+
if graphIDKey is None:
|
272
|
+
gid = Topology.UUID(graph)
|
273
|
+
else:
|
274
|
+
gid = Dictionary.ValueAtKey(d, graphIDKey, Topology.UUID(graph))
|
275
|
+
g_props = Dictionary.PythonDictionary(d)
|
276
|
+
mesh_data = Graph.MeshData(graph, mantissa=mantissa)
|
277
|
+
verts = mesh_data['vertices']
|
278
|
+
v_props = mesh_data['vertexDictionaries']
|
279
|
+
edges = mesh_data['edges']
|
280
|
+
e_props = mesh_data['edgeDictionaries']
|
281
|
+
num_nodes = len(verts)
|
282
|
+
num_edges = len(edges)
|
283
|
+
mgr = _Mgr(db_path)
|
284
|
+
try:
|
285
|
+
mgr.ensure_schema()
|
286
|
+
# Upsert GraphCard
|
287
|
+
mgr.exec("MATCH (g:GraphCard) WHERE g.id = $id DELETE g;", {"id": gid}, write=True)
|
288
|
+
mgr.exec("""
|
289
|
+
CREATE (g:GraphCard {id:$id, num_nodes:$num_nodes, num_edges: $num_edges, props:$props});
|
290
|
+
""", {"id": gid, "num_nodes": num_nodes, "num_edges": num_edges, "props": json.dumps(g_props)}, write=True)
|
291
|
+
|
292
|
+
# Remove existing vertices/edges for this graph_id
|
293
|
+
mgr.exec("""
|
294
|
+
MATCH (a:Vertex)-[r:CONNECT]->(b:Vertex)
|
295
|
+
WHERE a.graph_id = $gid AND b.graph_id = $gid
|
296
|
+
DELETE r;
|
297
|
+
""", {"gid": gid}, write=True)
|
298
|
+
mgr.exec("MATCH (v:Vertex) WHERE v.graph_id = $gid DELETE v;", {"gid": gid}, write=True)
|
299
|
+
|
300
|
+
# Insert vertices
|
301
|
+
for i, v in enumerate(verts):
|
302
|
+
x,y,z = v
|
303
|
+
if vertexIDKey is None:
|
304
|
+
vid = f"{gid}:{i}"
|
305
|
+
else:
|
306
|
+
vid = v_props[i].get(vertexIDKey, f"{gid}:{i}")
|
307
|
+
if vertexLabelKey is None:
|
308
|
+
label = str(i)
|
309
|
+
else:
|
310
|
+
label = v_props[i].get(vertexIDKey, str(i))
|
311
|
+
mgr.exec("""
|
312
|
+
CREATE (v:Vertex {id:$id, graph_id:$gid, label:$label, props:$props, x:$x, y:$y, z:$z});
|
313
|
+
""", {"id": vid, "gid": gid, "label": label, "x": x, "y": y, "z": z,
|
314
|
+
"props": json.dumps(v_props[i])}, write=True)
|
315
|
+
|
316
|
+
# Insert edges
|
317
|
+
for i, e in enumerate(edges):
|
318
|
+
a_id = v_props[e[0]].get(vertexIDKey, f"{gid}:{e[0]}")
|
319
|
+
b_id = v_props[e[1]].get(vertexIDKey, f"{gid}:{e[1]}")
|
320
|
+
mgr.exec("""
|
321
|
+
MATCH (a:Vertex {id:$a}), (b:Vertex {id:$b})
|
322
|
+
CREATE (a)-[:CONNECT {label:$label, props:$props}]->(b);
|
323
|
+
""", {"a": a_id, "b": b_id,
|
324
|
+
"label": e_props[i].get("label", str(i)),
|
325
|
+
"props": json.dumps(e_props[i])}, write=True)
|
326
|
+
|
327
|
+
return gid
|
328
|
+
except Exception as e:
|
329
|
+
if not silent:
|
330
|
+
print(f"Kuzu.UpsertGraph - Error: {e}")
|
331
|
+
raise
|
332
|
+
|
333
|
+
@staticmethod
|
334
|
+
def GraphByID(db_path: str, graphID: str, silent: bool = False):
|
335
|
+
"""
|
336
|
+
Reads a graph with id `graph_id` from Kùzu and constructs a TopologicPy graph.
|
337
|
+
|
338
|
+
Returns
|
339
|
+
-------
|
340
|
+
topologicpy.Graph
|
341
|
+
A new TopologicPy Graph, or None on error.
|
342
|
+
"""
|
343
|
+
# if TGraph is None:
|
344
|
+
# raise _KuzuError("TopologicPy is required to use Kuzu.ReadTopologicGraph.")
|
345
|
+
import random
|
346
|
+
mgr = _Mgr(db_path)
|
347
|
+
|
348
|
+
try:
|
349
|
+
mgr.ensure_schema()
|
350
|
+
# Read the GraphCard
|
351
|
+
g = mgr.exec("""
|
352
|
+
MATCH (g:GraphCard) WHERE g.id = $id
|
353
|
+
RETURN g.id AS id, g.num_nodes AS num_nodes, g.num_edges AS num_edges, g.props AS props
|
354
|
+
;
|
355
|
+
""", {"id": graphID}, write=False) or None
|
356
|
+
if g is None:
|
357
|
+
return None
|
358
|
+
g = g[0]
|
359
|
+
g_dict = dict(json.loads(g.get("props") or "{}") or {})
|
360
|
+
g_dict = Dictionary.ByPythonDictionary(g_dict)
|
361
|
+
# Read vertices
|
362
|
+
rows_v = mgr.exec("""
|
363
|
+
MATCH (v:Vertex) WHERE v.graph_id = $gid
|
364
|
+
RETURN v.id AS id, v.label AS label, v.x AS x, v.y AS y, v.z AS z, v.props AS props
|
365
|
+
ORDER BY id;
|
366
|
+
""", {"gid": graphID}, write=False) or []
|
367
|
+
|
368
|
+
id_to_vertex = {}
|
369
|
+
vertices = []
|
370
|
+
for row in rows_v:
|
371
|
+
try:
|
372
|
+
x = row.get("x") or random.uniform(0,1000)
|
373
|
+
y = row.get("y") or random.uniform(0,1000)
|
374
|
+
z = row.get("z") or random.uniform(0,1000)
|
375
|
+
except:
|
376
|
+
x = random.uniform(0,1000)
|
377
|
+
y = random.uniform(0,1000)
|
378
|
+
z = random.uniform(0,1000)
|
379
|
+
v = Vertex.ByCoordinates(x,y,z)
|
380
|
+
props = {}
|
381
|
+
try:
|
382
|
+
props = json.loads(row.get("props") or "{}")
|
383
|
+
except Exception:
|
384
|
+
props = {}
|
385
|
+
# Ensure 'label' key present
|
386
|
+
props = dict(props or {})
|
387
|
+
if "label" not in props:
|
388
|
+
props["label"] = row.get("label") or ""
|
389
|
+
d = Dictionary.ByKeysValues(list(props.keys()), list(props.values()))
|
390
|
+
v = Topology.SetDictionary(v, d)
|
391
|
+
id_to_vertex[row["id"]] = v
|
392
|
+
vertices.append(v)
|
393
|
+
|
394
|
+
# Read edges
|
395
|
+
rows_e = mgr.exec("""
|
396
|
+
MATCH (a:Vertex)-[r:CONNECT]->(b:Vertex)
|
397
|
+
WHERE a.graph_id = $gid AND b.graph_id = $gid
|
398
|
+
RETURN a.id AS a_id, b.id AS b_id, r.label AS label, r.props AS props;
|
399
|
+
""", {"gid": graphID}, write=False) or []
|
400
|
+
edges = []
|
401
|
+
for row in rows_e:
|
402
|
+
va = id_to_vertex.get(row["a_id"])
|
403
|
+
vb = id_to_vertex.get(row["b_id"])
|
404
|
+
if not va or not vb:
|
405
|
+
continue
|
406
|
+
e = Edge.ByStartVertexEndVertex(va, vb)
|
407
|
+
props = {}
|
408
|
+
try:
|
409
|
+
props = json.loads(row.get("props") or "{}")
|
410
|
+
except Exception:
|
411
|
+
props = {}
|
412
|
+
props = dict(props or {})
|
413
|
+
if "label" not in props:
|
414
|
+
props["label"] = row.get("label") or "connect"
|
415
|
+
d = Dictionary.ByKeysValues(list(props.keys()), list(props.values()))
|
416
|
+
e = Topology.SetDictionary(e, d)
|
417
|
+
edges.append(e)
|
418
|
+
if len(vertices) > 0:
|
419
|
+
g = Graph.ByVerticesEdges(vertices, edges)
|
420
|
+
g = Topology.SetDictionary(g, g_dict)
|
421
|
+
else:
|
422
|
+
g = None
|
423
|
+
return g
|
424
|
+
except Exception as e:
|
425
|
+
if not silent:
|
426
|
+
print(f"Kuzu.GraphByID - Error: {e}")
|
427
|
+
return None
|
428
|
+
|
429
|
+
@staticmethod
|
430
|
+
def GraphsByQuery(
|
431
|
+
db_path: str,
|
432
|
+
query: str,
|
433
|
+
params: dict | None = None,
|
434
|
+
graphIDKey: str = "graph_id",
|
435
|
+
silent: bool = False,
|
436
|
+
):
|
437
|
+
"""
|
438
|
+
Executes a Kùzu Cypher query and returns a list of TopologicPy Graphs.
|
439
|
+
The query should return at least one column identifying each graph.
|
440
|
+
By default this column is expected to be named 'graph_id', but you can
|
441
|
+
override that via `graph_id_field`.
|
442
|
+
|
443
|
+
The method will:
|
444
|
+
1) run the query,
|
445
|
+
2) extract distinct graph IDs from the result set (using `graph_id_field`
|
446
|
+
if present; otherwise it attempts to infer IDs from common fields like
|
447
|
+
'a_id', 'b_id', or 'id' that look like '<graph_id>:<vertex_index>'),
|
448
|
+
3) reconstruct each graph via Kuzu.ReadTopologicGraph(...).
|
449
|
+
|
450
|
+
Parameters
|
451
|
+
----------
|
452
|
+
db_path : str
|
453
|
+
Path to the Kùzu database directory.
|
454
|
+
query : str
|
455
|
+
A valid Kùzu Cypher query.
|
456
|
+
params : dict , optional
|
457
|
+
Parameters to pass with the query.
|
458
|
+
graph_id_field : str , optional
|
459
|
+
The field name in the query result that contains the graph ID(s).
|
460
|
+
Default is "graph_id".
|
461
|
+
silent : bool , optional
|
462
|
+
If True, suppresses errors and returns an empty list on failure.
|
463
|
+
|
464
|
+
Returns
|
465
|
+
-------
|
466
|
+
list[topologicpy.Graph]
|
467
|
+
A list of reconstructed TopologicPy graphs.
|
468
|
+
"""
|
469
|
+
# if TGraph is None:
|
470
|
+
# raise _KuzuError("TopologicPy is required to use Kuzu.GraphsFromQuery.")
|
471
|
+
|
472
|
+
try:
|
473
|
+
mgr = _Mgr(db_path)
|
474
|
+
mgr.ensure_schema()
|
475
|
+
rows = mgr.exec(query, params or {}, write=False) or []
|
476
|
+
|
477
|
+
# Collect distinct graph IDs
|
478
|
+
gids = []
|
479
|
+
for r in rows:
|
480
|
+
gid = r.get(graphIDKey)
|
481
|
+
|
482
|
+
# Fallback: try to infer from common id fields like "<graph_id>:<i>"
|
483
|
+
if gid is None:
|
484
|
+
for k in ("a_id", "b_id", "id"):
|
485
|
+
v = r.get(k)
|
486
|
+
if isinstance(v, str) and ":" in v:
|
487
|
+
gid = v.split(":", 1)[0]
|
488
|
+
break
|
489
|
+
|
490
|
+
if gid and gid not in gids:
|
491
|
+
gids.append(gid)
|
492
|
+
|
493
|
+
# Reconstruct each graph
|
494
|
+
graphs = []
|
495
|
+
for gid in gids:
|
496
|
+
g = Kuzu.GraphByID(db_path, gid, silent=True)
|
497
|
+
if g is not None:
|
498
|
+
graphs.append(g)
|
499
|
+
return graphs
|
500
|
+
|
501
|
+
except Exception as e:
|
502
|
+
if not silent:
|
503
|
+
print(f"Kuzu.GraphsByQuery - Error: {e}")
|
504
|
+
return []
|
505
|
+
|
506
|
+
@staticmethod
|
507
|
+
def DeleteGraph(db_path: str, graph_id: str, silent: bool = False) -> bool:
|
508
|
+
"""
|
509
|
+
Deletes a graph (vertices/edges) and its GraphCard by id.
|
510
|
+
"""
|
511
|
+
try:
|
512
|
+
mgr = _Mgr(db_path)
|
513
|
+
mgr.ensure_schema()
|
514
|
+
# Delete edges
|
515
|
+
mgr.exec("""
|
516
|
+
MATCH (a:Vertex)-[r:CONNECT]->(b:Vertex)
|
517
|
+
WHERE a.graph_id = $gid AND b.graph_id = $gid
|
518
|
+
DELETE r;
|
519
|
+
""", {"gid": graph_id}, write=True)
|
520
|
+
# Delete vertices
|
521
|
+
mgr.exec("MATCH (v:Vertex) WHERE v.graph_id = $gid DELETE v;", {"gid": graph_id}, write=True)
|
522
|
+
# Delete card
|
523
|
+
mgr.exec("MATCH (g:GraphCard) WHERE g.id = $gid DELETE g;", {"gid": graph_id}, write=True)
|
524
|
+
return True
|
525
|
+
except Exception as e:
|
526
|
+
if not silent:
|
527
|
+
print(f"Kuzu.DeleteGraph - Error: {e}")
|
528
|
+
return False
|
529
|
+
|
530
|
+
@staticmethod
|
531
|
+
def EmptyDatabase(db_path: str, drop_schema: bool = False, recreate_schema: bool = True, silent: bool = False) -> bool:
|
532
|
+
"""
|
533
|
+
Empties the Kùzu database at `db_path`.
|
534
|
+
|
535
|
+
Two modes:
|
536
|
+
- Soft clear (default): delete ALL relationships, then ALL nodes across all tables.
|
537
|
+
- Hard reset (drop_schema=True): drop known node/rel tables, optionally recreate schema.
|
538
|
+
|
539
|
+
Parameters
|
540
|
+
----------
|
541
|
+
db_path : str
|
542
|
+
Path to the Kùzu database directory.
|
543
|
+
drop_schema : bool , optional
|
544
|
+
If True, DROP the known tables instead of deleting rows. Default False.
|
545
|
+
recreate_schema : bool , optional
|
546
|
+
If True and drop_schema=True, re-create the minimal schema after dropping. Default True.
|
547
|
+
silent : bool , optional
|
548
|
+
Suppress errors if True. Default False.
|
549
|
+
|
550
|
+
Returns
|
551
|
+
-------
|
552
|
+
bool
|
553
|
+
True on success, False otherwise.
|
554
|
+
"""
|
555
|
+
try:
|
556
|
+
mgr = _Mgr(db_path)
|
557
|
+
# Ensure DB exists (does not create tables unless needed)
|
558
|
+
mgr.ensure_schema()
|
559
|
+
|
560
|
+
if drop_schema:
|
561
|
+
# Drop relationship tables FIRST (to release dependencies), then node tables.
|
562
|
+
# IF EXISTS is convenient; if your Kùzu version doesn't support it, remove and ignore exceptions.
|
563
|
+
for stmt in [
|
564
|
+
"DROP TABLE IF EXISTS CONNECT;",
|
565
|
+
"DROP TABLE IF EXISTS Vertex;",
|
566
|
+
"DROP TABLE IF EXISTS GraphCard;",
|
567
|
+
]:
|
568
|
+
try:
|
569
|
+
mgr.exec(stmt, write=True)
|
570
|
+
except Exception as _e:
|
571
|
+
if not silent:
|
572
|
+
print(f"Kuzu.EmptyDatabase - Warning dropping table: {_e}")
|
573
|
+
|
574
|
+
if recreate_schema:
|
575
|
+
mgr.ensure_schema()
|
576
|
+
return True
|
577
|
+
|
578
|
+
# Soft clear: remove all relationships, then all nodes (covers all labels/tables).
|
579
|
+
# Delete all edges (any direction)
|
580
|
+
mgr.exec("MATCH (a)-[r]->(b) DELETE r;", write=True)
|
581
|
+
# Delete all nodes (from all node tables)
|
582
|
+
mgr.exec("MATCH (n) DELETE n;", write=True)
|
583
|
+
return True
|
584
|
+
|
585
|
+
except Exception as e:
|
586
|
+
if not silent:
|
587
|
+
print(f"Kuzu.EmptyDatabase - Error: {e}")
|
588
|
+
return False
|
589
|
+
|
topologicpy/Vertex.py
CHANGED
@@ -1818,6 +1818,7 @@ class Vertex():
|
|
1818
1818
|
pt = project_point_onto_plane(Vertex.Coordinates(vertex), [eq["a"], eq["b"], eq["c"], eq["d"]], direction)
|
1819
1819
|
return Vertex.ByCoordinates(pt[0], pt[1], pt[2])
|
1820
1820
|
|
1821
|
+
|
1821
1822
|
@staticmethod
|
1822
1823
|
def Separate(*vertices, minDistance: float = 0.0001, iterations: int = 100, strength: float = 0.1, tolerance: float = 0.0001, silent: bool = False):
|
1823
1824
|
"""
|
@@ -1846,68 +1847,97 @@ class Vertex():
|
|
1846
1847
|
"""
|
1847
1848
|
from topologicpy.Topology import Topology
|
1848
1849
|
from topologicpy.Helper import Helper
|
1850
|
+
from topologicpy.Vertex import Vertex
|
1849
1851
|
import math
|
1852
|
+
from collections import defaultdict
|
1850
1853
|
|
1854
|
+
# --- Gather & validate inputs ---
|
1851
1855
|
if len(vertices) == 0:
|
1852
1856
|
if not silent:
|
1853
1857
|
print("Vertex.Separate - Error: The input vertices parameter is an empty list. Returning None.")
|
1854
1858
|
return None
|
1855
|
-
|
1856
|
-
|
1857
|
-
|
1858
|
-
|
1859
|
-
if not silent:
|
1860
|
-
print("Vertex.Separate - Error: The input vertices parameter is an empty list. Returning None.")
|
1861
|
-
return None
|
1862
|
-
else:
|
1863
|
-
vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
|
1864
|
-
if len(vertexList) == 0:
|
1865
|
-
if not silent:
|
1866
|
-
print("Vertex.Separate - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
|
1867
|
-
return None
|
1868
|
-
else:
|
1869
|
-
if not silent:
|
1870
|
-
print("Vertex.Separate - Warning: The input vertices parameter contains only one vertex. Returning the same vertex.")
|
1871
|
-
return vertices
|
1859
|
+
|
1860
|
+
# Allow either a single list or varargs
|
1861
|
+
if len(vertices) == 1 and isinstance(vertices[0], list):
|
1862
|
+
raw_list = vertices[0]
|
1872
1863
|
else:
|
1873
|
-
|
1874
|
-
|
1864
|
+
raw_list = Helper.Flatten(list(vertices))
|
1865
|
+
|
1866
|
+
vertexList = [v for v in raw_list if Topology.IsInstance(v, "Vertex")]
|
1875
1867
|
if len(vertexList) == 0:
|
1876
1868
|
if not silent:
|
1877
1869
|
print("Vertex.Separate - Error: The input parameters do not contain any valid vertices. Returning None.")
|
1878
1870
|
return None
|
1871
|
+
if len(vertexList) == 1:
|
1872
|
+
if not silent:
|
1873
|
+
print("Vertex.Separate - Warning: Only one vertex supplied. Returning it unchanged.")
|
1874
|
+
return vertexList
|
1875
|
+
|
1876
|
+
minDistance = float(minDistance) + float(tolerance) # safety margin
|
1877
|
+
n = len(vertexList)
|
1878
|
+
|
1879
|
+
# Mutable coordinates
|
1880
|
+
coords = [[vertexList[i].X(), vertexList[i].Y(), vertexList[i].Z()] for i in range(n)]
|
1881
|
+
dicts = [Topology.Dictionary(v) for v in vertexList]
|
1882
|
+
|
1883
|
+
# --- Pre-seed coincident vertices so they can start moving ---
|
1884
|
+
# Cluster indices by quantized coordinate to catch exact (or near-exact) duplicates
|
1885
|
+
key_scale = max(tolerance, 1e-12)
|
1886
|
+
clusters = defaultdict(list)
|
1887
|
+
for idx, (x, y, z) in enumerate(coords):
|
1888
|
+
key = (round(x / key_scale), round(y / key_scale), round(z / key_scale))
|
1889
|
+
clusters[key].append(idx)
|
1890
|
+
|
1891
|
+
# For any cluster with >1 vertex, spread them on a small circle in XY
|
1892
|
+
for idxs in clusters.values():
|
1893
|
+
k = len(idxs)
|
1894
|
+
if k > 1:
|
1895
|
+
r = minDistance * 0.5 # small initial spread; repulsion will take it from here
|
1896
|
+
for m, idx in enumerate(idxs):
|
1897
|
+
ang = (2.0 * math.pi * m) / k
|
1898
|
+
coords[idx][0] += r * math.cos(ang)
|
1899
|
+
coords[idx][1] += r * math.sin(ang)
|
1900
|
+
# leave Z unchanged to avoid unintended vertical drift
|
1901
|
+
|
1902
|
+
# --- Repulsion simulation ---
|
1903
|
+
eps = 1e-12
|
1904
|
+
for _ in range(int(iterations)):
|
1905
|
+
all_ok = True
|
1906
|
+
for i in range(n):
|
1907
|
+
xi, yi, zi = coords[i]
|
1908
|
+
for j in range(i + 1, n):
|
1909
|
+
xj, yj, zj = coords[j]
|
1910
|
+
dx = xj - xi
|
1911
|
+
dy = yj - yi
|
1912
|
+
dz = zj - zi
|
1913
|
+
dist_sq = dx*dx + dy*dy + dz*dz
|
1914
|
+
if dist_sq <= 0.0:
|
1915
|
+
# still coincident: nudge with a tiny deterministic push along x
|
1916
|
+
dx, dy, dz = (eps, 0.0, 0.0)
|
1917
|
+
dist_sq = eps*eps
|
1918
|
+
dist = math.sqrt(dist_sq)
|
1919
|
+
|
1920
|
+
if dist < minDistance:
|
1921
|
+
all_ok = False
|
1922
|
+
# Repulsion magnitude; clamp denominator to avoid blow-ups
|
1923
|
+
repel = (minDistance - dist) / max(dist, eps) * float(strength)
|
1924
|
+
# Split the move equally
|
1925
|
+
sx = 0.5 * dx * repel
|
1926
|
+
sy = 0.5 * dy * repel
|
1927
|
+
sz = 0.5 * dz * repel
|
1928
|
+
coords[i][0] -= sx; coords[i][1] -= sy; coords[i][2] -= sz
|
1929
|
+
coords[j][0] += sx; coords[j][1] += sy; coords[j][2] += sz
|
1930
|
+
if all_ok:
|
1931
|
+
break # everything already at least minDistance apart
|
1932
|
+
|
1933
|
+
# --- Rebuild vertices & restore dictionaries ---
|
1934
|
+
new_vertices = [Vertex.ByCoordinates(x, y, z) for (x, y, z) in coords]
|
1935
|
+
for i in range(n):
|
1936
|
+
new_vertices[i] = Topology.SetDictionary(new_vertices[i], dicts[i])
|
1879
1937
|
|
1880
|
-
minDistance = minDistance + tolerance # Add a bit of a safety factor
|
1881
|
-
|
1882
|
-
# Convert to mutable coordinates
|
1883
|
-
coords = [[v.X(), v.Y(), v.Z()] for v in vertices]
|
1884
|
-
|
1885
|
-
for _ in range(iterations):
|
1886
|
-
for i in range(len(coords)):
|
1887
|
-
for j in range(i + 1, len(coords)):
|
1888
|
-
dx = coords[j][0] - coords[i][0]
|
1889
|
-
dy = coords[j][1] - coords[i][1]
|
1890
|
-
dz = coords[j][2] - coords[i][2]
|
1891
|
-
dist = math.sqrt(dx*dx + dy*dy + dz*dz)
|
1892
|
-
|
1893
|
-
if dist < minDistance and dist > 1e-9:
|
1894
|
-
# Calculate repulsion vector
|
1895
|
-
repel = (minDistance - dist) / dist * strength
|
1896
|
-
shift = [dx * repel * 0.5, dy * repel * 0.5, dz * repel * 0.5]
|
1897
|
-
coords[i][0] -= shift[0]
|
1898
|
-
coords[i][1] -= shift[1]
|
1899
|
-
coords[i][2] -= shift[2]
|
1900
|
-
coords[j][0] += shift[0]
|
1901
|
-
coords[j][1] += shift[1]
|
1902
|
-
coords[j][2] += shift[2]
|
1903
|
-
|
1904
|
-
# Reconstruct TopologicPy Vertex objects
|
1905
|
-
new_vertices = [Vertex.ByCoordinates(x, y, z) for x, y, z in coords]
|
1906
|
-
# Transfer the dictionaries
|
1907
|
-
for i, v in enumerate(new_vertices):
|
1908
|
-
v = Topology.SetDictionary(v, Topology.Dictionary(vertices[i]))
|
1909
1938
|
return new_vertices
|
1910
1939
|
|
1940
|
+
|
1911
1941
|
@staticmethod
|
1912
1942
|
def Transform(vertex, matrix, mantissa: int = 6, silent: bool = False):
|
1913
1943
|
"""
|
topologicpy/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = '0.8.
|
1
|
+
__version__ = '0.8.57'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: topologicpy
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.57
|
4
4
|
Summary: An AI-Powered Spatial Modelling and Analysis Software Library for Architecture, Engineering, and Construction.
|
5
5
|
Author-email: Wassim Jabi <wassim.jabi@gmail.com>
|
6
6
|
License: AGPL v3 License
|
@@ -12,10 +12,11 @@ topologicpy/Dictionary.py,sha256=Z4YQ88tONWd-0X0dENQ8IZqIOa9mbBqhJkTBsHmft2g,446
|
|
12
12
|
topologicpy/Edge.py,sha256=DifItuyabFDUFC7CVMlt2DeMFMNaGOqCg43iU9CPP0A,74029
|
13
13
|
topologicpy/EnergyModel.py,sha256=hB1aiJe45gdDMFm1AhkBr-1djjtXSzn24iRpQMk43-4,57749
|
14
14
|
topologicpy/Face.py,sha256=aX9EcR3JGbLITElhd25J0Z8m9U8KkmbYivGg3oZN-Uw,202296
|
15
|
-
topologicpy/Graph.py,sha256=
|
15
|
+
topologicpy/Graph.py,sha256=EdB8N58bm74Uy6Y9xTrNgtewU-wx8Tq7y8EhyZ50wvs,716133
|
16
16
|
topologicpy/Grid.py,sha256=3OsBMyHh4w8gpFOTMKHMNTpo62V0CwRNu5cwm87yDUA,18421
|
17
17
|
topologicpy/Helper.py,sha256=Nr6pyzl0sZm4Cu11wOqoYKu6yYal5N6A9jErXnaZBJc,31765
|
18
18
|
topologicpy/Honeybee.py,sha256=DzaG9wpkJdcDWcjOGXhuN5X0gCqypmZGBa1y5E2MkjU,48964
|
19
|
+
topologicpy/Kuzu.py,sha256=4j6yPMOlCtugXKO-nrjjige98MeYOE1PfzKdFEenpjk,20959
|
19
20
|
topologicpy/Matrix.py,sha256=bOofT34G3YHu9aMIWx60YHAJga4R0GbDjsZBUD4Hu_k,22706
|
20
21
|
topologicpy/Neo4j.py,sha256=J8jU_mr5-mWC0Lg_D2dMjMlx1rY_eh8ks_aubUuTdWw,22319
|
21
22
|
topologicpy/Plotly.py,sha256=kF7JwBMWJQAuGezaJYI6Cq7ErNwEtcKzaExOfdGPIMc,123003
|
@@ -27,12 +28,12 @@ topologicpy/Speckle.py,sha256=-eiTqJugd7pHiHpD3pDUcDO6CGhVyPV14HFRzaqEoaw,18187
|
|
27
28
|
topologicpy/Sun.py,sha256=8S6dhCKfOhUGVny-jEk87Q08anLYMB1JEBKRGCklvbQ,36670
|
28
29
|
topologicpy/Topology.py,sha256=R5Ac3V_ADDRDZjhpcNvhM3AvDOLU6ORvB3yILyEkxnI,472559
|
29
30
|
topologicpy/Vector.py,sha256=pEC8YY3TeHGfGdeNgvdHjgMDwxGabp5aWjwYC1HSvMk,42236
|
30
|
-
topologicpy/Vertex.py,sha256=
|
31
|
+
topologicpy/Vertex.py,sha256=r_3cicgpino96ymm1ANptfOuqE59b99YWwksxyPOYK4,85914
|
31
32
|
topologicpy/Wire.py,sha256=gjgQUGHdBdXUIijgZc_VIW0E39w-smaVhhdl0jF63fQ,230466
|
32
33
|
topologicpy/__init__.py,sha256=RMftibjgAnHB1vdL-muo71RwMS4972JCxHuRHOlU428,928
|
33
|
-
topologicpy/version.py,sha256=
|
34
|
-
topologicpy-0.8.
|
35
|
-
topologicpy-0.8.
|
36
|
-
topologicpy-0.8.
|
37
|
-
topologicpy-0.8.
|
38
|
-
topologicpy-0.8.
|
34
|
+
topologicpy/version.py,sha256=9nWlfIrJh0NFxdTerGFsViWVZBvjxJ997KXXkuz9AiM,23
|
35
|
+
topologicpy-0.8.57.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
|
36
|
+
topologicpy-0.8.57.dist-info/METADATA,sha256=fHGtx2lc9MaC1L2EVY49mFCTJBz9V6zKGFpX08QyrZQ,10535
|
37
|
+
topologicpy-0.8.57.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
38
|
+
topologicpy-0.8.57.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
|
39
|
+
topologicpy-0.8.57.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|