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.
Files changed (132) hide show
  1. exonware/__init__.py +14 -0
  2. exonware/xwnode/__init__.py +127 -0
  3. exonware/xwnode/base.py +676 -0
  4. exonware/xwnode/config.py +178 -0
  5. exonware/xwnode/contracts.py +730 -0
  6. exonware/xwnode/errors.py +503 -0
  7. exonware/xwnode/facade.py +460 -0
  8. exonware/xwnode/strategies/__init__.py +158 -0
  9. exonware/xwnode/strategies/advisor.py +463 -0
  10. exonware/xwnode/strategies/edges/__init__.py +32 -0
  11. exonware/xwnode/strategies/edges/adj_list.py +227 -0
  12. exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
  13. exonware/xwnode/strategies/edges/base.py +169 -0
  14. exonware/xwnode/strategies/flyweight.py +328 -0
  15. exonware/xwnode/strategies/impls/__init__.py +13 -0
  16. exonware/xwnode/strategies/impls/_base_edge.py +403 -0
  17. exonware/xwnode/strategies/impls/_base_node.py +307 -0
  18. exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
  19. exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
  20. exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
  21. exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
  22. exonware/xwnode/strategies/impls/edge_coo.py +533 -0
  23. exonware/xwnode/strategies/impls/edge_csc.py +447 -0
  24. exonware/xwnode/strategies/impls/edge_csr.py +492 -0
  25. exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
  26. exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
  27. exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
  28. exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
  29. exonware/xwnode/strategies/impls/edge_octree.py +574 -0
  30. exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
  31. exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
  32. exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
  33. exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
  34. exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
  35. exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
  36. exonware/xwnode/strategies/manager.py +775 -0
  37. exonware/xwnode/strategies/metrics.py +538 -0
  38. exonware/xwnode/strategies/migration.py +432 -0
  39. exonware/xwnode/strategies/nodes/__init__.py +50 -0
  40. exonware/xwnode/strategies/nodes/_base_node.py +307 -0
  41. exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
  42. exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
  43. exonware/xwnode/strategies/nodes/array_list.py +209 -0
  44. exonware/xwnode/strategies/nodes/base.py +247 -0
  45. exonware/xwnode/strategies/nodes/deque.py +200 -0
  46. exonware/xwnode/strategies/nodes/hash_map.py +135 -0
  47. exonware/xwnode/strategies/nodes/heap.py +307 -0
  48. exonware/xwnode/strategies/nodes/linked_list.py +232 -0
  49. exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
  50. exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
  51. exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
  52. exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
  53. exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
  54. exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
  55. exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
  56. exonware/xwnode/strategies/nodes/node_btree.py +357 -0
  57. exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
  58. exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
  59. exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
  60. exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
  61. exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
  62. exonware/xwnode/strategies/nodes/node_heap.py +191 -0
  63. exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
  64. exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
  65. exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
  66. exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
  67. exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
  68. exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
  69. exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
  70. exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
  71. exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
  72. exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
  73. exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
  74. exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
  75. exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
  76. exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
  77. exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
  78. exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
  79. exonware/xwnode/strategies/nodes/node_treap.py +387 -0
  80. exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
  81. exonware/xwnode/strategies/nodes/node_trie.py +252 -0
  82. exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
  83. exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
  84. exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
  85. exonware/xwnode/strategies/nodes/queue.py +161 -0
  86. exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
  87. exonware/xwnode/strategies/nodes/stack.py +152 -0
  88. exonware/xwnode/strategies/nodes/trie.py +274 -0
  89. exonware/xwnode/strategies/nodes/union_find.py +283 -0
  90. exonware/xwnode/strategies/pattern_detector.py +603 -0
  91. exonware/xwnode/strategies/performance_monitor.py +487 -0
  92. exonware/xwnode/strategies/queries/__init__.py +24 -0
  93. exonware/xwnode/strategies/queries/base.py +236 -0
  94. exonware/xwnode/strategies/queries/cql.py +201 -0
  95. exonware/xwnode/strategies/queries/cypher.py +181 -0
  96. exonware/xwnode/strategies/queries/datalog.py +70 -0
  97. exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
  98. exonware/xwnode/strategies/queries/eql.py +70 -0
  99. exonware/xwnode/strategies/queries/flux.py +70 -0
  100. exonware/xwnode/strategies/queries/gql.py +70 -0
  101. exonware/xwnode/strategies/queries/graphql.py +240 -0
  102. exonware/xwnode/strategies/queries/gremlin.py +181 -0
  103. exonware/xwnode/strategies/queries/hiveql.py +214 -0
  104. exonware/xwnode/strategies/queries/hql.py +70 -0
  105. exonware/xwnode/strategies/queries/jmespath.py +219 -0
  106. exonware/xwnode/strategies/queries/jq.py +66 -0
  107. exonware/xwnode/strategies/queries/json_query.py +66 -0
  108. exonware/xwnode/strategies/queries/jsoniq.py +248 -0
  109. exonware/xwnode/strategies/queries/kql.py +70 -0
  110. exonware/xwnode/strategies/queries/linq.py +238 -0
  111. exonware/xwnode/strategies/queries/logql.py +70 -0
  112. exonware/xwnode/strategies/queries/mql.py +68 -0
  113. exonware/xwnode/strategies/queries/n1ql.py +210 -0
  114. exonware/xwnode/strategies/queries/partiql.py +70 -0
  115. exonware/xwnode/strategies/queries/pig.py +215 -0
  116. exonware/xwnode/strategies/queries/promql.py +70 -0
  117. exonware/xwnode/strategies/queries/sparql.py +220 -0
  118. exonware/xwnode/strategies/queries/sql.py +275 -0
  119. exonware/xwnode/strategies/queries/xml_query.py +66 -0
  120. exonware/xwnode/strategies/queries/xpath.py +223 -0
  121. exonware/xwnode/strategies/queries/xquery.py +258 -0
  122. exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
  123. exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
  124. exonware/xwnode/strategies/registry.py +604 -0
  125. exonware/xwnode/strategies/simple.py +273 -0
  126. exonware/xwnode/strategies/utils.py +532 -0
  127. exonware/xwnode/types.py +912 -0
  128. exonware/xwnode/version.py +78 -0
  129. exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
  130. exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
  131. exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
  132. exonware_xwnode-0.0.1.12.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,820 @@
