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 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.Flatten - Error: The input graph is not a valid topologic graph. Returning None.")
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
- if len(vertices) == 1:
1856
- vertices = vertices[0]
1857
- if isinstance(vertices, list):
1858
- if len(vertices) == 0:
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
- vertexList = Helper.Flatten(list(vertices))
1874
- vertexList = [x for x in vertexList if Topology.IsInstance(x, "Vertex")]
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.55'
1
+ __version__ = '0.8.57'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topologicpy
3
- Version: 0.8.55
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=Oa0oOrPoOSUGL5fvJYHBH_r6kRZ944wk-P828GyAjk4,705757
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=0f6HouARKaCuxhdxsUEYi8T9giJycnWhQ8Cn70YILBA,84885
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=EKCMooHLmkTpTBR1XTlkHxj-YkXZosf7ysuCeCcjiR8,23
34
- topologicpy-0.8.55.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
35
- topologicpy-0.8.55.dist-info/METADATA,sha256=BIWmEWd275UZuMnl423fCxI3Bv6-vZpot8niPNApQAE,10535
36
- topologicpy-0.8.55.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- topologicpy-0.8.55.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
38
- topologicpy-0.8.55.dist-info/RECORD,,
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,,