exonware-xwnode 0.0.1.22__py3-none-any.whl → 0.0.1.24__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 +1 -1
- exonware/xwnode/__init__.py +18 -5
- exonware/xwnode/add_strategy_types.py +165 -0
- exonware/xwnode/common/__init__.py +1 -1
- exonware/xwnode/common/graph/__init__.py +30 -0
- exonware/xwnode/common/graph/caching.py +131 -0
- exonware/xwnode/common/graph/contracts.py +100 -0
- exonware/xwnode/common/graph/errors.py +44 -0
- exonware/xwnode/common/graph/indexing.py +260 -0
- exonware/xwnode/common/graph/manager.py +568 -0
- exonware/xwnode/common/management/__init__.py +3 -5
- exonware/xwnode/common/management/manager.py +2 -2
- exonware/xwnode/common/management/migration.py +3 -3
- exonware/xwnode/common/monitoring/__init__.py +3 -5
- exonware/xwnode/common/monitoring/metrics.py +6 -2
- exonware/xwnode/common/monitoring/pattern_detector.py +1 -1
- exonware/xwnode/common/monitoring/performance_monitor.py +5 -1
- exonware/xwnode/common/patterns/__init__.py +3 -5
- exonware/xwnode/common/patterns/flyweight.py +5 -1
- exonware/xwnode/common/patterns/registry.py +202 -183
- exonware/xwnode/common/utils/__init__.py +25 -11
- exonware/xwnode/common/utils/simple.py +1 -1
- exonware/xwnode/config.py +3 -8
- exonware/xwnode/contracts.py +4 -105
- exonware/xwnode/defs.py +413 -159
- exonware/xwnode/edges/strategies/__init__.py +86 -4
- exonware/xwnode/edges/strategies/_base_edge.py +2 -2
- exonware/xwnode/edges/strategies/adj_list.py +287 -121
- exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
- exonware/xwnode/edges/strategies/base.py +1 -1
- exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
- exonware/xwnode/edges/strategies/bitemporal.py +520 -0
- exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
- exonware/xwnode/edges/strategies/bv_graph.py +664 -0
- exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
- exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
- exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
- exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
- exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
- exonware/xwnode/edges/strategies/edge_list.py +168 -0
- exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
- exonware/xwnode/edges/strategies/euler_tour.py +560 -0
- exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
- exonware/xwnode/edges/strategies/graphblas.py +449 -0
- exonware/xwnode/edges/strategies/hnsw.py +637 -0
- exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
- exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
- exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
- exonware/xwnode/edges/strategies/k2_tree.py +613 -0
- exonware/xwnode/edges/strategies/link_cut.py +626 -0
- exonware/xwnode/edges/strategies/multiplex.py +532 -0
- exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
- exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
- exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
- exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
- exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
- exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
- exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
- exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
- exonware/xwnode/errors.py +3 -6
- exonware/xwnode/facade.py +20 -20
- exonware/xwnode/nodes/strategies/__init__.py +29 -9
- exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
- exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
- exonware/xwnode/nodes/strategies/array_list.py +36 -3
- exonware/xwnode/nodes/strategies/art.py +581 -0
- exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
- exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
- exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
- exonware/xwnode/nodes/strategies/base.py +469 -98
- exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
- exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
- exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
- exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
- exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
- exonware/xwnode/nodes/strategies/contracts.py +1 -1
- exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
- exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
- exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
- exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
- exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
- exonware/xwnode/nodes/strategies/dawg.py +876 -0
- exonware/xwnode/nodes/strategies/deque.py +321 -153
- exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
- exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
- exonware/xwnode/nodes/strategies/hamt.py +403 -0
- exonware/xwnode/nodes/strategies/hash_map.py +354 -67
- exonware/xwnode/nodes/strategies/heap.py +105 -5
- exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
- exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
- exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
- exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
- exonware/xwnode/nodes/strategies/learned_index.py +533 -0
- exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
- exonware/xwnode/nodes/strategies/linked_list.py +316 -119
- exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
- exonware/xwnode/nodes/strategies/masstree.py +130 -0
- exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
- exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
- exonware/xwnode/nodes/strategies/queue.py +249 -120
- exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
- exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
- exonware/xwnode/nodes/strategies/rope.py +717 -0
- exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
- exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
- exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
- exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
- exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
- exonware/xwnode/nodes/strategies/stack.py +244 -112
- exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
- exonware/xwnode/nodes/strategies/t_tree.py +94 -0
- exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
- exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
- exonware/xwnode/nodes/strategies/trie.py +153 -9
- exonware/xwnode/nodes/strategies/union_find.py +111 -5
- exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
- exonware/xwnode/strategies/__init__.py +5 -51
- exonware/xwnode/version.py +3 -3
- exonware_xwnode-0.0.1.24.dist-info/METADATA +900 -0
- exonware_xwnode-0.0.1.24.dist-info/RECORD +130 -0
- exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
- exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
- exonware/xwnode/nodes/strategies/_base_node.py +0 -307
- exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
- exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
- exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
- exonware/xwnode/nodes/strategies/node_heap.py +0 -196
- exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
- exonware/xwnode/nodes/strategies/node_trie.py +0 -257
- exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
- exonware/xwnode/queries/executors/__init__.py +0 -47
- exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
- exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
- exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
- exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
- exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
- exonware/xwnode/queries/executors/array/__init__.py +0 -9
- exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
- exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
- exonware/xwnode/queries/executors/base.py +0 -257
- exonware/xwnode/queries/executors/capability_checker.py +0 -204
- exonware/xwnode/queries/executors/contracts.py +0 -166
- exonware/xwnode/queries/executors/core/__init__.py +0 -17
- exonware/xwnode/queries/executors/core/create_executor.py +0 -96
- exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
- exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
- exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
- exonware/xwnode/queries/executors/core/select_executor.py +0 -152
- exonware/xwnode/queries/executors/core/update_executor.py +0 -102
- exonware/xwnode/queries/executors/data/__init__.py +0 -13
- exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
- exonware/xwnode/queries/executors/data/load_executor.py +0 -50
- exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
- exonware/xwnode/queries/executors/data/store_executor.py +0 -50
- exonware/xwnode/queries/executors/defs.py +0 -93
- exonware/xwnode/queries/executors/engine.py +0 -221
- exonware/xwnode/queries/executors/errors.py +0 -68
- exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
- exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
- exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
- exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
- exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
- exonware/xwnode/queries/executors/graph/__init__.py +0 -15
- exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
- exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
- exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
- exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
- exonware/xwnode/queries/executors/projection/__init__.py +0 -9
- exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
- exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
- exonware/xwnode/queries/executors/registry.py +0 -173
- exonware/xwnode/queries/parsers/__init__.py +0 -26
- exonware/xwnode/queries/parsers/base.py +0 -86
- exonware/xwnode/queries/parsers/contracts.py +0 -46
- exonware/xwnode/queries/parsers/errors.py +0 -53
- exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
- exonware/xwnode/queries/strategies/__init__.py +0 -24
- exonware/xwnode/queries/strategies/base.py +0 -236
- exonware/xwnode/queries/strategies/cql.py +0 -201
- exonware/xwnode/queries/strategies/cypher.py +0 -181
- exonware/xwnode/queries/strategies/datalog.py +0 -70
- exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
- exonware/xwnode/queries/strategies/eql.py +0 -70
- exonware/xwnode/queries/strategies/flux.py +0 -70
- exonware/xwnode/queries/strategies/gql.py +0 -70
- exonware/xwnode/queries/strategies/graphql.py +0 -240
- exonware/xwnode/queries/strategies/gremlin.py +0 -181
- exonware/xwnode/queries/strategies/hiveql.py +0 -214
- exonware/xwnode/queries/strategies/hql.py +0 -70
- exonware/xwnode/queries/strategies/jmespath.py +0 -219
- exonware/xwnode/queries/strategies/jq.py +0 -66
- exonware/xwnode/queries/strategies/json_query.py +0 -66
- exonware/xwnode/queries/strategies/jsoniq.py +0 -248
- exonware/xwnode/queries/strategies/kql.py +0 -70
- exonware/xwnode/queries/strategies/linq.py +0 -238
- exonware/xwnode/queries/strategies/logql.py +0 -70
- exonware/xwnode/queries/strategies/mql.py +0 -68
- exonware/xwnode/queries/strategies/n1ql.py +0 -210
- exonware/xwnode/queries/strategies/partiql.py +0 -70
- exonware/xwnode/queries/strategies/pig.py +0 -215
- exonware/xwnode/queries/strategies/promql.py +0 -70
- exonware/xwnode/queries/strategies/sparql.py +0 -220
- exonware/xwnode/queries/strategies/sql.py +0 -275
- exonware/xwnode/queries/strategies/xml_query.py +0 -66
- exonware/xwnode/queries/strategies/xpath.py +0 -223
- exonware/xwnode/queries/strategies/xquery.py +0 -258
- exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
- exonware/xwnode/queries/strategies/xwquery.py +0 -456
- exonware_xwnode-0.0.1.22.dist-info/METADATA +0 -168
- exonware_xwnode-0.0.1.22.dist-info/RECORD +0 -214
- /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.24.dist-info}/WHEEL +0 -0
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.24.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.24
|
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
|
+
|