1
+ """
2
+ R-Tree Edge Strategy Implementation
3
+
4
+ This module implements the R_TREE strategy for spatial indexing of edges
5
+ with geometric coordinates and efficient spatial queries.
6
+ """
7
+
8
+ from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, NamedTuple
9
+ from collections import defaultdict
10
+ import math
11
+ from ._base_edge import aEdgeStrategy
12
+ from ...types import EdgeMode, EdgeTrait
13
+
14
+
15
+ class Rectangle(NamedTuple):
16
+ """Represents a bounding rectangle."""
17
+ min_x: float
18
+ min_y: float
19
+ max_x: float
20
+ max_y: float
21
+
22
+ def area(self) -> float:
23
+ """Calculate rectangle area."""
24
+ return max(0, self.max_x - self.min_x) * max(0, self.max_y - self.min_y)
25
+
26
+ def perimeter(self) -> float:
27
+ """Calculate rectangle perimeter."""
28
+ return 2 * (max(0, self.max_x - self.min_x) + max(0, self.max_y - self.min_y))
29
+
30
+ def contains_point(self, x: float, y: float) -> bool:
31
+ """Check if rectangle contains point."""
32
+ return self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y
33
+
34
+ def intersects(self, other: 'Rectangle') -> bool:
35
+ """Check if rectangle intersects with another."""
36
+ return not (other.max_x < self.min_x or other.min_x > self.max_x or
37
+ other.max_y < self.min_y or other.min_y > self.max_y)
38
+
39
+ def contains_rectangle(self, other: 'Rectangle') -> bool:
40
+ """Check if this rectangle contains another."""
41
+ return (self.min_x <= other.min_x and self.max_x >= other.max_x and
42
+ self.min_y <= other.min_y and self.max_y >= other.max_y)
43
+
44
+ def union(self, other: 'Rectangle') -> 'Rectangle':
45
+ """Get bounding rectangle of union."""
46
+ return Rectangle(
47
+ min(self.min_x, other.min_x),
48
+ min(self.min_y, other.min_y),
49
+ max(self.max_x, other.max_x),
50
+ max(self.max_y, other.max_y)
51
+ )
52
+
53
+ def enlargement_needed(self, other: 'Rectangle') -> float:
54
+ """Calculate area enlargement needed to include another rectangle."""
55
+ union_rect = self.union(other)
56
+ return union_rect.area() - self.area()
57
+
58
+
59
+ class SpatialEdge:
60
+ """Represents an edge with spatial coordinates."""
61
+
62
+ def __init__(self, edge_id: str, source: str, target: str,
63
+ source_coords: Tuple[float, float],
64
+ target_coords: Tuple[float, float], **properties):
65
+ self.edge_id = edge_id
66
+ self.source = source
67
+ self.target = target
68
+ self.source_coords = source_coords
69
+ self.target_coords = target_coords
70
+ self.properties = properties.copy()
71
+
72
+ # Calculate bounding rectangle
73
+ self.bounding_rect = Rectangle(
74
+ min(source_coords[0], target_coords[0]),
75
+ min(source_coords[1], target_coords[1]),
76
+ max(source_coords[0], target_coords[0]),
77
+ max(source_coords[1], target_coords[1])
78
+ )
79
+
80
+ # Calculate edge length
81
+ dx = target_coords[0] - source_coords[0]
82
+ dy = target_coords[1] - source_coords[1]
83
+ self.length = math.sqrt(dx * dx + dy * dy)
84
+
85
+ def intersects_rectangle(self, rect: Rectangle) -> bool:
86
+ """Check if edge intersects with rectangle."""
87
+ # First check if bounding rectangles intersect
88
+ if not self.bounding_rect.intersects(rect):
89
+ return False
90
+
91
+ # Check if either endpoint is inside rectangle
92
+ if (rect.contains_point(*self.source_coords) or
93
+ rect.contains_point(*self.target_coords)):
94
+ return True
95
+
96
+ # Check line-rectangle intersection (simplified)
97
+ return self._line_intersects_rectangle(rect)
98
+
99
+ def _line_intersects_rectangle(self, rect: Rectangle) -> bool:
100
+ """Check if line segment intersects rectangle edges."""
101
+ x1, y1 = self.source_coords
102
+ x2, y2 = self.target_coords
103
+
104
+ # Check intersection with each rectangle edge
105
+ edges = [
106
+ ((rect.min_x, rect.min_y), (rect.max_x, rect.min_y)), # Bottom
107
+ ((rect.max_x, rect.min_y), (rect.max_x, rect.max_y)), # Right
108
+ ((rect.max_x, rect.max_y), (rect.min_x, rect.max_y)), # Top
109
+ ((rect.min_x, rect.max_y), (rect.min_x, rect.min_y)) # Left
110
+ ]
111
+
112
+ for (ex1, ey1), (ex2, ey2) in edges:
113
+ if self._segments_intersect((x1, y1), (x2, y2), (ex1, ey1), (ex2, ey2)):
114
+ return True
115
+
116
+ return False
117
+
118
+ def _segments_intersect(self, p1: Tuple[float, float], p2: Tuple[float, float],
119
+ p3: Tuple[float, float], p4: Tuple[float, float]) -> bool:
120
+ """Check if two line segments intersect."""
121
+ def ccw(A, B, C):
122
+ return (C[1] - A[1]) * (B[0] - A[0]) > (B[1] - A[1]) * (C[0] - A[0])
123
+
124
+ return ccw(p1, p3, p4) != ccw(p2, p3, p4) and ccw(p1, p2, p3) != ccw(p1, p2, p4)
125
+
126
+ def distance_to_point(self, x: float, y: float) -> float:
127
+ """Calculate distance from point to edge."""
128
+ x1, y1 = self.source_coords
129
+ x2, y2 = self.target_coords
130
+
131
+ # Vector from source to target
132
+ dx = x2 - x1
133
+ dy = y2 - y1
134
+
135
+ if dx == 0 and dy == 0:
136
+ # Degenerate case: source and target are same point
137
+ return math.sqrt((x - x1) ** 2 + (y - y1) ** 2)
138
+
139
+ # Parameter t for projection onto line
140
+ t = max(0, min(1, ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy)))
141
+
142
+ # Closest point on line segment
143
+ closest_x = x1 + t * dx
144
+ closest_y = y1 + t * dy
145
+
146
+ return math.sqrt((x - closest_x) ** 2 + (y - closest_y) ** 2)
147
+
148
+ def to_dict(self) -> Dict[str, Any]:
149
+ """Convert to dictionary representation."""
150
+ return {
151
+ 'id': self.edge_id,
152
+ 'source': self.source,
153
+ 'target': self.target,
154
+ 'source_coords': self.source_coords,
155
+ 'target_coords': self.target_coords,
156
+ 'length': self.length,
157
+ 'bounding_rect': {
158
+ 'min_x': self.bounding_rect.min_x,
159
+ 'min_y': self.bounding_rect.min_y,
160
+ 'max_x': self.bounding_rect.max_x,
161
+ 'max_y': self.bounding_rect.max_y
162
+ },
163
+ 'properties': self.properties
164
+ }
165
+
166
+
167
+ class RTreeNode:
168
+ """Node in the R-Tree structure."""
169
+
170
+ def __init__(self, is_leaf: bool = False, max_entries: int = 4):
171
+ self.is_leaf = is_leaf
172
+ self.max_entries = max_entries
173
+ self.entries: List[Tuple[Rectangle, Any]] = [] # (bounding_rect, child_or_edge)
174
+ self.bounding_rect: Optional[Rectangle] = None
175
+
176
+ def is_full(self) -> bool:
177
+ """Check if node is full."""
178
+ return len(self.entries) >= self.max_entries
179
+
180
+ def update_bounding_rect(self) -> None:
181
+ """Update bounding rectangle to encompass all entries."""
182
+ if not self.entries:
183
+ self.bounding_rect = None
184
+ return
185
+
186
+ min_x = min(rect.min_x for rect, _ in self.entries)
187
+ min_y = min(rect.min_y for rect, _ in self.entries)
188
+ max_x = max(rect.max_x for rect, _ in self.entries)
189
+ max_y = max(rect.max_y for rect, _ in self.entries)
190
+
191
+ self.bounding_rect = Rectangle(min_x, min_y, max_x, max_y)
192
+
193
+ def add_entry(self, rect: Rectangle, data: Any) -> None:
194
+ """Add entry to node."""
195
+ self.entries.append((rect, data))
196
+ self.update_bounding_rect()
197
+
198
+
199
+ class xRTreeStrategy(aEdgeStrategy):
200
+ """
201
+ R-Tree edge strategy for spatial indexing of edges.
202
+
203
+ Efficiently manages edges with spatial coordinates, supporting
204
+ fast spatial queries like range searches, nearest neighbor,
205
+ and geometric intersections.
206
+ """
207
+
208
+ def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
209
+ """Initialize the R-Tree strategy."""
210
+ super().__init__(EdgeMode.R_TREE, traits, **options)
211
+
212
+ self.max_entries = options.get('max_entries', 4)
213
+ self.min_entries = max(1, self.max_entries // 2)
214
+ self.is_directed = options.get('directed', True)
215
+
216
+ # Core storage
217
+ self._edges: Dict[str, SpatialEdge] = {}
218
+ self._vertex_coords: Dict[str, Tuple[float, float]] = {}
219
+ self._vertices: Set[str] = set()
220
+
221
+ # R-Tree structure
222
+ self._root: Optional[RTreeNode] = None
223
+ self._edge_count = 0
224
+ self._edge_id_counter = 0
225
+
226
+ # Statistics
227
+ self._tree_height = 0
228
+ self._total_nodes = 0
229
+
230
+ def get_supported_traits(self) -> EdgeTrait:
231
+ """Get the traits supported by the R-tree strategy."""
232
+ return (EdgeTrait.SPATIAL | EdgeTrait.DIRECTED | EdgeTrait.WEIGHTED | EdgeTrait.SPARSE)
233
+
234
+ def _generate_edge_id(self) -> str:
235
+ """Generate unique edge ID."""
236
+ self._edge_id_counter += 1
237
+ return f"spatial_edge_{self._edge_id_counter}"
238
+
239
+ def _choose_leaf(self, rect: Rectangle) -> RTreeNode:
240
+ """Choose leaf node for insertion using minimum enlargement heuristic."""
241
+ if self._root is None:
242
+ self._root = RTreeNode(is_leaf=True, max_entries=self.max_entries)
243
+ return self._root
244
+
245
+ current = self._root
246
+
247
+ while not current.is_leaf:
248
+ best_child = None
249
+ best_enlargement = float('inf')
250
+ best_area = float('inf')
251
+
252
+ for child_rect, child_node in current.entries:
253
+ enlargement = child_rect.enlargement_needed(rect)
254
+ area = child_rect.area()
255
+
256
+ if (enlargement < best_enlargement or
257
+ (enlargement == best_enlargement and area < best_area)):
258
+ best_enlargement = enlargement
259
+ best_area = area
260
+ best_child = child_node
261
+
262
+ current = best_child
263
+
264
+ return current
265
+
266
+ def _split_node(self, node: RTreeNode) -> Tuple[RTreeNode, RTreeNode]:
267
+ """Split overfull node using quadratic split algorithm."""
268
+ # Find two entries with maximum waste of space
269
+ max_waste = -1
270
+ seed1_idx = seed2_idx = 0
271
+
272
+ for i in range(len(node.entries)):
273
+ for j in range(i + 1, len(node.entries)):
274
+ rect1, _ = node.entries[i]
275
+ rect2, _ = node.entries[j]
276
+
277
+ union_area = rect1.union(rect2).area()
278
+ waste = union_area - rect1.area() - rect2.area()
279
+
280
+ if waste > max_waste:
281
+ max_waste = waste
282
+ seed1_idx = i
283
+ seed2_idx = j
284
+
285
+ # Create two new nodes
286
+ node1 = RTreeNode(is_leaf=node.is_leaf, max_entries=self.max_entries)
287
+ node2 = RTreeNode(is_leaf=node.is_leaf, max_entries=self.max_entries)
288
+
289
+ # Add seeds
290
+ node1.add_entry(*node.entries[seed1_idx])
291
+ node2.add_entry(*node.entries[seed2_idx])
292
+
293
+ # Distribute remaining entries
294
+ remaining_entries = [node.entries[i] for i in range(len(node.entries))
295
+ if i != seed1_idx and i != seed2_idx]
296
+
297
+ for rect, data in remaining_entries:
298
+ enlargement1 = node1.bounding_rect.enlargement_needed(rect)
299
+ enlargement2 = node2.bounding_rect.enlargement_needed(rect)
300
+
301
+ if enlargement1 < enlargement2:
302
+ node1.add_entry(rect, data)
303
+ elif enlargement2 < enlargement1:
304
+ node2.add_entry(rect, data)
305
+ else:
306
+ # Equal enlargement, choose smaller area
307
+ if node1.bounding_rect.area() <= node2.bounding_rect.area():
308
+ node1.add_entry(rect, data)
309
+ else:
310
+ node2.add_entry(rect, data)
311
+
312
+ return node1, node2
313
+
314
+ def _insert_with_split(self, edge: SpatialEdge) -> None:
315
+ """Insert edge with node splitting if necessary."""
316
+ leaf = self._choose_leaf(edge.bounding_rect)
317
+ leaf.add_entry(edge.bounding_rect, edge)
318
+
319
+ # Handle overflow
320
+ if leaf.is_full() and len(leaf.entries) > self.max_entries:
321
+ node1, node2 = self._split_node(leaf)
322
+
323
+ if leaf == self._root:
324
+ # Create new root
325
+ new_root = RTreeNode(is_leaf=False, max_entries=self.max_entries)
326
+ new_root.add_entry(node1.bounding_rect, node1)
327
+ new_root.add_entry(node2.bounding_rect, node2)
328
+ self._root = new_root
329
+ self._tree_height += 1
330
+ else:
331
+ # Replace leaf with split nodes (simplified - full implementation would propagate splits)
332
+ pass
333
+
334
+ # ============================================================================
335
+ # CORE EDGE OPERATIONS
336
+ # ============================================================================
337
+
338
+ def add_edge(self, source: str, target: str, **properties) -> str:
339
+ """Add spatial edge with coordinates."""
340
+ # Extract coordinates from properties
341
+ source_coords = properties.pop('source_coords', None)
342
+ target_coords = properties.pop('target_coords', None)
343
+
344
+ if source_coords is None and source in self._vertex_coords:
345
+ source_coords = self._vertex_coords[source]
346
+ if target_coords is None and target in self._vertex_coords:
347
+ target_coords = self._vertex_coords[target]
348
+
349
+ if source_coords is None or target_coords is None:
350
+ raise ValueError("Both source_coords and target_coords must be provided")
351
+
352
+ # Generate edge ID
353
+ edge_id = properties.pop('edge_id', self._generate_edge_id())
354
+
355
+ # Create spatial edge
356
+ spatial_edge = SpatialEdge(edge_id, source, target, source_coords, target_coords, **properties)
357
+
358
+ # Store edge and update vertices
359
+ self._edges[edge_id] = spatial_edge
360
+ self._vertex_coords[source] = source_coords
361
+ self._vertex_coords[target] = target_coords
362
+ self._vertices.add(source)
363
+ self._vertices.add(target)
364
+
365
+ # Insert into R-Tree
366
+ self._insert_with_split(spatial_edge)
367
+ self._edge_count += 1
368
+
369
+ return edge_id
370
+
371
+ def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
372
+ """Remove spatial edge with proper R-Tree cleanup."""
373
+ edge_to_remove = None
374
+ edge_id_to_remove = None
375
+
376
+ # Find the edge to remove
377
+ if edge_id and edge_id in self._edges:
378
+ edge = self._edges[edge_id]
379
+ if edge.source == source and edge.target == target:
380
+ edge_to_remove = edge
381
+ edge_id_to_remove = edge_id
382
+ else:
383
+ # Find edge by endpoints
384
+ for eid, edge in self._edges.items():
385
+ if edge.source == source and edge.target == target:
386
+ edge_to_remove = edge
387
+ edge_id_to_remove = eid
388
+ break
389
+
390
+ if edge_to_remove is None:
391
+ return False
392
+
393
+ # Remove from edge storage
394
+ del self._edges[edge_id_to_remove]
395
+ self._edge_count -= 1
396
+
397
+ # Remove from R-Tree structure
398
+ self._remove_from_rtree(edge_to_remove)
399
+
400
+ return True
401
+
402
+ def _remove_from_rtree(self, edge: SpatialEdge) -> bool:
403
+ """Remove edge from R-Tree structure with proper cleanup."""
404
+ if self._root is None:
405
+ return False
406
+
407
+ # Find the leaf node containing this edge
408
+ leaf_node = self._find_leaf_containing_edge(self._root, edge)
409
+ if leaf_node is None:
410
+ return False
411
+
412
+ # Remove edge from leaf node
413
+ edge_removed = False
414
+ for i, (rect, data) in enumerate(leaf_node.entries):
415
+ if isinstance(data, SpatialEdge) and data.edge_id == edge.edge_id:
416
+ leaf_node.entries.pop(i)
417
+ edge_removed = True
418
+ break
419
+
420
+ if not edge_removed:
421
+ return False
422
+
423
+ # Update leaf node's bounding rectangle
424
+ leaf_node.update_bounding_rect()
425
+
426
+ # Handle underflow in leaf node
427
+ if len(leaf_node.entries) < self.min_entries and leaf_node != self._root:
428
+ self._handle_underflow(leaf_node)
429
+ else:
430
+ # Propagate bounding rectangle changes up the tree
431
+ self._propagate_changes_up(leaf_node)
432
+
433
+ # Rebuild tree if root becomes empty
434
+ if self._root and len(self._root.entries) == 0:
435
+ self._rebuild_tree()
436
+
437
+ return True
438
+
439
+ def _find_leaf_containing_edge(self, node: RTreeNode, edge: SpatialEdge) -> Optional[RTreeNode]:
440
+ """Find the leaf node containing the specified edge."""
441
+ if node.is_leaf:
442
+ # Check if this leaf contains the edge
443
+ for rect, data in node.entries:
444
+ if isinstance(data, SpatialEdge) and data.edge_id == edge.edge_id:
445
+ return node
446
+ return None
447
+
448
+ # Search in child nodes
449
+ for rect, child_node in node.entries:
450
+ if rect.intersects(edge.bounding_rect):
451
+ result = self._find_leaf_containing_edge(child_node, edge)
452
+ if result is not None:
453
+ return result
454
+
455
+ return None
456
+
457
+ def _handle_underflow(self, node: RTreeNode) -> None:
458
+ """Handle underflow in a node by redistributing or merging entries."""
459
+ # Find sibling nodes
460
+ parent = self._find_parent(self._root, node)
461
+ if parent is None:
462
+ return
463
+
464
+ siblings = []
465
+ for rect, child in parent.entries:
466
+ if child != node:
467
+ siblings.append(child)
468
+
469
+ # Try to redistribute entries from siblings
470
+ for sibling in siblings:
471
+ if len(sibling.entries) > self.min_entries:
472
+ # Redistribute one entry from sibling
473
+ entry_to_move = sibling.entries.pop()
474
+ node.entries.append(entry_to_move)
475
+
476
+ # Update bounding rectangles
477
+ node.update_bounding_rect()
478
+ sibling.update_bounding_rect()
479
+ self._propagate_changes_up(parent)
480
+ return
481
+
482
+ # If redistribution fails, merge with a sibling
483
+ if siblings:
484
+ sibling = siblings[0]
485
+ # Move all entries from node to sibling
486
+ sibling.entries.extend(node.entries)
487
+ sibling.update_bounding_rect()
488
+
489
+ # Remove node from parent
490
+ for i, (rect, child) in enumerate(parent.entries):
491
+ if child == node:
492
+ parent.entries.pop(i)
493
+ break
494
+
495
+ parent.update_bounding_rect()
496
+ self._propagate_changes_up(parent)
497
+
498
+ def _find_parent(self, current: RTreeNode, target: RTreeNode) -> Optional[RTreeNode]:
499
+ """Find the parent of a target node."""
500
+ if current.is_leaf:
501
+ return None
502
+
503
+ for rect, child in current.entries:
504
+ if child == target:
505
+ return current
506
+
507
+ result = self._find_parent(child, target)
508
+ if result is not None:
509
+ return result
510
+
511
+ return None
512
+
513
+ def _propagate_changes_up(self, node: RTreeNode) -> None:
514
+ """Propagate bounding rectangle changes up the tree."""
515
+ parent = self._find_parent(self._root, node)
516
+ if parent is not None:
517
+ parent.update_bounding_rect()
518
+ self._propagate_changes_up(parent)
519
+
520
+ def _rebuild_tree(self) -> None:
521
+ """Rebuild the R-Tree from existing edges."""
522
+ if not self._edges:
523
+ self._root = None
524
+ self._tree_height = 0
525
+ self._total_nodes = 0
526
+ return
527
+
528
+ # Collect all edges
529
+ edges = list(self._edges.values())
530
+
531
+ # Clear current tree
532
+ self._root = None
533
+ self._tree_height = 0
534
+ self._total_nodes = 0
535
+
536
+ # Rebuild tree by re-inserting all edges
537
+ for edge in edges:
538
+ self._insert_edge_into_tree(edge)
539
+
540
+ def _insert_edge_into_tree(self, edge: SpatialEdge) -> None:
541
+ """Insert edge into R-Tree structure."""
542
+ if self._root is None:
543
+ self._root = RTreeNode(is_leaf=True, max_entries=self.max_entries)
544
+ self._tree_height = 1
545
+ self._total_nodes = 1
546
+
547
+ # Choose leaf for insertion
548
+ leaf = self._choose_leaf(edge.bounding_rect)
549
+
550
+ # Add edge to leaf
551
+ leaf.add_entry(edge.bounding_rect, edge)
552
+
553
+ # Handle overflow
554
+ if leaf.is_full():
555
+ self._handle_overflow(leaf)
556
+
557
+ def _handle_overflow(self, node: RTreeNode) -> None:
558
+ """Handle overflow in a node by splitting."""
559
+ if not node.is_full():
560
+ return
561
+
562
+ # Split the node
563
+ node1, node2 = self._split_node(node)
564
+
565
+ # If this is the root, create a new root
566
+ if node == self._root:
567
+ new_root = RTreeNode(is_leaf=False, max_entries=self.max_entries)
568
+ new_root.add_entry(node1.bounding_rect, node1)
569
+ new_root.add_entry(node2.bounding_rect, node2)
570
+ self._root = new_root
571
+ self._tree_height += 1
572
+ self._total_nodes += 1
573
+ else:
574
+ # Replace the original node with the first split node
575
+ parent = self._find_parent(self._root, node)
576
+ if parent is not None:
577
+ # Find and replace the original node
578
+ for i, (rect, child) in enumerate(parent.entries):
579
+ if child == node:
580
+ parent.entries[i] = (node1.bounding_rect, node1)
581
+ break
582
+
583
+ # Add the second split node to parent
584
+ parent.add_entry(node2.bounding_rect, node2)
585
+ parent.update_bounding_rect()
586
+
587
+ # Handle overflow in parent if necessary
588
+ if parent.is_full():
589
+ self._handle_overflow(parent)
590
+
591
+ def has_edge(self, source: str, target: str) -> bool:
592
+ """Check if edge exists."""
593
+ for edge in self._edges.values():
594
+ if edge.source == source and edge.target == target:
595
+ return True
596
+ return False
597
+
598
+ def get_edge_data(self, source: str, target: str) -> Optional[Dict[str, Any]]:
599
+ """Get edge data."""
600
+ for edge in self._edges.values():
601
+ if edge.source == source and edge.target == target:
602
+ return edge.to_dict()
603
+ return None
604
+
605
+ def neighbors(self, vertex: str, direction: str = 'out') -> Iterator[str]:
606
+ """Get neighbors of vertex."""
607
+ neighbors = set()
608
+
609
+ for edge in self._edges.values():
610
+ if direction in ['out', 'both'] and edge.source == vertex:
611
+ neighbors.add(edge.target)
612
+ if direction in ['in', 'both'] and edge.target == vertex:
613
+ neighbors.add(edge.source)
614
+
615
+ return iter(neighbors)
616
+
617
+ def degree(self, vertex: str, direction: str = 'out') -> int:
618
+ """Get degree of vertex."""
619
+ return len(list(self.neighbors(vertex, direction)))
620
+
621
+ def edges(self, data: bool = False) -> Iterator[tuple]:
622
+ """Get all edges."""
623
+ for edge in self._edges.values():
624
+ if data:
625
+ yield (edge.source, edge.target, edge.to_dict())
626
+ else:
627
+ yield (edge.source, edge.target)
628
+
629
+ def vertices(self) -> Iterator[str]:
630
+ """Get all vertices."""
631
+ return iter(self._vertices)
632
+
633
+ def __len__(self) -> int:
634
+ """Get number of edges."""
635
+ return self._edge_count
636
+
637
+ def vertex_count(self) -> int:
638
+ """Get number of vertices."""
639
+ return len(self._vertices)
640
+
641
+ def clear(self) -> None:
642
+ """Clear all data."""
643
+ self._edges.clear()
644
+ self._vertex_coords.clear()
645
+ self._vertices.clear()
646
+ self._root = None
647
+ self._edge_count = 0
648
+ self._edge_id_counter = 0
649
+ self._tree_height = 0
650
+
651
+ def add_vertex(self, vertex: str, coords: Tuple[float, float] = None) -> None:
652
+ """Add vertex with coordinates."""
653
+ self._vertices.add(vertex)
654
+ if coords:
655
+ self._vertex_coords[vertex] = coords
656
+
657
+ def remove_vertex(self, vertex: str) -> bool:
658
+ """Remove vertex and all its edges."""
659
+ if vertex not in self._vertices:
660
+ return False
661
+
662
+ # Remove all edges involving this vertex
663
+ edges_to_remove = [eid for eid, edge in self._edges.items()
664
+ if edge.source == vertex or edge.target == vertex]
665
+
666
+ for edge_id in edges_to_remove:
667
+ edge = self._edges[edge_id]
668
+ self.remove_edge(edge.source, edge.target, edge_id)
669
+
670
+ self._vertices.discard(vertex)
671
+ self._vertex_coords.pop(vertex, None)
672
+
673
+ return True
674
+
675
+ # ============================================================================
676
+ # SPATIAL QUERY OPERATIONS
677
+ # ============================================================================
678
+
679
+ def range_query(self, min_x: float, min_y: float, max_x: float, max_y: float) -> List[SpatialEdge]:
680
+ """Find all edges intersecting with rectangle."""
681
+ query_rect = Rectangle(min_x, min_y, max_x, max_y)
682
+ result = []
683
+
684
+ def search_node(node: RTreeNode):
685
+ if node is None:
686
+ return
687
+
688
+ for rect, data in node.entries:
689
+ if query_rect.intersects(rect):
690
+ if node.is_leaf:
691
+ # data is SpatialEdge
692
+ if data.intersects_rectangle(query_rect):
693
+ result.append(data)
694
+ else:
695
+ # data is child node
696
+ search_node(data)
697
+
698
+ search_node(self._root)
699
+ return result
700
+
701
+ def point_query(self, x: float, y: float, radius: float = 0.0) -> List[SpatialEdge]:
702
+ """Find edges near a point within radius."""
703
+ if radius == 0.0:
704
+ # Exact point query
705
+ query_rect = Rectangle(x, y, x, y)
706
+ else:
707
+ # Range query with radius
708
+ query_rect = Rectangle(x - radius, y - radius, x + radius, y + radius)
709
+
710
+ candidates = self.range_query(query_rect.min_x, query_rect.min_y,
711
+ query_rect.max_x, query_rect.max_y)
712
+
713
+ if radius == 0.0:
714
+ return candidates
715
+
716
+ # Filter by actual distance
717
+ result = []
718
+ for edge in candidates:
719
+ if edge.distance_to_point(x, y) <= radius:
720
+ result.append(edge)
721
+
722
+ return result
723
+
724
+ def nearest_neighbor(self, x: float, y: float, k: int = 1) -> List[Tuple[SpatialEdge, float]]:
725
+ """Find k nearest edges to point."""
726
+ # Simple implementation - can be optimized with priority queue
727
+ distances = []
728
+
729
+ for edge in self._edges.values():
730
+ distance = edge.distance_to_point(x, y)
731
+ distances.append((edge, distance))
732
+
733
+ distances.sort(key=lambda x: x[1])
734
+ return distances[:k]
735
+
736
+ def edges_in_region(self, center_x: float, center_y: float, radius: float) -> List[SpatialEdge]:
737
+ """Find all edges within circular region."""
738
+ # Use square approximation for efficiency
739
+ candidates = self.range_query(center_x - radius, center_y - radius,
740
+ center_x + radius, center_y + radius)
741
+
742
+ result = []
743
+ for edge in candidates:
744
+ # Check if any part of edge is within radius
745
+ dist_to_source = math.sqrt((edge.source_coords[0] - center_x) ** 2 +
746
+ (edge.source_coords[1] - center_y) ** 2)
747
+ dist_to_target = math.sqrt((edge.target_coords[0] - center_x) ** 2 +
748
+ (edge.target_coords[1] - center_y) ** 2)
749
+
750
+ if dist_to_source <= radius or dist_to_target <= radius:
751
+ result.append(edge)
752
+ elif edge.distance_to_point(center_x, center_y) <= radius:
753
+ result.append(edge)
754
+
755
+ return result
756
+
757
+ def get_bounding_box(self) -> Optional[Rectangle]:
758
+ """Get bounding box of all edges."""
759
+ if self._root and self._root.bounding_rect:
760
+ return self._root.bounding_rect
761
+ return None
762
+
763
+ def spatial_statistics(self) -> Dict[str, Any]:
764
+ """Get spatial statistics."""
765
+ if not self._edges:
766
+ return {'edges': 0, 'vertices': 0, 'total_length': 0, 'avg_length': 0}
767
+
768
+ lengths = [edge.length for edge in self._edges.values()]
769
+ bounding_box = self.get_bounding_box()
770
+
771
+ return {
772
+ 'edges': len(self._edges),
773
+ 'vertices': len(self._vertices),
774
+ 'total_length': sum(lengths),
775
+ 'avg_length': sum(lengths) / len(lengths),
776
+ 'min_length': min(lengths),
777
+ 'max_length': max(lengths),
778
+ 'bounding_box': bounding_box._asdict() if bounding_box else None,
779
+ 'tree_height': self._tree_height,
780
+ 'spatial_extent': {
781
+ 'width': bounding_box.max_x - bounding_box.min_x if bounding_box else 0,
782
+ 'height': bounding_box.max_y - bounding_box.min_y if bounding_box else 0
783
+ }
784
+ }
785
+
786
+ # ============================================================================
787
+ # PERFORMANCE CHARACTERISTICS
788
+ # ============================================================================
789
+
790
+ @property
791
+ def backend_info(self) -> Dict[str, Any]:
792
+ """Get backend implementation info."""
793
+ return {
794
+ 'strategy': 'R_TREE',
795
+ 'backend': 'R-Tree spatial index with quadratic split',
796
+ 'max_entries': self.max_entries,
797
+ 'min_entries': self.min_entries,
798
+ 'is_directed': self.is_directed,
799
+ 'complexity': {
800
+ 'insert': f'O(log_M n)', # M = max_entries
801
+ 'delete': f'O(log_M n)',
802
+ 'range_query': f'O(log_M n + k)', # k = result size
803
+ 'point_query': f'O(log_M n + k)',
804
+ 'space': 'O(n)'
805
+ }
806
+ }
807
+
808
+ @property
809
+ def metrics(self) -> Dict[str, Any]:
810
+ """Get performance metrics."""
811
+ stats = self.spatial_statistics()
812
+
813
+ return {
814
+ 'edges': self._edge_count,
815
+ 'vertices': len(self._vertices),
816
+ 'tree_height': self._tree_height,
817
+ 'avg_edge_length': f"{stats.get('avg_length', 0):.2f}",
818
+ 'spatial_extent': f"{stats.get('spatial_extent', {}).get('width', 0):.1f} x {stats.get('spatial_extent', {}).get('height', 0):.1f}",
819
+ 'memory_usage': f"{self._edge_count * 150 + len(self._vertices) * 50} bytes (estimated)"
820
+ }