kdtree 0.17__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.
- kdtree-0.17.dist-info/METADATA +41 -0
- kdtree-0.17.dist-info/RECORD +7 -0
- kdtree-0.17.dist-info/WHEEL +5 -0
- kdtree-0.17.dist-info/licenses/AUTHORS +2 -0
- kdtree-0.17.dist-info/licenses/LICENSE +14 -0
- kdtree-0.17.dist-info/top_level.txt +1 -0
- kdtree.py +739 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kdtree
|
|
3
|
+
Version: 0.17
|
|
4
|
+
Summary: A Python implemntation of a kd-tree
|
|
5
|
+
Home-page: https://github.com/stefankoegl/kdtree
|
|
6
|
+
Download-URL: http://pypi.python.org/packages/source/k/kdtree/kdtree-0.17.tar.gz
|
|
7
|
+
Author: Stefan Kögl
|
|
8
|
+
Author-email: stefan@skoegl.net
|
|
9
|
+
License: ISC license
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
License-File: AUTHORS
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: download-url
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+
This package provides a simple implementation of a kd-tree in Python.
|
|
41
|
+
https://en.wikipedia.org/wiki/K-d_tree
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
kdtree.py,sha256=sBi-07Bjs2CFCKV41Y3ov93urcIp2HQdDUsD7XmLBdc,21646
|
|
2
|
+
kdtree-0.17.dist-info/licenses/AUTHORS,sha256=EQwTKAN11v6bFyYqOKAUJmXAg17_AJTiaAawWn99FKk,64
|
|
3
|
+
kdtree-0.17.dist-info/licenses/LICENSE,sha256=4ERmqqlC4WM1msAMITP-Fx59IcmzJMCvnSBX5qUhjaw,746
|
|
4
|
+
kdtree-0.17.dist-info/METADATA,sha256=UEVlWgFYNIl9PtKNzozPebdNhOHeXUrUad_lPG3Kc0c,1509
|
|
5
|
+
kdtree-0.17.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
kdtree-0.17.dist-info/top_level.txt,sha256=ZSOSvfNz-j937kN4qixyfSsq4DOgY_Azho2wQbxLB5Y,7
|
|
7
|
+
kdtree-0.17.dist-info/RECORD,,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
Copyright (c) Stefan Kögl <stefan@skoegl.net>
|
|
3
|
+
|
|
4
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
5
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
6
|
+
copyright notice and this permission notice appear in all copies.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
10
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kdtree
|
kdtree.py
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""A Python implemntation of a kd-tree
|
|
5
|
+
|
|
6
|
+
This package provides a simple implementation of a kd-tree in Python.
|
|
7
|
+
https://en.wikipedia.org/wiki/K-d_tree
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import print_function
|
|
11
|
+
|
|
12
|
+
import heapq
|
|
13
|
+
import itertools
|
|
14
|
+
import operator
|
|
15
|
+
import math
|
|
16
|
+
from collections import deque
|
|
17
|
+
from functools import wraps
|
|
18
|
+
|
|
19
|
+
__author__ = u'Stefan Kögl <stefan@skoegl.net>'
|
|
20
|
+
__version__ = '0.17'
|
|
21
|
+
__website__ = 'https://github.com/stefankoegl/kdtree'
|
|
22
|
+
__license__ = 'ISC license'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Node(object):
|
|
26
|
+
""" A Node in a kd-tree
|
|
27
|
+
|
|
28
|
+
A tree is represented by its root node, and every node represents
|
|
29
|
+
its subtree"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, data=None, left=None, right=None):
|
|
32
|
+
self.data = data
|
|
33
|
+
self.left = left
|
|
34
|
+
self.right = right
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_leaf(self):
|
|
39
|
+
""" Returns True if a Node has no subnodes
|
|
40
|
+
|
|
41
|
+
>>> Node().is_leaf
|
|
42
|
+
True
|
|
43
|
+
|
|
44
|
+
>>> Node( 1, left=Node(2) ).is_leaf
|
|
45
|
+
False
|
|
46
|
+
"""
|
|
47
|
+
return (not self.data) or \
|
|
48
|
+
(all(not bool(c) for c, p in self.children))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def preorder(self):
|
|
52
|
+
""" iterator for nodes: root, left, right """
|
|
53
|
+
|
|
54
|
+
if not self:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
yield self
|
|
58
|
+
|
|
59
|
+
if self.left:
|
|
60
|
+
for x in self.left.preorder():
|
|
61
|
+
yield x
|
|
62
|
+
|
|
63
|
+
if self.right:
|
|
64
|
+
for x in self.right.preorder():
|
|
65
|
+
yield x
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def inorder(self):
|
|
69
|
+
""" iterator for nodes: left, root, right """
|
|
70
|
+
|
|
71
|
+
if not self:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if self.left:
|
|
75
|
+
for x in self.left.inorder():
|
|
76
|
+
yield x
|
|
77
|
+
|
|
78
|
+
yield self
|
|
79
|
+
|
|
80
|
+
if self.right:
|
|
81
|
+
for x in self.right.inorder():
|
|
82
|
+
yield x
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def postorder(self):
|
|
86
|
+
""" iterator for nodes: left, right, root """
|
|
87
|
+
|
|
88
|
+
if not self:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if self.left:
|
|
92
|
+
for x in self.left.postorder():
|
|
93
|
+
yield x
|
|
94
|
+
|
|
95
|
+
if self.right:
|
|
96
|
+
for x in self.right.postorder():
|
|
97
|
+
yield x
|
|
98
|
+
|
|
99
|
+
yield self
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def children(self):
|
|
104
|
+
"""
|
|
105
|
+
Returns an iterator for the non-empty children of the Node
|
|
106
|
+
|
|
107
|
+
The children are returned as (Node, pos) tuples where pos is 0 for the
|
|
108
|
+
left subnode and 1 for the right.
|
|
109
|
+
|
|
110
|
+
>>> len(list(create(dimensions=2).children))
|
|
111
|
+
0
|
|
112
|
+
|
|
113
|
+
>>> len(list(create([ (1, 2) ]).children))
|
|
114
|
+
0
|
|
115
|
+
|
|
116
|
+
>>> len(list(create([ (2, 2), (2, 1), (2, 3) ]).children))
|
|
117
|
+
2
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
if self.left and self.left.data is not None:
|
|
121
|
+
yield self.left, 0
|
|
122
|
+
if self.right and self.right.data is not None:
|
|
123
|
+
yield self.right, 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def set_child(self, index, child):
|
|
127
|
+
""" Sets one of the node's children
|
|
128
|
+
|
|
129
|
+
index 0 refers to the left, 1 to the right child """
|
|
130
|
+
|
|
131
|
+
if index == 0:
|
|
132
|
+
self.left = child
|
|
133
|
+
else:
|
|
134
|
+
self.right = child
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def height(self):
|
|
138
|
+
"""
|
|
139
|
+
Returns height of the (sub)tree, without considering
|
|
140
|
+
empty leaf-nodes
|
|
141
|
+
|
|
142
|
+
>>> create(dimensions=2).height()
|
|
143
|
+
0
|
|
144
|
+
|
|
145
|
+
>>> create([ (1, 2) ]).height()
|
|
146
|
+
1
|
|
147
|
+
|
|
148
|
+
>>> create([ (1, 2), (2, 3) ]).height()
|
|
149
|
+
2
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
min_height = int(bool(self))
|
|
153
|
+
return max([min_height] + [c.height()+1 for c, p in self.children])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_child_pos(self, child):
|
|
157
|
+
""" Returns the position if the given child
|
|
158
|
+
|
|
159
|
+
If the given node is the left child, 0 is returned. If its the right
|
|
160
|
+
child, 1 is returned. Otherwise None """
|
|
161
|
+
|
|
162
|
+
for c, pos in self.children:
|
|
163
|
+
if child == c:
|
|
164
|
+
return pos
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def __repr__(self):
|
|
168
|
+
return '<%(cls)s - %(data)s>' % \
|
|
169
|
+
dict(cls=self.__class__.__name__, data=repr(self.data))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def __nonzero__(self):
|
|
173
|
+
return self.data is not None
|
|
174
|
+
|
|
175
|
+
__bool__ = __nonzero__
|
|
176
|
+
|
|
177
|
+
def __eq__(self, other):
|
|
178
|
+
if isinstance(other, tuple):
|
|
179
|
+
return self.data == other
|
|
180
|
+
else:
|
|
181
|
+
return self.data == other.data
|
|
182
|
+
|
|
183
|
+
def __hash__(self):
|
|
184
|
+
return id(self)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def require_axis(f):
|
|
188
|
+
""" Check if the object of the function has axis and sel_axis members """
|
|
189
|
+
|
|
190
|
+
@wraps(f)
|
|
191
|
+
def _wrapper(self, *args, **kwargs):
|
|
192
|
+
if None in (self.axis, self.sel_axis):
|
|
193
|
+
raise ValueError('%(func_name) requires the node %(node)s '
|
|
194
|
+
'to have an axis and a sel_axis function' %
|
|
195
|
+
dict(func_name=f.__name__, node=repr(self)))
|
|
196
|
+
|
|
197
|
+
return f(self, *args, **kwargs)
|
|
198
|
+
|
|
199
|
+
return _wrapper
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class KDNode(Node):
|
|
204
|
+
""" A Node that contains kd-tree specific data and methods """
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def __init__(self, data=None, left=None, right=None, axis=None,
|
|
208
|
+
sel_axis=None, dimensions=None):
|
|
209
|
+
""" Creates a new node for a kd-tree
|
|
210
|
+
|
|
211
|
+
If the node will be used within a tree, the axis and the sel_axis
|
|
212
|
+
function should be supplied.
|
|
213
|
+
|
|
214
|
+
sel_axis(axis) is used when creating subnodes of the current node. It
|
|
215
|
+
receives the axis of the parent node and returns the axis of the child
|
|
216
|
+
node. """
|
|
217
|
+
super(KDNode, self).__init__(data, left, right)
|
|
218
|
+
self.axis = axis
|
|
219
|
+
self.sel_axis = sel_axis
|
|
220
|
+
self.dimensions = dimensions
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@require_axis
|
|
224
|
+
def add(self, point):
|
|
225
|
+
"""
|
|
226
|
+
Adds a point to the current node or iteratively
|
|
227
|
+
descends to one of its children.
|
|
228
|
+
|
|
229
|
+
Users should call add() only to the topmost tree.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
current = self
|
|
233
|
+
while True:
|
|
234
|
+
check_dimensionality([point], dimensions=current.dimensions)
|
|
235
|
+
|
|
236
|
+
# Adding has hit an empty leaf-node, add here
|
|
237
|
+
if current.data is None:
|
|
238
|
+
current.data = point
|
|
239
|
+
return current
|
|
240
|
+
|
|
241
|
+
# split on self.axis, recurse either left or right
|
|
242
|
+
if point[current.axis] < current.data[current.axis]:
|
|
243
|
+
if current.left is None:
|
|
244
|
+
current.left = current.create_subnode(point)
|
|
245
|
+
return current.left
|
|
246
|
+
else:
|
|
247
|
+
current = current.left
|
|
248
|
+
else:
|
|
249
|
+
if current.right is None:
|
|
250
|
+
current.right = current.create_subnode(point)
|
|
251
|
+
return current.right
|
|
252
|
+
else:
|
|
253
|
+
current = current.right
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@require_axis
|
|
257
|
+
def create_subnode(self, data):
|
|
258
|
+
""" Creates a subnode for the current node """
|
|
259
|
+
|
|
260
|
+
return self.__class__(data,
|
|
261
|
+
axis=self.sel_axis(self.axis),
|
|
262
|
+
sel_axis=self.sel_axis,
|
|
263
|
+
dimensions=self.dimensions)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@require_axis
|
|
267
|
+
def find_replacement(self):
|
|
268
|
+
""" Finds a replacement for the current node
|
|
269
|
+
|
|
270
|
+
The replacement is returned as a
|
|
271
|
+
(replacement-node, replacements-parent-node) tuple """
|
|
272
|
+
|
|
273
|
+
if self.right:
|
|
274
|
+
child, parent = self.right.extreme_child(min, self.axis)
|
|
275
|
+
else:
|
|
276
|
+
child, parent = self.left.extreme_child(max, self.axis)
|
|
277
|
+
|
|
278
|
+
return (child, parent if parent is not None else self)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def should_remove(self, point, node):
|
|
282
|
+
""" checks if self's point (and maybe identity) matches """
|
|
283
|
+
if not self.data == point:
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
return (node is None) or (node is self)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@require_axis
|
|
290
|
+
def remove(self, point, node=None):
|
|
291
|
+
""" Removes the node with the given point from the tree
|
|
292
|
+
|
|
293
|
+
Returns the new root node of the (sub)tree.
|
|
294
|
+
|
|
295
|
+
If there are multiple points matching "point", only one is removed. The
|
|
296
|
+
optional "node" parameter is used for checking the identity, once the
|
|
297
|
+
removeal candidate is decided."""
|
|
298
|
+
|
|
299
|
+
# Recursion has reached an empty leaf node, nothing here to delete
|
|
300
|
+
if not self:
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
# Recursion has reached the node to be deleted
|
|
304
|
+
if self.should_remove(point, node):
|
|
305
|
+
return self._remove(point)
|
|
306
|
+
|
|
307
|
+
# Remove direct subnode
|
|
308
|
+
if self.left and self.left.should_remove(point, node):
|
|
309
|
+
self.left = self.left._remove(point)
|
|
310
|
+
|
|
311
|
+
elif self.right and self.right.should_remove(point, node):
|
|
312
|
+
self.right = self.right._remove(point)
|
|
313
|
+
|
|
314
|
+
# Recurse to subtrees
|
|
315
|
+
if point[self.axis] <= self.data[self.axis]:
|
|
316
|
+
if self.left:
|
|
317
|
+
self.left = self.left.remove(point, node)
|
|
318
|
+
|
|
319
|
+
if point[self.axis] >= self.data[self.axis]:
|
|
320
|
+
if self.right:
|
|
321
|
+
self.right = self.right.remove(point, node)
|
|
322
|
+
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@require_axis
|
|
327
|
+
def _remove(self, point):
|
|
328
|
+
# we have reached the node to be deleted here
|
|
329
|
+
|
|
330
|
+
# deleting a leaf node is trivial
|
|
331
|
+
if self.is_leaf:
|
|
332
|
+
self.data = None
|
|
333
|
+
return self
|
|
334
|
+
|
|
335
|
+
# we have to delete a non-leaf node here
|
|
336
|
+
|
|
337
|
+
# find a replacement for the node (will be the new subtree-root)
|
|
338
|
+
root, max_p = self.find_replacement()
|
|
339
|
+
|
|
340
|
+
# self and root swap positions
|
|
341
|
+
tmp_l, tmp_r = self.left, self.right
|
|
342
|
+
self.left, self.right = root.left, root.right
|
|
343
|
+
root.left, root.right = tmp_l if tmp_l is not root else self, tmp_r if tmp_r is not root else self
|
|
344
|
+
self.axis, root.axis = root.axis, self.axis
|
|
345
|
+
|
|
346
|
+
# Special-case if we have not chosen a direct child as the replacement
|
|
347
|
+
if max_p is not self:
|
|
348
|
+
pos = max_p.get_child_pos(root)
|
|
349
|
+
max_p.set_child(pos, self)
|
|
350
|
+
max_p.remove(point, self)
|
|
351
|
+
|
|
352
|
+
else:
|
|
353
|
+
root.remove(point, self)
|
|
354
|
+
|
|
355
|
+
return root
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def is_balanced(self):
|
|
360
|
+
""" Returns True if the (sub)tree is balanced
|
|
361
|
+
|
|
362
|
+
The tree is balanced if the heights of both subtrees differ at most by
|
|
363
|
+
1 """
|
|
364
|
+
|
|
365
|
+
left_height = self.left.height() if self.left else 0
|
|
366
|
+
right_height = self.right.height() if self.right else 0
|
|
367
|
+
|
|
368
|
+
if abs(left_height - right_height) > 1:
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
return all(c.is_balanced for c, _ in self.children)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def rebalance(self):
|
|
375
|
+
"""
|
|
376
|
+
Returns the (possibly new) root of the rebalanced tree
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
return create([x.data for x in self.inorder()])
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def axis_dist(self, point, axis):
|
|
383
|
+
"""
|
|
384
|
+
Squared distance at the given axis between
|
|
385
|
+
the current Node and the given point
|
|
386
|
+
"""
|
|
387
|
+
return math.pow(self.data[axis] - point[axis], 2)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def dist(self, point, axis=None):
|
|
391
|
+
"""
|
|
392
|
+
Squared distance between the current Node
|
|
393
|
+
and the given point
|
|
394
|
+
"""
|
|
395
|
+
if axis is None:
|
|
396
|
+
axes = range(self.dimensions)
|
|
397
|
+
else:
|
|
398
|
+
axes = [axis]
|
|
399
|
+
|
|
400
|
+
return sum([self.axis_dist(point, i) for i in axes])
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def search_knn(self, point, k, dist=None):
|
|
404
|
+
""" Return the k nearest neighbors of point and their distances
|
|
405
|
+
|
|
406
|
+
point must be an actual point, not a node.
|
|
407
|
+
|
|
408
|
+
k is the number of results to return. The actual results can be less
|
|
409
|
+
(if there aren't more nodes to return) or more in case of equal
|
|
410
|
+
distances.
|
|
411
|
+
|
|
412
|
+
dist is a distance function, expecting two points and returning a
|
|
413
|
+
distance value. dist should expect an optional `axis` parameter. If
|
|
414
|
+
given, the distance on the specified axis should be calculated.
|
|
415
|
+
Distance values must support comparison and unary negation.
|
|
416
|
+
|
|
417
|
+
The result is an ordered list of (node, distance) tuples.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
if k < 1:
|
|
421
|
+
raise ValueError("k must be greater than 0.")
|
|
422
|
+
|
|
423
|
+
def get_dist(n, axis=None):
|
|
424
|
+
if dist is None:
|
|
425
|
+
return n.dist(point, axis=axis)
|
|
426
|
+
else:
|
|
427
|
+
return dist(n.data, point, axis=axis)
|
|
428
|
+
|
|
429
|
+
results = []
|
|
430
|
+
|
|
431
|
+
self._search_node(point, k, results, get_dist, itertools.count(),
|
|
432
|
+
prune=dist is None)
|
|
433
|
+
|
|
434
|
+
# We sort the final result by the distance in the tuple
|
|
435
|
+
# (<KdNode>, distance).
|
|
436
|
+
return [(node, -d) for d, _, node in sorted(results, reverse=True)]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _search_node(self, point, k, results, get_dist, counter, prune=True):
|
|
440
|
+
if not self:
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
nodeDist = get_dist(self)
|
|
444
|
+
|
|
445
|
+
# Add current node to the priority queue if it closer than
|
|
446
|
+
# at least one point in the queue.
|
|
447
|
+
#
|
|
448
|
+
# If the heap is at its capacity, we need to check if the
|
|
449
|
+
# current node is closer than the current farthest node, and if
|
|
450
|
+
# so, replace it.
|
|
451
|
+
item = (-nodeDist, next(counter), self)
|
|
452
|
+
if len(results) >= k:
|
|
453
|
+
if -nodeDist > results[0][0]:
|
|
454
|
+
heapq.heapreplace(results, item)
|
|
455
|
+
else:
|
|
456
|
+
heapq.heappush(results, item)
|
|
457
|
+
|
|
458
|
+
# get the splitting plane
|
|
459
|
+
split_plane = self.data[self.axis]
|
|
460
|
+
# get the distance between the point and the splitting plane
|
|
461
|
+
plane_dist = get_dist(self, axis=self.axis)
|
|
462
|
+
|
|
463
|
+
# Search the side of the splitting plane that the point is in
|
|
464
|
+
if point[self.axis] < split_plane:
|
|
465
|
+
if self.left is not None:
|
|
466
|
+
self.left._search_node(point, k, results, get_dist, counter,
|
|
467
|
+
prune=prune)
|
|
468
|
+
else:
|
|
469
|
+
if self.right is not None:
|
|
470
|
+
self.right._search_node(point, k, results, get_dist, counter,
|
|
471
|
+
prune=prune)
|
|
472
|
+
|
|
473
|
+
# Search the other side of the splitting plane if it may contain
|
|
474
|
+
# points closer than the farthest point in the current results.
|
|
475
|
+
if not prune or -plane_dist > results[0][0] or len(results) < k:
|
|
476
|
+
if point[self.axis] < self.data[self.axis]:
|
|
477
|
+
if self.right is not None:
|
|
478
|
+
self.right._search_node(point, k, results, get_dist,
|
|
479
|
+
counter, prune=prune)
|
|
480
|
+
else:
|
|
481
|
+
if self.left is not None:
|
|
482
|
+
self.left._search_node(point, k, results, get_dist,
|
|
483
|
+
counter, prune=prune)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@require_axis
|
|
487
|
+
def search_nn(self, point, dist=None):
|
|
488
|
+
"""
|
|
489
|
+
Search the nearest node of the given point
|
|
490
|
+
|
|
491
|
+
point must be an actual point, not a node. The nearest node to the
|
|
492
|
+
point is returned. If a location of an actual node is used, the Node
|
|
493
|
+
with this location will be returned (not its neighbor).
|
|
494
|
+
|
|
495
|
+
dist is a distance function, expecting two points and returning a
|
|
496
|
+
distance value. dist should expect an optional `axis` parameter. If
|
|
497
|
+
given, the distance on the specified axis should be calculated.
|
|
498
|
+
Distance values must support comparison and unary negation.
|
|
499
|
+
|
|
500
|
+
The result is a (node, distance) tuple.
|
|
501
|
+
"""
|
|
502
|
+
if not self:
|
|
503
|
+
raise ValueError("tree is empty")
|
|
504
|
+
|
|
505
|
+
return next(iter(self.search_knn(point, 1, dist)), None)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _search_nn_dist(self, point, dist, results, get_dist):
|
|
509
|
+
if not self:
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
nodeDist = get_dist(self)
|
|
513
|
+
|
|
514
|
+
if nodeDist < dist:
|
|
515
|
+
results.append(self.data)
|
|
516
|
+
|
|
517
|
+
# get the splitting plane
|
|
518
|
+
split_plane = self.data[self.axis]
|
|
519
|
+
|
|
520
|
+
# Search the side of the splitting plane that the point is in
|
|
521
|
+
if point[self.axis] <= split_plane + dist:
|
|
522
|
+
if self.left is not None:
|
|
523
|
+
self.left._search_nn_dist(point, dist, results, get_dist)
|
|
524
|
+
if point[self.axis] >= split_plane - dist:
|
|
525
|
+
if self.right is not None:
|
|
526
|
+
self.right._search_nn_dist(point, dist, results, get_dist)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@require_axis
|
|
530
|
+
def search_nn_dist(self, point, distance):
|
|
531
|
+
"""
|
|
532
|
+
Search the n nearest nodes of the given point which are within given
|
|
533
|
+
distance
|
|
534
|
+
|
|
535
|
+
point must be a location, not a node. A list containing the n nearest
|
|
536
|
+
nodes to the point within the distance will be returned.
|
|
537
|
+
|
|
538
|
+
distance is the threshold distance to filter nodes.
|
|
539
|
+
Note: KDNode.dist() returns squared distance, so the comparison will
|
|
540
|
+
be done using squared_distance < distance.
|
|
541
|
+
"""
|
|
542
|
+
|
|
543
|
+
results = []
|
|
544
|
+
get_dist = lambda n: n.dist(point)
|
|
545
|
+
|
|
546
|
+
self._search_nn_dist(point, distance, results, get_dist)
|
|
547
|
+
return results
|
|
548
|
+
|
|
549
|
+
@require_axis
|
|
550
|
+
def range_search(self, top_left, bottom_right):
|
|
551
|
+
"""
|
|
552
|
+
Search all nodes in or on the hyperrectangle defined by the points top_left and bottom_right.
|
|
553
|
+
|
|
554
|
+
Note: the list returned is not in any particular order.
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
invalid_rect = False
|
|
558
|
+
if len(top_left) != len(bottom_right) or len(top_left) != self.dimensions:
|
|
559
|
+
invalid_rect = True
|
|
560
|
+
else:
|
|
561
|
+
for i in range(len(top_left)):
|
|
562
|
+
if top_left[i] > bottom_right[i]:
|
|
563
|
+
invalid_rect = True
|
|
564
|
+
break
|
|
565
|
+
|
|
566
|
+
if invalid_rect:
|
|
567
|
+
raise ValueError("invalid rectangle")
|
|
568
|
+
|
|
569
|
+
results = []
|
|
570
|
+
stack = [self]
|
|
571
|
+
|
|
572
|
+
while(stack):
|
|
573
|
+
node = stack.pop()
|
|
574
|
+
|
|
575
|
+
data = node.data
|
|
576
|
+
axis = node.axis
|
|
577
|
+
|
|
578
|
+
if data is None:
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
in_range = True
|
|
582
|
+
for d in range(0, len(data)):
|
|
583
|
+
in_range &= (data[d] >= top_left[d]) and (data[d] <= bottom_right[d])
|
|
584
|
+
|
|
585
|
+
if in_range:
|
|
586
|
+
results.append(node)
|
|
587
|
+
|
|
588
|
+
if data[axis] >= top_left[axis] and node.left:
|
|
589
|
+
stack.append(node.left)
|
|
590
|
+
if data[axis] <= bottom_right[axis] and node.right:
|
|
591
|
+
stack.append(node.right)
|
|
592
|
+
|
|
593
|
+
return results
|
|
594
|
+
|
|
595
|
+
@require_axis
|
|
596
|
+
def is_valid(self):
|
|
597
|
+
""" Checks recursively if the tree is valid
|
|
598
|
+
|
|
599
|
+
It is valid if each node splits correctly """
|
|
600
|
+
|
|
601
|
+
if not self:
|
|
602
|
+
return True
|
|
603
|
+
|
|
604
|
+
if self.left and self.data[self.axis] < self.left.data[self.axis]:
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
if self.right and self.data[self.axis] > self.right.data[self.axis]:
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
return all(c.is_valid() for c, _ in self.children) or self.is_leaf
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def extreme_child(self, sel_func, axis):
|
|
614
|
+
""" Returns a child of the subtree and its parent
|
|
615
|
+
|
|
616
|
+
The child is selected by sel_func which is either min or max
|
|
617
|
+
(or a different function with similar semantics). """
|
|
618
|
+
|
|
619
|
+
max_key = lambda child_parent: child_parent[0].data[axis]
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# we don't know our parent, so we include None
|
|
623
|
+
me = [(self, None)] if self else []
|
|
624
|
+
|
|
625
|
+
child_max = [c.extreme_child(sel_func, axis) for c, _ in self.children]
|
|
626
|
+
# insert self for unknown parents
|
|
627
|
+
child_max = [(c, p if p is not None else self) for c, p in child_max]
|
|
628
|
+
|
|
629
|
+
candidates = me + child_max
|
|
630
|
+
|
|
631
|
+
if not candidates:
|
|
632
|
+
return None, None
|
|
633
|
+
|
|
634
|
+
return sel_func(candidates, key=max_key)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def create(point_list=None, dimensions=None, axis=0, sel_axis=None):
|
|
639
|
+
""" Creates a kd-tree from a list of points
|
|
640
|
+
|
|
641
|
+
All points in the list must be of the same dimensionality.
|
|
642
|
+
|
|
643
|
+
If no point_list is given, an empty tree is created. The number of
|
|
644
|
+
dimensions has to be given instead.
|
|
645
|
+
|
|
646
|
+
If both a point_list and dimensions are given, the numbers must agree.
|
|
647
|
+
|
|
648
|
+
Axis is the axis on which the root-node should split.
|
|
649
|
+
|
|
650
|
+
sel_axis(axis) is used when creating subnodes of a node. It receives the
|
|
651
|
+
axis of the parent node and returns the axis of the child node. """
|
|
652
|
+
|
|
653
|
+
if not point_list and not dimensions:
|
|
654
|
+
raise ValueError('either point_list or dimensions must be provided')
|
|
655
|
+
|
|
656
|
+
elif point_list:
|
|
657
|
+
dimensions = check_dimensionality(point_list, dimensions)
|
|
658
|
+
|
|
659
|
+
# by default cycle through the axis
|
|
660
|
+
sel_axis = sel_axis or (lambda prev_axis: (prev_axis+1) % dimensions)
|
|
661
|
+
|
|
662
|
+
if not point_list:
|
|
663
|
+
return KDNode(sel_axis=sel_axis, axis=axis, dimensions=dimensions)
|
|
664
|
+
|
|
665
|
+
# Sort point list and choose median as pivot element
|
|
666
|
+
point_list = list(point_list)
|
|
667
|
+
point_list.sort(key=lambda point: point[axis])
|
|
668
|
+
median = len(point_list) // 2
|
|
669
|
+
|
|
670
|
+
loc = point_list[median]
|
|
671
|
+
left = create(point_list[:median], dimensions, sel_axis(axis))
|
|
672
|
+
right = create(point_list[median + 1:], dimensions, sel_axis(axis))
|
|
673
|
+
return KDNode(loc, left, right, axis=axis, sel_axis=sel_axis, dimensions=dimensions)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def check_dimensionality(point_list, dimensions=None):
|
|
677
|
+
dimensions = dimensions or len(point_list[0])
|
|
678
|
+
for p in point_list:
|
|
679
|
+
if len(p) != dimensions:
|
|
680
|
+
raise ValueError('All Points in the point_list must have the same dimensionality')
|
|
681
|
+
|
|
682
|
+
return dimensions
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def level_order(tree, include_all=False):
|
|
687
|
+
""" Returns an iterator over the tree in level-order
|
|
688
|
+
|
|
689
|
+
If include_all is set to True, empty parts of the tree are filled
|
|
690
|
+
with dummy entries and the iterator becomes infinite. """
|
|
691
|
+
|
|
692
|
+
q = deque()
|
|
693
|
+
q.append(tree)
|
|
694
|
+
while q:
|
|
695
|
+
node = q.popleft()
|
|
696
|
+
yield node
|
|
697
|
+
|
|
698
|
+
if include_all or node.left:
|
|
699
|
+
q.append(node.left or node.__class__())
|
|
700
|
+
|
|
701
|
+
if include_all or node.right:
|
|
702
|
+
q.append(node.right or node.__class__())
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def visualize(tree, max_level=100, node_width=10, left_padding=5):
|
|
707
|
+
""" Prints the tree to stdout """
|
|
708
|
+
|
|
709
|
+
height = min(max_level, tree.height()-1)
|
|
710
|
+
max_width = pow(2, height)
|
|
711
|
+
|
|
712
|
+
per_level = 1
|
|
713
|
+
in_level = 0
|
|
714
|
+
level = 0
|
|
715
|
+
|
|
716
|
+
for node in level_order(tree, include_all=True):
|
|
717
|
+
|
|
718
|
+
if in_level == 0:
|
|
719
|
+
print()
|
|
720
|
+
print()
|
|
721
|
+
print(' '*left_padding, end=' ')
|
|
722
|
+
|
|
723
|
+
width = int(max_width*node_width/per_level)
|
|
724
|
+
|
|
725
|
+
node_str = (str(node.data) if node else '').center(width)
|
|
726
|
+
print(node_str, end=' ')
|
|
727
|
+
|
|
728
|
+
in_level += 1
|
|
729
|
+
|
|
730
|
+
if in_level == per_level:
|
|
731
|
+
in_level = 0
|
|
732
|
+
per_level *= 2
|
|
733
|
+
level += 1
|
|
734
|
+
|
|
735
|
+
if level > height:
|
|
736
|
+
break
|
|
737
|
+
|
|
738
|
+
print()
|
|
739
|
+
print()
|