exonware-xwnode 0.0.1.21__py3-none-any.whl → 0.0.1.23__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 (250) hide show
  1. exonware/__init__.py +8 -1
  2. exonware/xwnode/__init__.py +18 -5
  3. exonware/xwnode/add_strategy_types.py +165 -0
  4. exonware/xwnode/base.py +7 -5
  5. exonware/xwnode/common/__init__.py +1 -1
  6. exonware/xwnode/common/graph/__init__.py +30 -0
  7. exonware/xwnode/common/graph/caching.py +131 -0
  8. exonware/xwnode/common/graph/contracts.py +100 -0
  9. exonware/xwnode/common/graph/errors.py +44 -0
  10. exonware/xwnode/common/graph/indexing.py +260 -0
  11. exonware/xwnode/common/graph/manager.py +568 -0
  12. exonware/xwnode/common/management/__init__.py +3 -5
  13. exonware/xwnode/common/management/manager.py +9 -9
  14. exonware/xwnode/common/management/migration.py +6 -6
  15. exonware/xwnode/common/monitoring/__init__.py +3 -5
  16. exonware/xwnode/common/monitoring/metrics.py +7 -3
  17. exonware/xwnode/common/monitoring/pattern_detector.py +2 -2
  18. exonware/xwnode/common/monitoring/performance_monitor.py +6 -2
  19. exonware/xwnode/common/patterns/__init__.py +3 -5
  20. exonware/xwnode/common/patterns/advisor.py +1 -1
  21. exonware/xwnode/common/patterns/flyweight.py +6 -2
  22. exonware/xwnode/common/patterns/registry.py +203 -184
  23. exonware/xwnode/common/utils/__init__.py +25 -11
  24. exonware/xwnode/common/utils/simple.py +1 -1
  25. exonware/xwnode/config.py +3 -8
  26. exonware/xwnode/contracts.py +4 -105
  27. exonware/xwnode/defs.py +413 -159
  28. exonware/xwnode/edges/strategies/__init__.py +86 -4
  29. exonware/xwnode/edges/strategies/_base_edge.py +2 -2
  30. exonware/xwnode/edges/strategies/adj_list.py +287 -121
  31. exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
  32. exonware/xwnode/edges/strategies/base.py +1 -1
  33. exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
  34. exonware/xwnode/edges/strategies/bitemporal.py +520 -0
  35. exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
  36. exonware/xwnode/edges/strategies/bv_graph.py +664 -0
  37. exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
  38. exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
  39. exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
  40. exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
  41. exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
  42. exonware/xwnode/edges/strategies/edge_list.py +168 -0
  43. exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
  44. exonware/xwnode/edges/strategies/euler_tour.py +560 -0
  45. exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
  46. exonware/xwnode/edges/strategies/graphblas.py +449 -0
  47. exonware/xwnode/edges/strategies/hnsw.py +637 -0
  48. exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
  49. exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
  50. exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
  51. exonware/xwnode/edges/strategies/k2_tree.py +613 -0
  52. exonware/xwnode/edges/strategies/link_cut.py +626 -0
  53. exonware/xwnode/edges/strategies/multiplex.py +532 -0
  54. exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
  55. exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
  56. exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
  57. exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
  58. exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
  59. exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
  60. exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
  61. exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
  62. exonware/xwnode/errors.py +3 -6
  63. exonware/xwnode/facade.py +20 -20
  64. exonware/xwnode/nodes/strategies/__init__.py +29 -9
  65. exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
  66. exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
  67. exonware/xwnode/nodes/strategies/array_list.py +36 -3
  68. exonware/xwnode/nodes/strategies/art.py +581 -0
  69. exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
  70. exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
  71. exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
  72. exonware/xwnode/nodes/strategies/base.py +469 -98
  73. exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
  74. exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
  75. exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
  76. exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
  77. exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
  78. exonware/xwnode/nodes/strategies/contracts.py +1 -1
  79. exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
  80. exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
  81. exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
  82. exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
  83. exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
  84. exonware/xwnode/nodes/strategies/dawg.py +876 -0
  85. exonware/xwnode/nodes/strategies/deque.py +321 -153
  86. exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
  87. exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
  88. exonware/xwnode/nodes/strategies/hamt.py +403 -0
  89. exonware/xwnode/nodes/strategies/hash_map.py +354 -67
  90. exonware/xwnode/nodes/strategies/heap.py +105 -5
  91. exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
  92. exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
  93. exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
  94. exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
  95. exonware/xwnode/nodes/strategies/learned_index.py +533 -0
  96. exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
  97. exonware/xwnode/nodes/strategies/linked_list.py +316 -119
  98. exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
  99. exonware/xwnode/nodes/strategies/masstree.py +130 -0
  100. exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
  101. exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
  102. exonware/xwnode/nodes/strategies/queue.py +249 -120
  103. exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
  104. exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
  105. exonware/xwnode/nodes/strategies/rope.py +717 -0
  106. exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
  107. exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
  108. exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
  109. exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
  110. exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
  111. exonware/xwnode/nodes/strategies/stack.py +244 -112
  112. exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
  113. exonware/xwnode/nodes/strategies/t_tree.py +94 -0
  114. exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
  115. exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
  116. exonware/xwnode/nodes/strategies/trie.py +153 -9
  117. exonware/xwnode/nodes/strategies/union_find.py +111 -5
  118. exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
  119. exonware/xwnode/strategies/__init__.py +5 -51
  120. exonware/xwnode/version.py +3 -3
  121. {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/METADATA +23 -3
  122. exonware_xwnode-0.0.1.23.dist-info/RECORD +130 -0
  123. exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
  124. exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
  125. exonware/xwnode/nodes/strategies/_base_node.py +0 -307
  126. exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
  127. exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
  128. exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
  129. exonware/xwnode/nodes/strategies/node_heap.py +0 -196
  130. exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
  131. exonware/xwnode/nodes/strategies/node_trie.py +0 -257
  132. exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
  133. exonware/xwnode/queries/executors/__init__.py +0 -47
  134. exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
  135. exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
  136. exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
  137. exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
  138. exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
  139. exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
  140. exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
  141. exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
  142. exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
  143. exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
  144. exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
  145. exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
  146. exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
  147. exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
  148. exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
  149. exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
  150. exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
  151. exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
  152. exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
  153. exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
  154. exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
  155. exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
  156. exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
  157. exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
  158. exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
  159. exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
  160. exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
  161. exonware/xwnode/queries/executors/array/__init__.py +0 -9
  162. exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
  163. exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
  164. exonware/xwnode/queries/executors/base.py +0 -257
  165. exonware/xwnode/queries/executors/capability_checker.py +0 -204
  166. exonware/xwnode/queries/executors/contracts.py +0 -166
  167. exonware/xwnode/queries/executors/core/__init__.py +0 -17
  168. exonware/xwnode/queries/executors/core/create_executor.py +0 -96
  169. exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
  170. exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
  171. exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
  172. exonware/xwnode/queries/executors/core/select_executor.py +0 -152
  173. exonware/xwnode/queries/executors/core/update_executor.py +0 -102
  174. exonware/xwnode/queries/executors/data/__init__.py +0 -13
  175. exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
  176. exonware/xwnode/queries/executors/data/load_executor.py +0 -50
  177. exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
  178. exonware/xwnode/queries/executors/data/store_executor.py +0 -50
  179. exonware/xwnode/queries/executors/defs.py +0 -93
  180. exonware/xwnode/queries/executors/engine.py +0 -221
  181. exonware/xwnode/queries/executors/errors.py +0 -68
  182. exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
  183. exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
  184. exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
  185. exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
  186. exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
  187. exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
  188. exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
  189. exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
  190. exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
  191. exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
  192. exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
  193. exonware/xwnode/queries/executors/graph/__init__.py +0 -15
  194. exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
  195. exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
  196. exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
  197. exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
  198. exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
  199. exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
  200. exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
  201. exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
  202. exonware/xwnode/queries/executors/projection/__init__.py +0 -9
  203. exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
  204. exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
  205. exonware/xwnode/queries/executors/registry.py +0 -173
  206. exonware/xwnode/queries/parsers/__init__.py +0 -26
  207. exonware/xwnode/queries/parsers/base.py +0 -86
  208. exonware/xwnode/queries/parsers/contracts.py +0 -46
  209. exonware/xwnode/queries/parsers/errors.py +0 -53
  210. exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
  211. exonware/xwnode/queries/strategies/__init__.py +0 -24
  212. exonware/xwnode/queries/strategies/base.py +0 -236
  213. exonware/xwnode/queries/strategies/cql.py +0 -201
  214. exonware/xwnode/queries/strategies/cypher.py +0 -181
  215. exonware/xwnode/queries/strategies/datalog.py +0 -70
  216. exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
  217. exonware/xwnode/queries/strategies/eql.py +0 -70
  218. exonware/xwnode/queries/strategies/flux.py +0 -70
  219. exonware/xwnode/queries/strategies/gql.py +0 -70
  220. exonware/xwnode/queries/strategies/graphql.py +0 -240
  221. exonware/xwnode/queries/strategies/gremlin.py +0 -181
  222. exonware/xwnode/queries/strategies/hiveql.py +0 -214
  223. exonware/xwnode/queries/strategies/hql.py +0 -70
  224. exonware/xwnode/queries/strategies/jmespath.py +0 -219
  225. exonware/xwnode/queries/strategies/jq.py +0 -66
  226. exonware/xwnode/queries/strategies/json_query.py +0 -66
  227. exonware/xwnode/queries/strategies/jsoniq.py +0 -248
  228. exonware/xwnode/queries/strategies/kql.py +0 -70
  229. exonware/xwnode/queries/strategies/linq.py +0 -238
  230. exonware/xwnode/queries/strategies/logql.py +0 -70
  231. exonware/xwnode/queries/strategies/mql.py +0 -68
  232. exonware/xwnode/queries/strategies/n1ql.py +0 -210
  233. exonware/xwnode/queries/strategies/partiql.py +0 -70
  234. exonware/xwnode/queries/strategies/pig.py +0 -215
  235. exonware/xwnode/queries/strategies/promql.py +0 -70
  236. exonware/xwnode/queries/strategies/sparql.py +0 -220
  237. exonware/xwnode/queries/strategies/sql.py +0 -275
  238. exonware/xwnode/queries/strategies/xml_query.py +0 -66
  239. exonware/xwnode/queries/strategies/xpath.py +0 -223
  240. exonware/xwnode/queries/strategies/xquery.py +0 -258
  241. exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
  242. exonware/xwnode/queries/strategies/xwquery.py +0 -456
  243. exonware_xwnode-0.0.1.21.dist-info/RECORD +0 -214
  244. /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
  245. /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
  246. /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
  247. /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
  248. /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
  249. {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
  250. {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,703 @@
1
+ """
2
+ #exonware/xwnode/src/exonware/xwnode/nodes/strategies/kd_tree.py
3
+
4
+ k-d Tree Node Strategy Implementation
5
+
6
+ This module implements the KD_TREE strategy for multi-dimensional point
7
+ queries and nearest neighbor search.
8
+
9
+ Company: eXonware.com
10
+ Author: Eng. Muhammad AlShehri
11
+ Email: connect@exonware.com
12
+ Version: 0.0.1.23
13
+ Generation Date: 12-Oct-2025
14
+ """
15
+
16
+ import math
17
+ from typing import Any, Iterator, List, Dict, Optional, Tuple, Callable
18
+ from .base import ANodeTreeStrategy
19
+ from .contracts import NodeType
20
+ from ...defs import NodeMode, NodeTrait
21
+ from ...errors import XWNodeError, XWNodeValueError
22
+
23
+
24
+ class KdNode:
25
+ """
26
+ Node in k-d tree.
27
+
28
+ WHY dimension cycling:
29
+ - Ensures balanced partitioning across all dimensions
30
+ - Enables efficient multi-dimensional search
31
+ - Logarithmic depth for n points
32
+ """
33
+
34
+ def __init__(self, point: Tuple[float, ...], value: Any, axis: int):
35
+ """
36
+ Initialize k-d tree node.
37
+
38
+ Args:
39
+ point: k-dimensional point coordinates
40
+ value: Associated data
41
+ axis: Splitting dimension at this level
42
+ """
43
+ self.point = point
44
+ self.value = value
45
+ self.axis = axis
46
+ self.left: Optional['KdNode'] = None
47
+ self.right: Optional['KdNode'] = None
48
+
49
+
50
+ class KdTreeStrategy(ANodeTreeStrategy):
51
+ """
52
+ k-d Tree strategy for multi-dimensional point queries.
53
+
54
+ WHY k-d Tree:
55
+ - Efficient multi-dimensional point queries: O(log n) average
56
+ - Near-optimal nearest neighbor search in low dimensions
57
+ - Natural partitioning for spatial data
58
+ - Excellent for machine learning (k-NN, clustering)
59
+ - Widely used in computer graphics and GIS
60
+
61
+ WHY this implementation:
62
+ - Median-based splitting ensures balanced trees
63
+ - Cycles through dimensions for uniform partitioning
64
+ - Supports arbitrary dimensions (k >= 1)
65
+ - Distance metrics customizable (Euclidean, Manhattan, etc.)
66
+ - Bounding box pruning for efficient range queries
67
+
68
+ Time Complexity:
69
+ - Insert: O(log n) average, O(n) worst (unbalanced)
70
+ - Search: O(log n) average
71
+ - Nearest neighbor: O(log n) average, O(n) worst
72
+ - Range query: O(n^(1-1/k) + m) where m is result size
73
+ - Build from n points: O(n log n) with median finding
74
+
75
+ Space Complexity: O(n) for n points
76
+
77
+ Trade-offs:
78
+ - Advantage: Efficient for low dimensions (k ≤ 20)
79
+ - Advantage: Simple structure, easy to understand
80
+ - Advantage: Good cache locality for small k
81
+ - Limitation: Performance degrades for high dimensions (curse of dimensionality)
82
+ - Limitation: Requires rebalancing for optimal performance
83
+ - Limitation: Nearest neighbor becomes O(n) for k > 20
84
+ - Compared to R-tree: Simpler, better for points, worse for rectangles
85
+ - Compared to Ball tree: Better for low k, worse for high k
86
+
87
+ Best for:
88
+ - 2D/3D point clouds (graphics, GIS, robotics)
89
+ - Machine learning k-NN classification
90
+ - Nearest neighbor search (k ≤ 20 dimensions)
91
+ - Spatial indexing for games
92
+ - Computer vision applications
93
+ - Low-dimensional scientific data
94
+
95
+ Not recommended for:
96
+ - High-dimensional data (k > 20) - use LSH, HNSW instead
97
+ - Dynamic frequent updates (use R-tree)
98
+ - Rectangle/region queries (use R-tree)
99
+ - Very large datasets in high dimensions
100
+ - String or categorical data
101
+
102
+ Following eXonware Priorities:
103
+ 1. Security: Validates dimensions, prevents malformed points
104
+ 2. Usability: Natural point API, clear dimension handling
105
+ 3. Maintainability: Clean recursive structure, dimension cycling
106
+ 4. Performance: O(log n) queries for low dimensions
107
+ 5. Extensibility: Easy to add metrics, balancing strategies
108
+
109
+ Industry Best Practices:
110
+ - Follows Bentley's original k-d tree paper (1975)
111
+ - Implements median-based splitting for balance
112
+ - Supports customizable distance metrics
113
+ - Provides bounding box pruning
114
+ - Compatible with k-NN algorithms
115
+ """
116
+
117
+ # Tree node type for classification
118
+ STRATEGY_TYPE: NodeType = NodeType.TREE
119
+
120
+ def __init__(self, mode: NodeMode = NodeMode.KD_TREE,
121
+ traits: NodeTrait = NodeTrait.NONE,
122
+ dimensions: int = 2, **options):
123
+ """
124
+ Initialize k-d tree strategy.
125
+
126
+ Args:
127
+ mode: Node mode
128
+ traits: Node traits
129
+ dimensions: Number of dimensions (k)
130
+ **options: Additional options
131
+
132
+ Raises:
133
+ XWNodeValueError: If dimensions < 1
134
+ """
135
+ if dimensions < 1:
136
+ raise XWNodeValueError(f"Dimensions must be >= 1, got {dimensions}")
137
+
138
+ super().__init__(mode, traits, **options)
139
+
140
+ self.dimensions = dimensions
141
+ self._root: Optional[KdNode] = None
142
+ self._size = 0
143
+ self._points: Dict[Tuple[float, ...], Any] = {} # Point -> value mapping
144
+
145
+ def get_supported_traits(self) -> NodeTrait:
146
+ """Get supported traits."""
147
+ return NodeTrait.SPATIAL | NodeTrait.INDEXED | NodeTrait.HIERARCHICAL
148
+
149
+ # ============================================================================
150
+ # HELPER METHODS
151
+ # ============================================================================
152
+
153
+ def _validate_point(self, point: Any) -> Tuple[float, ...]:
154
+ """
155
+ Validate and normalize point to tuple.
156
+
157
+ Args:
158
+ point: Point coordinates
159
+
160
+ Returns:
161
+ Normalized point tuple
162
+
163
+ Raises:
164
+ XWNodeValueError: If point is invalid
165
+ """
166
+ if isinstance(point, (tuple, list)):
167
+ if len(point) != self.dimensions:
168
+ raise XWNodeValueError(
169
+ f"Point must have {self.dimensions} dimensions, got {len(point)}"
170
+ )
171
+ return tuple(float(x) for x in point)
172
+ else:
173
+ raise XWNodeValueError(
174
+ f"Point must be tuple or list, got {type(point).__name__}"
175
+ )
176
+
177
+ def _euclidean_distance(self, p1: Tuple[float, ...], p2: Tuple[float, ...]) -> float:
178
+ """Calculate Euclidean distance between points."""
179
+ return math.sqrt(sum((a - b) ** 2 for a, b in zip(p1, p2)))
180
+
181
+ # ============================================================================
182
+ # CORE OPERATIONS
183
+ # ============================================================================
184
+
185
+ def put(self, key: Any, value: Any = None) -> None:
186
+ """
187
+ Insert point into k-d tree.
188
+
189
+ Args:
190
+ key: Point coordinates (tuple or list)
191
+ value: Associated value
192
+
193
+ Raises:
194
+ XWNodeValueError: If key is invalid point
195
+ """
196
+ # Security: Validate point
197
+ point = self._validate_point(key)
198
+
199
+ # Insert into tree
200
+ if self._root is None:
201
+ self._root = KdNode(point, value, 0)
202
+ self._points[point] = value
203
+ self._size += 1
204
+ else:
205
+ # Check if point already exists
206
+ if point in self._points:
207
+ # Update existing value
208
+ self._update_value(self._root, point, value, 0)
209
+ self._points[point] = value
210
+ else:
211
+ # Insert new point
212
+ self._insert_recursive(self._root, point, value, 0)
213
+ self._points[point] = value
214
+ self._size += 1
215
+
216
+ def _insert_recursive(self, node: KdNode, point: Tuple[float, ...],
217
+ value: Any, depth: int) -> KdNode:
218
+ """
219
+ Recursively insert point.
220
+
221
+ Args:
222
+ node: Current node
223
+ point: Point to insert
224
+ value: Associated value
225
+ depth: Current depth
226
+
227
+ Returns:
228
+ Node (for tree structure building)
229
+ """
230
+ axis = depth % self.dimensions
231
+
232
+ if point[axis] < node.point[axis]:
233
+ if node.left is None:
234
+ node.left = KdNode(point, value, axis)
235
+ else:
236
+ self._insert_recursive(node.left, point, value, depth + 1)
237
+ else:
238
+ if node.right is None:
239
+ node.right = KdNode(point, value, axis)
240
+ else:
241
+ self._insert_recursive(node.right, point, value, depth + 1)
242
+
243
+ return node
244
+
245
+ def _update_value(self, node: Optional[KdNode], point: Tuple[float, ...],
246
+ value: Any, depth: int) -> bool:
247
+ """Update value for existing point."""
248
+ if node is None:
249
+ return False
250
+
251
+ if node.point == point:
252
+ node.value = value
253
+ return True
254
+
255
+ axis = depth % self.dimensions
256
+
257
+ if point[axis] < node.point[axis]:
258
+ return self._update_value(node.left, point, value, depth + 1)
259
+ else:
260
+ return self._update_value(node.right, point, value, depth + 1)
261
+
262
+ def get(self, key: Any, default: Any = None) -> Any:
263
+ """
264
+ Retrieve value by point.
265
+
266
+ Args:
267
+ key: Point coordinates
268
+ default: Default value
269
+
270
+ Returns:
271
+ Value or default
272
+ """
273
+ try:
274
+ point = self._validate_point(key)
275
+ except XWNodeValueError:
276
+ return default
277
+
278
+ return self._points.get(point, default)
279
+
280
+ def has(self, key: Any) -> bool:
281
+ """Check if point exists."""
282
+ try:
283
+ point = self._validate_point(key)
284
+ return point in self._points
285
+ except XWNodeValueError:
286
+ return False
287
+
288
+ def delete(self, key: Any) -> bool:
289
+ """
290
+ Delete point.
291
+
292
+ Args:
293
+ key: Point coordinates
294
+
295
+ Returns:
296
+ True if deleted, False if not found
297
+
298
+ Note: Simplified deletion. Full implementation would rebalance.
299
+ """
300
+ try:
301
+ point = self._validate_point(key)
302
+ except XWNodeValueError:
303
+ return False
304
+
305
+ if point not in self._points:
306
+ return False
307
+
308
+ del self._points[point]
309
+ self._size -= 1
310
+
311
+ # For simplicity, rebuild tree (O(n log n))
312
+ # Full implementation would do node removal with rebalancing
313
+ points_list = list(self._points.items())
314
+ self._root = None
315
+ self._size = 0
316
+ self._points.clear()
317
+
318
+ for pt, val in points_list:
319
+ if pt != point:
320
+ self.put(pt, val)
321
+
322
+ return True
323
+
324
+ # ============================================================================
325
+ # K-D TREE SPECIFIC OPERATIONS
326
+ # ============================================================================
327
+
328
+ def nearest_neighbor(self, query_point: Tuple[float, ...],
329
+ distance_fn: Optional[Callable] = None) -> Optional[Tuple[Tuple[float, ...], Any]]:
330
+ """
331
+ Find nearest neighbor to query point.
332
+
333
+ Args:
334
+ query_point: Query coordinates
335
+ distance_fn: Distance function (default: Euclidean)
336
+
337
+ Returns:
338
+ (point, value) tuple of nearest neighbor or None
339
+
340
+ Raises:
341
+ XWNodeValueError: If query_point is invalid
342
+ """
343
+ # Security: Validate query
344
+ query = self._validate_point(query_point)
345
+
346
+ if self._root is None:
347
+ return None
348
+
349
+ if distance_fn is None:
350
+ distance_fn = self._euclidean_distance
351
+
352
+ best = [None, float('inf')] # [node, distance]
353
+
354
+ def search_nn(node: Optional[KdNode], depth: int) -> None:
355
+ """Recursive nearest neighbor search."""
356
+ if node is None:
357
+ return
358
+
359
+ # Calculate distance to current node
360
+ dist = distance_fn(query, node.point)
361
+
362
+ if dist < best[1]:
363
+ best[0] = node
364
+ best[1] = dist
365
+
366
+ # Determine which subtree to search first
367
+ axis = depth % self.dimensions
368
+
369
+ if query[axis] < node.point[axis]:
370
+ near, far = node.left, node.right
371
+ else:
372
+ near, far = node.right, node.left
373
+
374
+ # Search near subtree
375
+ search_nn(near, depth + 1)
376
+
377
+ # Search far subtree if necessary
378
+ axis_dist = abs(query[axis] - node.point[axis])
379
+ if axis_dist < best[1]:
380
+ search_nn(far, depth + 1)
381
+
382
+ search_nn(self._root, 0)
383
+
384
+ if best[0] is None:
385
+ return None
386
+
387
+ return (best[0].point, best[0].value)
388
+
389
+ def range_search(self, min_bounds: Tuple[float, ...],
390
+ max_bounds: Tuple[float, ...]) -> List[Tuple[Tuple[float, ...], Any]]:
391
+ """
392
+ Find all points within hyperrectangle.
393
+
394
+ Args:
395
+ min_bounds: Minimum bounds for each dimension
396
+ max_bounds: Maximum bounds for each dimension
397
+
398
+ Returns:
399
+ List of (point, value) tuples in range
400
+
401
+ Raises:
402
+ XWNodeValueError: If bounds are invalid
403
+ """
404
+ # Security: Validate bounds
405
+ min_b = self._validate_point(min_bounds)
406
+ max_b = self._validate_point(max_bounds)
407
+
408
+ for i in range(self.dimensions):
409
+ if min_b[i] > max_b[i]:
410
+ raise XWNodeValueError(
411
+ f"Invalid range: min[{i}]={min_b[i]} > max[{i}]={max_b[i]}"
412
+ )
413
+
414
+ result = []
415
+
416
+ def search_range(node: Optional[KdNode], depth: int) -> None:
417
+ """Recursive range search."""
418
+ if node is None:
419
+ return
420
+
421
+ # Check if point is in range
422
+ in_range = all(
423
+ min_b[i] <= node.point[i] <= max_b[i]
424
+ for i in range(self.dimensions)
425
+ )
426
+
427
+ if in_range:
428
+ result.append((node.point, node.value))
429
+
430
+ # Determine which subtrees to search
431
+ axis = depth % self.dimensions
432
+
433
+ # Search left if range overlaps left subtree
434
+ if min_b[axis] <= node.point[axis]:
435
+ search_range(node.left, depth + 1)
436
+
437
+ # Search right if range overlaps right subtree
438
+ if max_b[axis] >= node.point[axis]:
439
+ search_range(node.right, depth + 1)
440
+
441
+ search_range(self._root, 0)
442
+ return result
443
+
444
+ def k_nearest_neighbors(self, query_point: Tuple[float, ...], k: int,
445
+ distance_fn: Optional[Callable] = None) -> List[Tuple[Tuple[float, ...], Any, float]]:
446
+ """
447
+ Find k nearest neighbors.
448
+
449
+ Args:
450
+ query_point: Query coordinates
451
+ k: Number of neighbors
452
+ distance_fn: Distance function
453
+
454
+ Returns:
455
+ List of (point, value, distance) tuples
456
+
457
+ Raises:
458
+ XWNodeValueError: If query_point invalid or k < 1
459
+ """
460
+ if k < 1:
461
+ raise XWNodeValueError(f"k must be >= 1, got {k}")
462
+
463
+ query = self._validate_point(query_point)
464
+
465
+ if distance_fn is None:
466
+ distance_fn = self._euclidean_distance
467
+
468
+ # Priority queue of k nearest (max heap by distance)
469
+ nearest: List[Tuple[float, KdNode]] = []
470
+
471
+ def search_knn(node: Optional[KdNode], depth: int) -> None:
472
+ """Recursive k-NN search."""
473
+ if node is None:
474
+ return
475
+
476
+ dist = distance_fn(query, node.point)
477
+
478
+ # Add to nearest if:
479
+ # 1. We have < k neighbors, or
480
+ # 2. This is closer than farthest current neighbor
481
+ if len(nearest) < k:
482
+ nearest.append((dist, node))
483
+ nearest.sort(reverse=True) # Max heap
484
+ elif dist < nearest[0][0]:
485
+ nearest[0] = (dist, node)
486
+ nearest.sort(reverse=True)
487
+
488
+ # Determine search order
489
+ axis = depth % self.dimensions
490
+
491
+ if query[axis] < node.point[axis]:
492
+ near, far = node.left, node.right
493
+ else:
494
+ near, far = node.right, node.left
495
+
496
+ # Search near subtree
497
+ search_knn(near, depth + 1)
498
+
499
+ # Search far if necessary
500
+ axis_dist = abs(query[axis] - node.point[axis])
501
+ if len(nearest) < k or axis_dist < nearest[0][0]:
502
+ search_knn(far, depth + 1)
503
+
504
+ search_knn(self._root, 0)
505
+
506
+ # Convert to result format
507
+ return [(node.point, node.value, dist) for dist, node in reversed(nearest)]
508
+
509
+ # ============================================================================
510
+ # STANDARD OPERATIONS
511
+ # ============================================================================
512
+
513
+ def keys(self) -> Iterator[Any]:
514
+ """Get iterator over all points."""
515
+ yield from self._inorder_traversal(self._root)
516
+
517
+ def _inorder_traversal(self, node: Optional[KdNode]) -> Iterator[Tuple[float, ...]]:
518
+ """Inorder traversal."""
519
+ if node is None:
520
+ return
521
+
522
+ yield from self._inorder_traversal(node.left)
523
+ yield node.point
524
+ yield from self._inorder_traversal(node.right)
525
+
526
+ def values(self) -> Iterator[Any]:
527
+ """Get iterator over all values."""
528
+ for point in self.keys():
529
+ yield self._points[point]
530
+
531
+ def items(self) -> Iterator[tuple[Any, Any]]:
532
+ """Get iterator over point-value pairs."""
533
+ for point in self.keys():
534
+ yield (point, self._points[point])
535
+
536
+ def __len__(self) -> int:
537
+ """Get number of points."""
538
+ return self._size
539
+
540
+ def to_native(self) -> Any:
541
+ """Convert to native dict."""
542
+ return {point: value for point, value in self.items()}
543
+
544
+ # ============================================================================
545
+ # UTILITY METHODS
546
+ # ============================================================================
547
+
548
+ def clear(self) -> None:
549
+ """Clear all points."""
550
+ self._root = None
551
+ self._size = 0
552
+ self._points.clear()
553
+
554
+ def is_empty(self) -> bool:
555
+ """Check if empty."""
556
+ return self._size == 0
557
+
558
+ def size(self) -> int:
559
+ """Get number of points."""
560
+ return self._size
561
+
562
+ def get_mode(self) -> NodeMode:
563
+ """Get strategy mode."""
564
+ return self.mode
565
+
566
+ def get_traits(self) -> NodeTrait:
567
+ """Get strategy traits."""
568
+ return self.traits
569
+
570
+ def get_height(self) -> int:
571
+ """Get tree height."""
572
+ def height(node: Optional[KdNode]) -> int:
573
+ if node is None:
574
+ return 0
575
+ return 1 + max(height(node.left), height(node.right))
576
+
577
+ return height(self._root)
578
+
579
+ # ============================================================================
580
+ # STATISTICS
581
+ # ============================================================================
582
+
583
+ def get_statistics(self) -> Dict[str, Any]:
584
+ """
585
+ Get k-d tree statistics.
586
+
587
+ Returns:
588
+ Statistics dictionary
589
+ """
590
+ return {
591
+ 'size': self._size,
592
+ 'dimensions': self.dimensions,
593
+ 'height': self.get_height(),
594
+ 'optimal_height': math.ceil(math.log2(self._size + 1)) if self._size > 0 else 0,
595
+ 'balance_factor': self.get_height() / max(math.log2(self._size + 1), 1) if self._size > 0 else 1.0
596
+ }
597
+
598
+ # ============================================================================
599
+ # COMPATIBILITY METHODS
600
+ # ============================================================================
601
+
602
+ def find(self, key: Any) -> Optional[Any]:
603
+ """Find value by point."""
604
+ return self.get(key)
605
+
606
+ def insert(self, key: Any, value: Any = None) -> None:
607
+ """Insert point."""
608
+ self.put(key, value)
609
+
610
+ def __str__(self) -> str:
611
+ """String representation."""
612
+ return f"KdTreeStrategy(k={self.dimensions}, size={self._size}, height={self.get_height()})"
613
+
614
+ def __repr__(self) -> str:
615
+ """Detailed representation."""
616
+ return f"KdTreeStrategy(mode={self.mode.name}, k={self.dimensions}, size={self._size}, traits={self.traits})"
617
+
618
+ # ============================================================================
619
+ # FACTORY METHOD
620
+ # ============================================================================
621
+
622
+ @classmethod
623
+ def create_from_data(cls, data: Any, dimensions: int = 2) -> 'KdTreeStrategy':
624
+ """
625
+ Create k-d tree from data.
626
+
627
+ Args:
628
+ data: Dict with point tuples as keys or list of points
629
+ dimensions: Number of dimensions
630
+
631
+ Returns:
632
+ New KdTreeStrategy instance
633
+ """
634
+ instance = cls(dimensions=dimensions)
635
+
636
+ if isinstance(data, dict):
637
+ for key, value in data.items():
638
+ instance.put(key, value)
639
+ elif isinstance(data, (list, tuple)):
640
+ for item in data:
641
+ if isinstance(item, (tuple, list)):
642
+ if len(item) == dimensions + 1:
643
+ # Point with value
644
+ instance.put(tuple(item[:dimensions]), item[dimensions])
645
+ else:
646
+ # Just point
647
+ instance.put(tuple(item), None)
648
+ else:
649
+ raise XWNodeValueError(
650
+ "List items must be point tuples or point+value tuples"
651
+ )
652
+ else:
653
+ raise XWNodeValueError(
654
+ "Data must be dict with point keys or list of point tuples"
655
+ )
656
+
657
+ return instance
658
+
659
+ # ============================================================================
660
+ # BULK CONSTRUCTION (OPTIMAL)
661
+ # ============================================================================
662
+
663
+ def build_balanced(self, points: List[Tuple[Tuple[float, ...], Any]]) -> None:
664
+ """
665
+ Build balanced k-d tree from points using median splitting.
666
+
667
+ Args:
668
+ points: List of (point, value) tuples
669
+
670
+ WHY median splitting:
671
+ - Ensures balanced tree construction
672
+ - O(n log n) build time
673
+ - Optimal for static datasets
674
+ """
675
+ self.clear()
676
+
677
+ def build_recursive(point_list: List[Tuple[Tuple[float, ...], Any]],
678
+ depth: int) -> Optional[KdNode]:
679
+ """Recursively build balanced tree."""
680
+ if not point_list:
681
+ return None
682
+
683
+ axis = depth % self.dimensions
684
+
685
+ # Sort by current axis and find median
686
+ point_list.sort(key=lambda x: x[0][axis])
687
+ median = len(point_list) // 2
688
+
689
+ point, value = point_list[median]
690
+ node = KdNode(point, value, axis)
691
+
692
+ # Recursively build subtrees
693
+ node.left = build_recursive(point_list[:median], depth + 1)
694
+ node.right = build_recursive(point_list[median + 1:], depth + 1)
695
+
696
+ # Track in points dict
697
+ self._points[point] = value
698
+ self._size += 1
699
+
700
+ return node
701
+
702
+ self._root = build_recursive(points, 0)
703
+