exonware-xwnode 0.0.1.12__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.
- exonware/__init__.py +14 -0
- exonware/xwnode/__init__.py +127 -0
- exonware/xwnode/base.py +676 -0
- exonware/xwnode/config.py +178 -0
- exonware/xwnode/contracts.py +730 -0
- exonware/xwnode/errors.py +503 -0
- exonware/xwnode/facade.py +460 -0
- exonware/xwnode/strategies/__init__.py +158 -0
- exonware/xwnode/strategies/advisor.py +463 -0
- exonware/xwnode/strategies/edges/__init__.py +32 -0
- exonware/xwnode/strategies/edges/adj_list.py +227 -0
- exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
- exonware/xwnode/strategies/edges/base.py +169 -0
- exonware/xwnode/strategies/flyweight.py +328 -0
- exonware/xwnode/strategies/impls/__init__.py +13 -0
- exonware/xwnode/strategies/impls/_base_edge.py +403 -0
- exonware/xwnode/strategies/impls/_base_node.py +307 -0
- exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
- exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
- exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
- exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
- exonware/xwnode/strategies/impls/edge_coo.py +533 -0
- exonware/xwnode/strategies/impls/edge_csc.py +447 -0
- exonware/xwnode/strategies/impls/edge_csr.py +492 -0
- exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
- exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
- exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
- exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
- exonware/xwnode/strategies/impls/edge_octree.py +574 -0
- exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
- exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
- exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
- exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
- exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
- exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
- exonware/xwnode/strategies/manager.py +775 -0
- exonware/xwnode/strategies/metrics.py +538 -0
- exonware/xwnode/strategies/migration.py +432 -0
- exonware/xwnode/strategies/nodes/__init__.py +50 -0
- exonware/xwnode/strategies/nodes/_base_node.py +307 -0
- exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
- exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
- exonware/xwnode/strategies/nodes/array_list.py +209 -0
- exonware/xwnode/strategies/nodes/base.py +247 -0
- exonware/xwnode/strategies/nodes/deque.py +200 -0
- exonware/xwnode/strategies/nodes/hash_map.py +135 -0
- exonware/xwnode/strategies/nodes/heap.py +307 -0
- exonware/xwnode/strategies/nodes/linked_list.py +232 -0
- exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
- exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
- exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
- exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
- exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
- exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
- exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
- exonware/xwnode/strategies/nodes/node_btree.py +357 -0
- exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
- exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
- exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
- exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
- exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
- exonware/xwnode/strategies/nodes/node_heap.py +191 -0
- exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
- exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
- exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
- exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
- exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
- exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
- exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
- exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
- exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
- exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
- exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
- exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
- exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
- exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
- exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
- exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
- exonware/xwnode/strategies/nodes/node_treap.py +387 -0
- exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
- exonware/xwnode/strategies/nodes/node_trie.py +252 -0
- exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
- exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
- exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
- exonware/xwnode/strategies/nodes/queue.py +161 -0
- exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
- exonware/xwnode/strategies/nodes/stack.py +152 -0
- exonware/xwnode/strategies/nodes/trie.py +274 -0
- exonware/xwnode/strategies/nodes/union_find.py +283 -0
- exonware/xwnode/strategies/pattern_detector.py +603 -0
- exonware/xwnode/strategies/performance_monitor.py +487 -0
- exonware/xwnode/strategies/queries/__init__.py +24 -0
- exonware/xwnode/strategies/queries/base.py +236 -0
- exonware/xwnode/strategies/queries/cql.py +201 -0
- exonware/xwnode/strategies/queries/cypher.py +181 -0
- exonware/xwnode/strategies/queries/datalog.py +70 -0
- exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
- exonware/xwnode/strategies/queries/eql.py +70 -0
- exonware/xwnode/strategies/queries/flux.py +70 -0
- exonware/xwnode/strategies/queries/gql.py +70 -0
- exonware/xwnode/strategies/queries/graphql.py +240 -0
- exonware/xwnode/strategies/queries/gremlin.py +181 -0
- exonware/xwnode/strategies/queries/hiveql.py +214 -0
- exonware/xwnode/strategies/queries/hql.py +70 -0
- exonware/xwnode/strategies/queries/jmespath.py +219 -0
- exonware/xwnode/strategies/queries/jq.py +66 -0
- exonware/xwnode/strategies/queries/json_query.py +66 -0
- exonware/xwnode/strategies/queries/jsoniq.py +248 -0
- exonware/xwnode/strategies/queries/kql.py +70 -0
- exonware/xwnode/strategies/queries/linq.py +238 -0
- exonware/xwnode/strategies/queries/logql.py +70 -0
- exonware/xwnode/strategies/queries/mql.py +68 -0
- exonware/xwnode/strategies/queries/n1ql.py +210 -0
- exonware/xwnode/strategies/queries/partiql.py +70 -0
- exonware/xwnode/strategies/queries/pig.py +215 -0
- exonware/xwnode/strategies/queries/promql.py +70 -0
- exonware/xwnode/strategies/queries/sparql.py +220 -0
- exonware/xwnode/strategies/queries/sql.py +275 -0
- exonware/xwnode/strategies/queries/xml_query.py +66 -0
- exonware/xwnode/strategies/queries/xpath.py +223 -0
- exonware/xwnode/strategies/queries/xquery.py +258 -0
- exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
- exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
- exonware/xwnode/strategies/registry.py +604 -0
- exonware/xwnode/strategies/simple.py +273 -0
- exonware/xwnode/strategies/utils.py +532 -0
- exonware/xwnode/types.py +912 -0
- exonware/xwnode/version.py +78 -0
- exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
- exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
- exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
- exonware_xwnode-0.0.1.12.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,574 @@
|
|
1
|
+
"""
|
2
|
+
Octree Edge Strategy Implementation
|
3
|
+
|
4
|
+
This module implements the OCTREE strategy for 3D spatial
|
5
|
+
graph partitioning and efficient 3D spatial queries.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Iterator, List, Dict, Set, Optional, Tuple
|
9
|
+
from collections import defaultdict
|
10
|
+
import math
|
11
|
+
from ._base_edge import aEdgeStrategy
|
12
|
+
from ...types import EdgeMode, EdgeTrait
|
13
|
+
|
14
|
+
|
15
|
+
class OctreeNode:
|
16
|
+
"""Node in the octree (3D spatial partitioning)."""
|
17
|
+
|
18
|
+
def __init__(self, x: float, y: float, z: float, size: float, capacity: int = 8):
|
19
|
+
self.x = x # Center coordinates
|
20
|
+
self.y = y
|
21
|
+
self.z = z
|
22
|
+
self.size = size # Half-size of the cube
|
23
|
+
self.capacity = capacity
|
24
|
+
|
25
|
+
# Points stored in this node
|
26
|
+
self.points: List[Tuple[float, float, float, str]] = [] # (x, y, z, vertex_id)
|
27
|
+
|
28
|
+
# Child nodes (8 octants)
|
29
|
+
self.children: List[Optional['OctreeNode']] = [None] * 8
|
30
|
+
self.is_leaf = True
|
31
|
+
|
32
|
+
def contains_point(self, x: float, y: float, z: float) -> bool:
|
33
|
+
"""Check if point is within this node's bounds."""
|
34
|
+
return (self.x - self.size <= x < self.x + self.size and
|
35
|
+
self.y - self.size <= y < self.y + self.size and
|
36
|
+
self.z - self.size <= z < self.z + self.size)
|
37
|
+
|
38
|
+
def intersects_box(self, box_x: float, box_y: float, box_z: float,
|
39
|
+
box_w: float, box_h: float, box_d: float) -> bool:
|
40
|
+
"""Check if this node intersects with given box."""
|
41
|
+
return not (box_x >= self.x + self.size or
|
42
|
+
box_x + box_w <= self.x - self.size or
|
43
|
+
box_y >= self.y + self.size or
|
44
|
+
box_y + box_h <= self.y - self.size or
|
45
|
+
box_z >= self.z + self.size or
|
46
|
+
box_z + box_d <= self.z - self.size)
|
47
|
+
|
48
|
+
def intersects_sphere(self, center_x: float, center_y: float, center_z: float, radius: float) -> bool:
|
49
|
+
"""Check if this node intersects with given sphere."""
|
50
|
+
# Find closest point on cube to sphere center
|
51
|
+
closest_x = max(self.x - self.size, min(center_x, self.x + self.size))
|
52
|
+
closest_y = max(self.y - self.size, min(center_y, self.y + self.size))
|
53
|
+
closest_z = max(self.z - self.size, min(center_z, self.z + self.size))
|
54
|
+
|
55
|
+
# Calculate distance from sphere center to closest point
|
56
|
+
dx = center_x - closest_x
|
57
|
+
dy = center_y - closest_y
|
58
|
+
dz = center_z - closest_z
|
59
|
+
distance_sq = dx * dx + dy * dy + dz * dz
|
60
|
+
|
61
|
+
return distance_sq <= radius * radius
|
62
|
+
|
63
|
+
def subdivide(self) -> None:
|
64
|
+
"""Subdivide this node into eight children."""
|
65
|
+
if not self.is_leaf:
|
66
|
+
return
|
67
|
+
|
68
|
+
half_size = self.size / 2
|
69
|
+
quarter_size = half_size / 2
|
70
|
+
|
71
|
+
# Create eight children (octants)
|
72
|
+
octants = [
|
73
|
+
(self.x - quarter_size, self.y - quarter_size, self.z - quarter_size), # 0: ---
|
74
|
+
(self.x + quarter_size, self.y - quarter_size, self.z - quarter_size), # 1: +--
|
75
|
+
(self.x - quarter_size, self.y + quarter_size, self.z - quarter_size), # 2: -+-
|
76
|
+
(self.x + quarter_size, self.y + quarter_size, self.z - quarter_size), # 3: ++-
|
77
|
+
(self.x - quarter_size, self.y - quarter_size, self.z + quarter_size), # 4: --+
|
78
|
+
(self.x + quarter_size, self.y - quarter_size, self.z + quarter_size), # 5: +-+
|
79
|
+
(self.x - quarter_size, self.y + quarter_size, self.z + quarter_size), # 6: -++
|
80
|
+
(self.x + quarter_size, self.y + quarter_size, self.z + quarter_size), # 7: +++
|
81
|
+
]
|
82
|
+
|
83
|
+
for i, (cx, cy, cz) in enumerate(octants):
|
84
|
+
self.children[i] = OctreeNode(cx, cy, cz, quarter_size, self.capacity)
|
85
|
+
|
86
|
+
self.is_leaf = False
|
87
|
+
|
88
|
+
# Redistribute points to children
|
89
|
+
for point in self.points:
|
90
|
+
x, y, z, vertex_id = point
|
91
|
+
for child in self.children:
|
92
|
+
if child and child.contains_point(x, y, z):
|
93
|
+
child.insert(x, y, z, vertex_id)
|
94
|
+
break
|
95
|
+
|
96
|
+
self.points.clear()
|
97
|
+
|
98
|
+
def insert(self, x: float, y: float, z: float, vertex_id: str) -> bool:
|
99
|
+
"""Insert point into octree."""
|
100
|
+
if not self.contains_point(x, y, z):
|
101
|
+
return False
|
102
|
+
|
103
|
+
if self.is_leaf:
|
104
|
+
self.points.append((x, y, z, vertex_id))
|
105
|
+
|
106
|
+
# Subdivide if capacity exceeded
|
107
|
+
if len(self.points) > self.capacity:
|
108
|
+
self.subdivide()
|
109
|
+
|
110
|
+
return True
|
111
|
+
else:
|
112
|
+
# Insert into appropriate child
|
113
|
+
for child in self.children:
|
114
|
+
if child and child.insert(x, y, z, vertex_id):
|
115
|
+
return True
|
116
|
+
return False
|
117
|
+
|
118
|
+
def query_box(self, box_x: float, box_y: float, box_z: float,
|
119
|
+
box_w: float, box_h: float, box_d: float) -> List[Tuple[float, float, float, str]]:
|
120
|
+
"""Query points within given box."""
|
121
|
+
result = []
|
122
|
+
|
123
|
+
if not self.intersects_box(box_x, box_y, box_z, box_w, box_h, box_d):
|
124
|
+
return result
|
125
|
+
|
126
|
+
if self.is_leaf:
|
127
|
+
for x, y, z, vertex_id in self.points:
|
128
|
+
if (box_x <= x < box_x + box_w and
|
129
|
+
box_y <= y < box_y + box_h and
|
130
|
+
box_z <= z < box_z + box_d):
|
131
|
+
result.append((x, y, z, vertex_id))
|
132
|
+
else:
|
133
|
+
for child in self.children:
|
134
|
+
if child:
|
135
|
+
result.extend(child.query_box(box_x, box_y, box_z, box_w, box_h, box_d))
|
136
|
+
|
137
|
+
return result
|
138
|
+
|
139
|
+
def query_sphere(self, center_x: float, center_y: float, center_z: float, radius: float) -> List[Tuple[float, float, float, str]]:
|
140
|
+
"""Query points within given sphere."""
|
141
|
+
if not self.intersects_sphere(center_x, center_y, center_z, radius):
|
142
|
+
return []
|
143
|
+
|
144
|
+
result = []
|
145
|
+
radius_sq = radius * radius
|
146
|
+
|
147
|
+
if self.is_leaf:
|
148
|
+
for x, y, z, vertex_id in self.points:
|
149
|
+
dx = x - center_x
|
150
|
+
dy = y - center_y
|
151
|
+
dz = z - center_z
|
152
|
+
dist_sq = dx * dx + dy * dy + dz * dz
|
153
|
+
|
154
|
+
if dist_sq <= radius_sq:
|
155
|
+
result.append((x, y, z, vertex_id))
|
156
|
+
else:
|
157
|
+
for child in self.children:
|
158
|
+
if child:
|
159
|
+
result.extend(child.query_sphere(center_x, center_y, center_z, radius))
|
160
|
+
|
161
|
+
return result
|
162
|
+
|
163
|
+
def remove(self, x: float, y: float, z: float, vertex_id: str) -> bool:
|
164
|
+
"""Remove point from octree."""
|
165
|
+
if not self.contains_point(x, y, z):
|
166
|
+
return False
|
167
|
+
|
168
|
+
if self.is_leaf:
|
169
|
+
for i, point in enumerate(self.points):
|
170
|
+
if (point[0] == x and point[1] == y and
|
171
|
+
point[2] == z and point[3] == vertex_id):
|
172
|
+
del self.points[i]
|
173
|
+
return True
|
174
|
+
return False
|
175
|
+
else:
|
176
|
+
for child in self.children:
|
177
|
+
if child and child.remove(x, y, z, vertex_id):
|
178
|
+
return True
|
179
|
+
return False
|
180
|
+
|
181
|
+
|
182
|
+
class xOctreeStrategy(aEdgeStrategy):
|
183
|
+
"""
|
184
|
+
Octree edge strategy for 3D spatial graphs.
|
185
|
+
|
186
|
+
Efficiently manages 3D spatial relationships with logarithmic
|
187
|
+
complexity for 3D spatial queries and nearest neighbor searches.
|
188
|
+
"""
|
189
|
+
|
190
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
191
|
+
"""Initialize the Octree strategy."""
|
192
|
+
super().__init__(EdgeMode.OCTREE, traits, **options)
|
193
|
+
|
194
|
+
# 3D spatial bounds
|
195
|
+
self.center_x = options.get('center_x', 0.0)
|
196
|
+
self.center_y = options.get('center_y', 0.0)
|
197
|
+
self.center_z = options.get('center_z', 0.0)
|
198
|
+
self.size = options.get('size', 500.0) # Half-size of the root cube
|
199
|
+
self.capacity = options.get('capacity', 8)
|
200
|
+
|
201
|
+
# Core octree
|
202
|
+
self._root = OctreeNode(self.center_x, self.center_y, self.center_z, self.size, self.capacity)
|
203
|
+
|
204
|
+
# Vertex management
|
205
|
+
self._vertices: Dict[str, Tuple[float, float, float]] = {} # vertex_id -> (x, y, z)
|
206
|
+
self._edges: Dict[Tuple[str, str], Dict[str, Any]] = {} # (source, target) -> properties
|
207
|
+
self._spatial_edges: Set[Tuple[str, str]] = set() # Edges based on spatial proximity
|
208
|
+
|
209
|
+
# Performance tracking
|
210
|
+
self._edge_count = 0
|
211
|
+
self._spatial_threshold = options.get('spatial_threshold', 50.0) # Auto-connect distance
|
212
|
+
|
213
|
+
def get_supported_traits(self) -> EdgeTrait:
|
214
|
+
"""Get the traits supported by the octree strategy."""
|
215
|
+
return (EdgeTrait.SPATIAL | EdgeTrait.SPARSE | EdgeTrait.CACHE_FRIENDLY)
|
216
|
+
|
217
|
+
def _auto_connect_spatial(self, vertex: str, x: float, y: float, z: float) -> None:
|
218
|
+
"""Automatically connect vertex to nearby vertices."""
|
219
|
+
if self._spatial_threshold <= 0:
|
220
|
+
return
|
221
|
+
|
222
|
+
# Find nearby vertices
|
223
|
+
nearby = self._root.query_sphere(x, y, z, self._spatial_threshold)
|
224
|
+
|
225
|
+
for nx, ny, nz, neighbor_id in nearby:
|
226
|
+
if neighbor_id != vertex:
|
227
|
+
# Calculate 3D distance
|
228
|
+
distance = math.sqrt((x - nx) ** 2 + (y - ny) ** 2 + (z - nz) ** 2)
|
229
|
+
|
230
|
+
# Add spatial edge
|
231
|
+
edge_key = (min(vertex, neighbor_id), max(vertex, neighbor_id))
|
232
|
+
if edge_key not in self._edges:
|
233
|
+
self._edges[edge_key] = {
|
234
|
+
'distance': distance,
|
235
|
+
'spatial': True,
|
236
|
+
'weight': 1.0 / (1.0 + distance), # Inverse distance weight
|
237
|
+
'dimension': '3D'
|
238
|
+
}
|
239
|
+
self._spatial_edges.add(edge_key)
|
240
|
+
self._edge_count += 1
|
241
|
+
|
242
|
+
# ============================================================================
|
243
|
+
# CORE EDGE OPERATIONS
|
244
|
+
# ============================================================================
|
245
|
+
|
246
|
+
def add_edge(self, source: str, target: str, **properties) -> str:
|
247
|
+
"""Add edge between 3D spatial vertices."""
|
248
|
+
# Ensure vertices exist with positions
|
249
|
+
if source not in self._vertices:
|
250
|
+
x = properties.get('source_x', 0.0)
|
251
|
+
y = properties.get('source_y', 0.0)
|
252
|
+
z = properties.get('source_z', 0.0)
|
253
|
+
self.add_spatial_vertex(source, x, y, z)
|
254
|
+
|
255
|
+
if target not in self._vertices:
|
256
|
+
x = properties.get('target_x', 0.0)
|
257
|
+
y = properties.get('target_y', 0.0)
|
258
|
+
z = properties.get('target_z', 0.0)
|
259
|
+
self.add_spatial_vertex(target, x, y, z)
|
260
|
+
|
261
|
+
# Calculate 3D distance
|
262
|
+
sx, sy, sz = self._vertices[source]
|
263
|
+
tx, ty, tz = self._vertices[target]
|
264
|
+
distance = math.sqrt((sx - tx) ** 2 + (sy - ty) ** 2 + (sz - tz) ** 2)
|
265
|
+
|
266
|
+
# Add edge
|
267
|
+
edge_key = (min(source, target), max(source, target))
|
268
|
+
self._edges[edge_key] = {
|
269
|
+
'distance': distance,
|
270
|
+
'spatial': properties.get('spatial', False),
|
271
|
+
'weight': properties.get('weight', 1.0),
|
272
|
+
'dimension': '3D',
|
273
|
+
**properties
|
274
|
+
}
|
275
|
+
|
276
|
+
if edge_key not in self._spatial_edges:
|
277
|
+
self._edge_count += 1
|
278
|
+
|
279
|
+
return f"{source}<->{target}"
|
280
|
+
|
281
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
282
|
+
"""Remove edge between vertices."""
|
283
|
+
edge_key = (min(source, target), max(source, target))
|
284
|
+
|
285
|
+
if edge_key in self._edges:
|
286
|
+
del self._edges[edge_key]
|
287
|
+
self._spatial_edges.discard(edge_key)
|
288
|
+
self._edge_count -= 1
|
289
|
+
return True
|
290
|
+
|
291
|
+
return False
|
292
|
+
|
293
|
+
def has_edge(self, source: str, target: str) -> bool:
|
294
|
+
"""Check if edge exists."""
|
295
|
+
edge_key = (min(source, target), max(source, target))
|
296
|
+
return edge_key in self._edges
|
297
|
+
|
298
|
+
def get_edge_data(self, source: str, target: str) -> Optional[Dict[str, Any]]:
|
299
|
+
"""Get edge data."""
|
300
|
+
edge_key = (min(source, target), max(source, target))
|
301
|
+
return self._edges.get(edge_key)
|
302
|
+
|
303
|
+
def neighbors(self, vertex: str, direction: str = 'both') -> Iterator[str]:
|
304
|
+
"""Get neighbors of vertex."""
|
305
|
+
for (v1, v2) in self._edges:
|
306
|
+
if v1 == vertex:
|
307
|
+
yield v2
|
308
|
+
elif v2 == vertex:
|
309
|
+
yield v1
|
310
|
+
|
311
|
+
def degree(self, vertex: str, direction: str = 'both') -> int:
|
312
|
+
"""Get degree of vertex."""
|
313
|
+
return len(list(self.neighbors(vertex, direction)))
|
314
|
+
|
315
|
+
def edges(self, data: bool = False) -> Iterator[tuple]:
|
316
|
+
"""Get all edges."""
|
317
|
+
for (source, target), edge_data in self._edges.items():
|
318
|
+
if data:
|
319
|
+
yield (source, target, edge_data)
|
320
|
+
else:
|
321
|
+
yield (source, target)
|
322
|
+
|
323
|
+
def vertices(self) -> Iterator[str]:
|
324
|
+
"""Get all vertices."""
|
325
|
+
return iter(self._vertices.keys())
|
326
|
+
|
327
|
+
def __len__(self) -> int:
|
328
|
+
"""Get number of edges."""
|
329
|
+
return self._edge_count
|
330
|
+
|
331
|
+
def vertex_count(self) -> int:
|
332
|
+
"""Get number of vertices."""
|
333
|
+
return len(self._vertices)
|
334
|
+
|
335
|
+
def clear(self) -> None:
|
336
|
+
"""Clear all data."""
|
337
|
+
self._root = OctreeNode(self.center_x, self.center_y, self.center_z, self.size, self.capacity)
|
338
|
+
self._vertices.clear()
|
339
|
+
self._edges.clear()
|
340
|
+
self._spatial_edges.clear()
|
341
|
+
self._edge_count = 0
|
342
|
+
|
343
|
+
def add_vertex(self, vertex: str) -> None:
|
344
|
+
"""Add vertex at random 3D position."""
|
345
|
+
if vertex not in self._vertices:
|
346
|
+
# Random position within bounds
|
347
|
+
import random
|
348
|
+
x = self.center_x + (random.random() - 0.5) * 2 * self.size
|
349
|
+
y = self.center_y + (random.random() - 0.5) * 2 * self.size
|
350
|
+
z = self.center_z + (random.random() - 0.5) * 2 * self.size
|
351
|
+
self.add_spatial_vertex(vertex, x, y, z)
|
352
|
+
|
353
|
+
def remove_vertex(self, vertex: str) -> bool:
|
354
|
+
"""Remove vertex and all its edges."""
|
355
|
+
if vertex not in self._vertices:
|
356
|
+
return False
|
357
|
+
|
358
|
+
# Remove from octree
|
359
|
+
x, y, z = self._vertices[vertex]
|
360
|
+
self._root.remove(x, y, z, vertex)
|
361
|
+
|
362
|
+
# Remove all edges
|
363
|
+
edges_to_remove = []
|
364
|
+
for (v1, v2) in self._edges:
|
365
|
+
if v1 == vertex or v2 == vertex:
|
366
|
+
edges_to_remove.append((v1, v2))
|
367
|
+
|
368
|
+
for edge_key in edges_to_remove:
|
369
|
+
del self._edges[edge_key]
|
370
|
+
self._spatial_edges.discard(edge_key)
|
371
|
+
self._edge_count -= 1
|
372
|
+
|
373
|
+
# Remove vertex
|
374
|
+
del self._vertices[vertex]
|
375
|
+
return True
|
376
|
+
|
377
|
+
# ============================================================================
|
378
|
+
# 3D SPATIAL OPERATIONS
|
379
|
+
# ============================================================================
|
380
|
+
|
381
|
+
def add_spatial_vertex(self, vertex: str, x: float, y: float, z: float) -> None:
|
382
|
+
"""Add vertex at specific 3D spatial position."""
|
383
|
+
# Remove old position if exists
|
384
|
+
if vertex in self._vertices:
|
385
|
+
old_x, old_y, old_z = self._vertices[vertex]
|
386
|
+
self._root.remove(old_x, old_y, old_z, vertex)
|
387
|
+
|
388
|
+
# Add to octree
|
389
|
+
self._vertices[vertex] = (x, y, z)
|
390
|
+
self._root.insert(x, y, z, vertex)
|
391
|
+
|
392
|
+
# Auto-connect to nearby vertices
|
393
|
+
self._auto_connect_spatial(vertex, x, y, z)
|
394
|
+
|
395
|
+
def get_vertex_position(self, vertex: str) -> Optional[Tuple[float, float, float]]:
|
396
|
+
"""Get vertex 3D position."""
|
397
|
+
return self._vertices.get(vertex)
|
398
|
+
|
399
|
+
def set_vertex_position(self, vertex: str, x: float, y: float, z: float) -> None:
|
400
|
+
"""Update vertex 3D position."""
|
401
|
+
self.add_spatial_vertex(vertex, x, y, z)
|
402
|
+
|
403
|
+
def query_box(self, x: float, y: float, z: float,
|
404
|
+
width: float, height: float, depth: float) -> List[str]:
|
405
|
+
"""Query vertices within 3D box."""
|
406
|
+
points = self._root.query_box(x, y, z, width, height, depth)
|
407
|
+
return [vertex_id for _, _, _, vertex_id in points]
|
408
|
+
|
409
|
+
def query_sphere(self, center_x: float, center_y: float, center_z: float, radius: float) -> List[str]:
|
410
|
+
"""Query vertices within 3D sphere."""
|
411
|
+
points = self._root.query_sphere(center_x, center_y, center_z, radius)
|
412
|
+
return [vertex_id for _, _, _, vertex_id in points]
|
413
|
+
|
414
|
+
def nearest_neighbors_3d(self, vertex: str, k: int = 1) -> List[Tuple[str, float]]:
|
415
|
+
"""Find k nearest neighbors to vertex in 3D space."""
|
416
|
+
if vertex not in self._vertices:
|
417
|
+
return []
|
418
|
+
|
419
|
+
x, y, z = self._vertices[vertex]
|
420
|
+
|
421
|
+
# Query expanding radius until we have enough candidates
|
422
|
+
radius = 10.0
|
423
|
+
candidates = []
|
424
|
+
|
425
|
+
while len(candidates) < k * 2 and radius <= self.size * 2:
|
426
|
+
candidates = self.query_sphere(x, y, z, radius)
|
427
|
+
candidates = [v for v in candidates if v != vertex]
|
428
|
+
radius *= 2
|
429
|
+
|
430
|
+
# Calculate 3D distances and sort
|
431
|
+
distances = []
|
432
|
+
for neighbor in candidates:
|
433
|
+
nx, ny, nz = self._vertices[neighbor]
|
434
|
+
dist = math.sqrt((x - nx) ** 2 + (y - ny) ** 2 + (z - nz) ** 2)
|
435
|
+
distances.append((neighbor, dist))
|
436
|
+
|
437
|
+
distances.sort(key=lambda x: x[1])
|
438
|
+
return distances[:k]
|
439
|
+
|
440
|
+
def get_spatial_edges_in_box(self, x: float, y: float, z: float,
|
441
|
+
width: float, height: float, depth: float) -> List[Tuple[str, str]]:
|
442
|
+
"""Get edges where both vertices are in given 3D box."""
|
443
|
+
vertices_in_box = set(self.query_box(x, y, z, width, height, depth))
|
444
|
+
|
445
|
+
spatial_edges = []
|
446
|
+
for (v1, v2) in self._edges:
|
447
|
+
if v1 in vertices_in_box and v2 in vertices_in_box:
|
448
|
+
spatial_edges.append((v1, v2))
|
449
|
+
|
450
|
+
return spatial_edges
|
451
|
+
|
452
|
+
def cluster_vertices_3d(self, max_distance: float) -> List[List[str]]:
|
453
|
+
"""Cluster vertices based on 3D spatial proximity."""
|
454
|
+
visited = set()
|
455
|
+
clusters = []
|
456
|
+
|
457
|
+
for vertex in self._vertices:
|
458
|
+
if vertex in visited:
|
459
|
+
continue
|
460
|
+
|
461
|
+
# Start new cluster
|
462
|
+
cluster = []
|
463
|
+
stack = [vertex]
|
464
|
+
|
465
|
+
while stack:
|
466
|
+
current = stack.pop()
|
467
|
+
if current in visited:
|
468
|
+
continue
|
469
|
+
|
470
|
+
visited.add(current)
|
471
|
+
cluster.append(current)
|
472
|
+
|
473
|
+
# Find nearby vertices in 3D
|
474
|
+
x, y, z = self._vertices[current]
|
475
|
+
nearby = self.query_sphere(x, y, z, max_distance)
|
476
|
+
|
477
|
+
for neighbor in nearby:
|
478
|
+
if neighbor not in visited:
|
479
|
+
stack.append(neighbor)
|
480
|
+
|
481
|
+
if cluster:
|
482
|
+
clusters.append(cluster)
|
483
|
+
|
484
|
+
return clusters
|
485
|
+
|
486
|
+
def get_bounding_box(self) -> Tuple[float, float, float, float, float, float]:
|
487
|
+
"""Get 3D bounding box of all vertices."""
|
488
|
+
if not self._vertices:
|
489
|
+
return (0, 0, 0, 0, 0, 0)
|
490
|
+
|
491
|
+
positions = list(self._vertices.values())
|
492
|
+
min_x = min(pos[0] for pos in positions)
|
493
|
+
max_x = max(pos[0] for pos in positions)
|
494
|
+
min_y = min(pos[1] for pos in positions)
|
495
|
+
max_y = max(pos[1] for pos in positions)
|
496
|
+
min_z = min(pos[2] for pos in positions)
|
497
|
+
max_z = max(pos[2] for pos in positions)
|
498
|
+
|
499
|
+
return (min_x, min_y, min_z, max_x, max_y, max_z)
|
500
|
+
|
501
|
+
def get_3d_statistics(self) -> Dict[str, Any]:
|
502
|
+
"""Get comprehensive 3D spatial statistics."""
|
503
|
+
if not self._vertices:
|
504
|
+
return {'vertices': 0, 'edges': 0, 'volume': 0}
|
505
|
+
|
506
|
+
# Calculate 3D extent
|
507
|
+
min_x, min_y, min_z, max_x, max_y, max_z = self.get_bounding_box()
|
508
|
+
|
509
|
+
volume = (max_x - min_x) * (max_y - min_y) * (max_z - min_z) if len(self._vertices) > 1 else 0
|
510
|
+
vertex_density = len(self._vertices) / max(1, volume)
|
511
|
+
|
512
|
+
# Edge length statistics (3D distances)
|
513
|
+
edge_lengths = []
|
514
|
+
for (v1, v2), edge_data in self._edges.items():
|
515
|
+
edge_lengths.append(edge_data.get('distance', 0))
|
516
|
+
|
517
|
+
avg_edge_length = sum(edge_lengths) / len(edge_lengths) if edge_lengths else 0
|
518
|
+
max_edge_length = max(edge_lengths) if edge_lengths else 0
|
519
|
+
min_edge_length = min(edge_lengths) if edge_lengths else 0
|
520
|
+
|
521
|
+
return {
|
522
|
+
'vertices': len(self._vertices),
|
523
|
+
'edges': self._edge_count,
|
524
|
+
'spatial_edges': len(self._spatial_edges),
|
525
|
+
'bounding_box': (min_x, min_y, min_z, max_x, max_y, max_z),
|
526
|
+
'volume': volume,
|
527
|
+
'vertex_density': vertex_density,
|
528
|
+
'avg_edge_length': avg_edge_length,
|
529
|
+
'min_edge_length': min_edge_length,
|
530
|
+
'max_edge_length': max_edge_length,
|
531
|
+
'spatial_threshold': self._spatial_threshold,
|
532
|
+
'octree_capacity': self.capacity,
|
533
|
+
'octree_size': self.size * 2 # Full cube size
|
534
|
+
}
|
535
|
+
|
536
|
+
# ============================================================================
|
537
|
+
# PERFORMANCE CHARACTERISTICS
|
538
|
+
# ============================================================================
|
539
|
+
|
540
|
+
@property
|
541
|
+
def backend_info(self) -> Dict[str, Any]:
|
542
|
+
"""Get backend implementation info."""
|
543
|
+
return {
|
544
|
+
'strategy': 'OCTREE',
|
545
|
+
'backend': '3D spatial partitioning with octree',
|
546
|
+
'center': f"({self.center_x}, {self.center_y}, {self.center_z})",
|
547
|
+
'size': self.size * 2, # Full cube size
|
548
|
+
'capacity': self.capacity,
|
549
|
+
'spatial_threshold': self._spatial_threshold,
|
550
|
+
'complexity': {
|
551
|
+
'insert': 'O(log n)',
|
552
|
+
'remove': 'O(log n)',
|
553
|
+
'box_query': 'O(log n + k)', # k = results
|
554
|
+
'sphere_query': 'O(log n + k)',
|
555
|
+
'nearest_neighbor': 'O(log n + k)',
|
556
|
+
'space': 'O(n)'
|
557
|
+
}
|
558
|
+
}
|
559
|
+
|
560
|
+
@property
|
561
|
+
def metrics(self) -> Dict[str, Any]:
|
562
|
+
"""Get performance metrics."""
|
563
|
+
stats = self.get_3d_statistics()
|
564
|
+
|
565
|
+
return {
|
566
|
+
'vertices': stats['vertices'],
|
567
|
+
'edges': stats['edges'],
|
568
|
+
'spatial_edges': stats['spatial_edges'],
|
569
|
+
'vertex_density': f"{stats['vertex_density']:.3f}",
|
570
|
+
'avg_edge_length': f"{stats['avg_edge_length']:.1f}",
|
571
|
+
'volume': f"{stats['volume']:.1f}",
|
572
|
+
'octree_size': f"{stats['octree_size']:.1f}",
|
573
|
+
'memory_usage': f"{self._edge_count * 100 + len(self._vertices) * 120} bytes (estimated)"
|
574
|
+
}
|