kdtree 0.16__tar.gz → 1.0__tar.gz
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-1.0/MANIFEST.in +7 -0
- kdtree-1.0/PKG-INFO +42 -0
- kdtree-1.0/kdtree.egg-info/PKG-INFO +42 -0
- {kdtree-0.16 → kdtree-1.0}/kdtree.egg-info/SOURCES.txt +3 -0
- {kdtree-0.16 → kdtree-1.0}/kdtree.py +131 -45
- kdtree-1.0/kdtree.pyi +165 -0
- {kdtree-0.16 → kdtree-1.0}/readme.md +19 -4
- kdtree-1.0/setup.cfg +15 -0
- {kdtree-0.16 → kdtree-1.0}/setup.py +36 -8
- kdtree-1.0/test.py +749 -0
- kdtree-1.0/test_typing.py +369 -0
- kdtree-1.0/typing_sample.py +227 -0
- kdtree-0.16/MANIFEST.in +0 -4
- kdtree-0.16/PKG-INFO +0 -29
- kdtree-0.16/kdtree.egg-info/PKG-INFO +0 -29
- kdtree-0.16/setup.cfg +0 -7
- kdtree-0.16/test.py +0 -371
- {kdtree-0.16 → kdtree-1.0}/AUTHORS +0 -0
- {kdtree-0.16 → kdtree-1.0}/LICENSE +0 -0
- {kdtree-0.16 → kdtree-1.0}/kdtree.egg-info/dependency_links.txt +0 -0
- {kdtree-0.16 → kdtree-1.0}/kdtree.egg-info/top_level.txt +0 -0
kdtree-1.0/MANIFEST.in
ADDED
kdtree-1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kdtree
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: A Python implementation of a kd-tree
|
|
5
|
+
Home-page: https://github.com/stefankoegl/kdtree
|
|
6
|
+
Download-URL: http://pypi.python.org/packages/source/k/kdtree/kdtree-1.0.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.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
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
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
License-File: AUTHORS
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: author-email
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: download-url
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
Dynamic: summary
|
|
40
|
+
|
|
41
|
+
This package provides a simple implementation of a kd-tree in Python.
|
|
42
|
+
https://en.wikipedia.org/wiki/K-d_tree
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kdtree
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: A Python implementation of a kd-tree
|
|
5
|
+
Home-page: https://github.com/stefankoegl/kdtree
|
|
6
|
+
Download-URL: http://pypi.python.org/packages/source/k/kdtree/kdtree-1.0.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.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
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
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
License-File: AUTHORS
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: author-email
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: download-url
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
Dynamic: summary
|
|
40
|
+
|
|
41
|
+
This package provides a simple implementation of a kd-tree in Python.
|
|
42
|
+
https://en.wikipedia.org/wiki/K-d_tree
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
"""A Python
|
|
4
|
+
"""A Python implementation of a kd-tree
|
|
5
5
|
|
|
6
6
|
This package provides a simple implementation of a kd-tree in Python.
|
|
7
7
|
https://en.wikipedia.org/wiki/K-d_tree
|
|
@@ -17,7 +17,7 @@ from collections import deque
|
|
|
17
17
|
from functools import wraps
|
|
18
18
|
|
|
19
19
|
__author__ = u'Stefan Kögl <stefan@skoegl.net>'
|
|
20
|
-
__version__ = '0
|
|
20
|
+
__version__ = '1.0'
|
|
21
21
|
__website__ = 'https://github.com/stefankoegl/kdtree'
|
|
22
22
|
__license__ = 'ISC license'
|
|
23
23
|
|
|
@@ -44,7 +44,7 @@ class Node(object):
|
|
|
44
44
|
>>> Node( 1, left=Node(2) ).is_leaf
|
|
45
45
|
False
|
|
46
46
|
"""
|
|
47
|
-
return (
|
|
47
|
+
return (self.data is None) or \
|
|
48
48
|
(all(not bool(c) for c, p in self.children))
|
|
49
49
|
|
|
50
50
|
|
|
@@ -175,10 +175,10 @@ class Node(object):
|
|
|
175
175
|
__bool__ = __nonzero__
|
|
176
176
|
|
|
177
177
|
def __eq__(self, other):
|
|
178
|
-
if isinstance(other,
|
|
179
|
-
return self.data == other
|
|
180
|
-
else:
|
|
178
|
+
if isinstance(other, Node):
|
|
181
179
|
return self.data == other.data
|
|
180
|
+
else:
|
|
181
|
+
return self.data == other
|
|
182
182
|
|
|
183
183
|
def __hash__(self):
|
|
184
184
|
return id(self)
|
|
@@ -190,7 +190,7 @@ def require_axis(f):
|
|
|
190
190
|
@wraps(f)
|
|
191
191
|
def _wrapper(self, *args, **kwargs):
|
|
192
192
|
if None in (self.axis, self.sel_axis):
|
|
193
|
-
raise ValueError('%(func_name) requires the node %(node)s '
|
|
193
|
+
raise ValueError('%(func_name)s requires the node %(node)s '
|
|
194
194
|
'to have an axis and a sel_axis function' %
|
|
195
195
|
dict(func_name=f.__name__, node=repr(self)))
|
|
196
196
|
|
|
@@ -229,10 +229,10 @@ class KDNode(Node):
|
|
|
229
229
|
Users should call add() only to the topmost tree.
|
|
230
230
|
"""
|
|
231
231
|
|
|
232
|
+
check_dimensionality([point], dimensions=self.dimensions)
|
|
233
|
+
|
|
232
234
|
current = self
|
|
233
235
|
while True:
|
|
234
|
-
check_dimensionality([point], dimensions=current.dimensions)
|
|
235
|
-
|
|
236
236
|
# Adding has hit an empty leaf-node, add here
|
|
237
237
|
if current.data is None:
|
|
238
238
|
current.data = point
|
|
@@ -294,33 +294,49 @@ class KDNode(Node):
|
|
|
294
294
|
|
|
295
295
|
If there are multiple points matching "point", only one is removed. The
|
|
296
296
|
optional "node" parameter is used for checking the identity, once the
|
|
297
|
-
|
|
297
|
+
removal candidate is decided."""
|
|
298
|
+
|
|
299
|
+
return self._remove_one(point, node)[0]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _remove_one(self, point, node):
|
|
303
|
+
""" Removes at most one matching node from the (sub)tree
|
|
304
|
+
|
|
305
|
+
Returns a (new-subtree-root, removed) tuple, where removed indicates
|
|
306
|
+
whether a node has been removed. """
|
|
298
307
|
|
|
299
308
|
# Recursion has reached an empty leaf node, nothing here to delete
|
|
300
309
|
if not self:
|
|
301
|
-
return
|
|
310
|
+
return self, False
|
|
302
311
|
|
|
303
312
|
# Recursion has reached the node to be deleted
|
|
304
313
|
if self.should_remove(point, node):
|
|
305
|
-
return self._remove(point)
|
|
314
|
+
return self._remove(point), True
|
|
306
315
|
|
|
307
316
|
# Remove direct subnode
|
|
308
317
|
if self.left and self.left.should_remove(point, node):
|
|
309
318
|
self.left = self.left._remove(point)
|
|
319
|
+
return self, True
|
|
310
320
|
|
|
311
321
|
elif self.right and self.right.should_remove(point, node):
|
|
312
322
|
self.right = self.right._remove(point)
|
|
323
|
+
return self, True
|
|
313
324
|
|
|
314
|
-
# Recurse to subtrees
|
|
325
|
+
# Recurse to subtrees. If the point lies on the splitting plane, both
|
|
326
|
+
# subtrees may contain it, but only one node must be removed
|
|
315
327
|
if point[self.axis] <= self.data[self.axis]:
|
|
316
328
|
if self.left:
|
|
317
|
-
self.left = self.left.
|
|
329
|
+
self.left, removed = self.left._remove_one(point, node)
|
|
330
|
+
if removed:
|
|
331
|
+
return self, True
|
|
318
332
|
|
|
319
333
|
if point[self.axis] >= self.data[self.axis]:
|
|
320
334
|
if self.right:
|
|
321
|
-
self.right = self.right.
|
|
335
|
+
self.right, removed = self.right._remove_one(point, node)
|
|
336
|
+
if removed:
|
|
337
|
+
return self, True
|
|
322
338
|
|
|
323
|
-
return self
|
|
339
|
+
return self, False
|
|
324
340
|
|
|
325
341
|
|
|
326
342
|
@require_axis
|
|
@@ -376,7 +392,10 @@ class KDNode(Node):
|
|
|
376
392
|
Returns the (possibly new) root of the rebalanced tree
|
|
377
393
|
"""
|
|
378
394
|
|
|
379
|
-
|
|
395
|
+
points = [x.data for x in self.inorder()]
|
|
396
|
+
axis = self.axis if self.axis is not None else 0
|
|
397
|
+
return create(points, dimensions=self.dimensions, axis=axis,
|
|
398
|
+
sel_axis=self.sel_axis)
|
|
380
399
|
|
|
381
400
|
|
|
382
401
|
def axis_dist(self, point, axis):
|
|
@@ -387,13 +406,17 @@ class KDNode(Node):
|
|
|
387
406
|
return math.pow(self.data[axis] - point[axis], 2)
|
|
388
407
|
|
|
389
408
|
|
|
390
|
-
def dist(self, point):
|
|
409
|
+
def dist(self, point, axis=None):
|
|
391
410
|
"""
|
|
392
411
|
Squared distance between the current Node
|
|
393
412
|
and the given point
|
|
394
413
|
"""
|
|
395
|
-
|
|
396
|
-
|
|
414
|
+
if axis is None:
|
|
415
|
+
axes = range(self.dimensions)
|
|
416
|
+
else:
|
|
417
|
+
axes = [axis]
|
|
418
|
+
|
|
419
|
+
return sum([self.axis_dist(point, i) for i in axes])
|
|
397
420
|
|
|
398
421
|
|
|
399
422
|
def search_knn(self, point, k, dist=None):
|
|
@@ -406,7 +429,9 @@ class KDNode(Node):
|
|
|
406
429
|
distances.
|
|
407
430
|
|
|
408
431
|
dist is a distance function, expecting two points and returning a
|
|
409
|
-
distance value.
|
|
432
|
+
distance value. dist should expect an optional `axis` parameter. If
|
|
433
|
+
given, the distance on the specified axis should be calculated.
|
|
434
|
+
Distance values must support comparison and unary negation.
|
|
410
435
|
|
|
411
436
|
The result is an ordered list of (node, distance) tuples.
|
|
412
437
|
"""
|
|
@@ -414,21 +439,23 @@ class KDNode(Node):
|
|
|
414
439
|
if k < 1:
|
|
415
440
|
raise ValueError("k must be greater than 0.")
|
|
416
441
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
442
|
+
def get_dist(n, axis=None):
|
|
443
|
+
if dist is None:
|
|
444
|
+
return n.dist(point, axis=axis)
|
|
445
|
+
else:
|
|
446
|
+
return dist(n.data, point, axis=axis)
|
|
421
447
|
|
|
422
448
|
results = []
|
|
423
449
|
|
|
424
|
-
self._search_node(point, k, results, get_dist, itertools.count()
|
|
450
|
+
self._search_node(point, k, results, get_dist, itertools.count(),
|
|
451
|
+
prune=dist is None)
|
|
425
452
|
|
|
426
453
|
# We sort the final result by the distance in the tuple
|
|
427
454
|
# (<KdNode>, distance).
|
|
428
455
|
return [(node, -d) for d, _, node in sorted(results, reverse=True)]
|
|
429
456
|
|
|
430
457
|
|
|
431
|
-
def _search_node(self, point, k, results, get_dist, counter):
|
|
458
|
+
def _search_node(self, point, k, results, get_dist, counter, prune=True):
|
|
432
459
|
if not self:
|
|
433
460
|
return
|
|
434
461
|
|
|
@@ -446,32 +473,33 @@ class KDNode(Node):
|
|
|
446
473
|
heapq.heapreplace(results, item)
|
|
447
474
|
else:
|
|
448
475
|
heapq.heappush(results, item)
|
|
476
|
+
|
|
449
477
|
# get the splitting plane
|
|
450
478
|
split_plane = self.data[self.axis]
|
|
451
|
-
# get the
|
|
452
|
-
|
|
453
|
-
plane_dist = point[self.axis] - split_plane
|
|
454
|
-
plane_dist2 = plane_dist * plane_dist
|
|
479
|
+
# get the distance between the point and the splitting plane
|
|
480
|
+
plane_dist = get_dist(self, axis=self.axis)
|
|
455
481
|
|
|
456
482
|
# Search the side of the splitting plane that the point is in
|
|
457
483
|
if point[self.axis] < split_plane:
|
|
458
484
|
if self.left is not None:
|
|
459
|
-
self.left._search_node(point, k, results, get_dist, counter
|
|
485
|
+
self.left._search_node(point, k, results, get_dist, counter,
|
|
486
|
+
prune=prune)
|
|
460
487
|
else:
|
|
461
488
|
if self.right is not None:
|
|
462
|
-
self.right._search_node(point, k, results, get_dist, counter
|
|
489
|
+
self.right._search_node(point, k, results, get_dist, counter,
|
|
490
|
+
prune=prune)
|
|
463
491
|
|
|
464
492
|
# Search the other side of the splitting plane if it may contain
|
|
465
493
|
# points closer than the farthest point in the current results.
|
|
466
|
-
if -
|
|
494
|
+
if not prune or -plane_dist > results[0][0] or len(results) < k:
|
|
467
495
|
if point[self.axis] < self.data[self.axis]:
|
|
468
496
|
if self.right is not None:
|
|
469
497
|
self.right._search_node(point, k, results, get_dist,
|
|
470
|
-
counter)
|
|
498
|
+
counter, prune=prune)
|
|
471
499
|
else:
|
|
472
500
|
if self.left is not None:
|
|
473
501
|
self.left._search_node(point, k, results, get_dist,
|
|
474
|
-
counter)
|
|
502
|
+
counter, prune=prune)
|
|
475
503
|
|
|
476
504
|
|
|
477
505
|
@require_axis
|
|
@@ -484,10 +512,14 @@ class KDNode(Node):
|
|
|
484
512
|
with this location will be returned (not its neighbor).
|
|
485
513
|
|
|
486
514
|
dist is a distance function, expecting two points and returning a
|
|
487
|
-
distance value.
|
|
515
|
+
distance value. dist should expect an optional `axis` parameter. If
|
|
516
|
+
given, the distance on the specified axis should be calculated.
|
|
517
|
+
Distance values must support comparison and unary negation.
|
|
488
518
|
|
|
489
519
|
The result is a (node, distance) tuple.
|
|
490
520
|
"""
|
|
521
|
+
if not self:
|
|
522
|
+
raise ValueError("tree is empty")
|
|
491
523
|
|
|
492
524
|
return next(iter(self.search_knn(point, 1, dist)), None)
|
|
493
525
|
|
|
@@ -499,28 +531,36 @@ class KDNode(Node):
|
|
|
499
531
|
nodeDist = get_dist(self)
|
|
500
532
|
|
|
501
533
|
if nodeDist < dist:
|
|
502
|
-
|
|
534
|
+
results.append(self.data)
|
|
503
535
|
|
|
504
536
|
# get the splitting plane
|
|
505
537
|
split_plane = self.data[self.axis]
|
|
538
|
+
# dist is compared against squared distances, so the distance to the
|
|
539
|
+
# splitting plane has to be squared as well
|
|
540
|
+
plane_dist = self.axis_dist(point, self.axis)
|
|
506
541
|
|
|
507
|
-
# Search
|
|
508
|
-
|
|
542
|
+
# Search each side of the splitting plane that may contain points
|
|
543
|
+
# within the given distance
|
|
544
|
+
if point[self.axis] <= split_plane or plane_dist < dist:
|
|
509
545
|
if self.left is not None:
|
|
510
546
|
self.left._search_nn_dist(point, dist, results, get_dist)
|
|
511
|
-
if point[self.axis] >= split_plane
|
|
547
|
+
if point[self.axis] >= split_plane or plane_dist < dist:
|
|
512
548
|
if self.right is not None:
|
|
513
549
|
self.right._search_nn_dist(point, dist, results, get_dist)
|
|
514
550
|
|
|
515
551
|
|
|
516
552
|
@require_axis
|
|
517
|
-
def search_nn_dist(self, point, distance
|
|
553
|
+
def search_nn_dist(self, point, distance):
|
|
518
554
|
"""
|
|
519
555
|
Search the n nearest nodes of the given point which are within given
|
|
520
556
|
distance
|
|
521
557
|
|
|
522
558
|
point must be a location, not a node. A list containing the n nearest
|
|
523
559
|
nodes to the point within the distance will be returned.
|
|
560
|
+
|
|
561
|
+
distance is the threshold distance to filter nodes.
|
|
562
|
+
Note: KDNode.dist() returns squared distance, so the comparison will
|
|
563
|
+
be done using squared_distance < distance.
|
|
524
564
|
"""
|
|
525
565
|
|
|
526
566
|
results = []
|
|
@@ -529,6 +569,51 @@ class KDNode(Node):
|
|
|
529
569
|
self._search_nn_dist(point, distance, results, get_dist)
|
|
530
570
|
return results
|
|
531
571
|
|
|
572
|
+
@require_axis
|
|
573
|
+
def range_search(self, top_left, bottom_right):
|
|
574
|
+
"""
|
|
575
|
+
Search all nodes in or on the hyperrectangle defined by the points top_left and bottom_right.
|
|
576
|
+
|
|
577
|
+
Note: the list returned is not in any particular order.
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
invalid_rect = False
|
|
581
|
+
if len(top_left) != len(bottom_right) or len(top_left) != self.dimensions:
|
|
582
|
+
invalid_rect = True
|
|
583
|
+
else:
|
|
584
|
+
for i in range(len(top_left)):
|
|
585
|
+
if top_left[i] > bottom_right[i]:
|
|
586
|
+
invalid_rect = True
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
if invalid_rect:
|
|
590
|
+
raise ValueError("invalid rectangle")
|
|
591
|
+
|
|
592
|
+
results = []
|
|
593
|
+
stack = [self]
|
|
594
|
+
|
|
595
|
+
while(stack):
|
|
596
|
+
node = stack.pop()
|
|
597
|
+
|
|
598
|
+
data = node.data
|
|
599
|
+
axis = node.axis
|
|
600
|
+
|
|
601
|
+
if data is None:
|
|
602
|
+
continue
|
|
603
|
+
|
|
604
|
+
in_range = True
|
|
605
|
+
for d in range(0, len(data)):
|
|
606
|
+
in_range &= (data[d] >= top_left[d]) and (data[d] <= bottom_right[d])
|
|
607
|
+
|
|
608
|
+
if in_range:
|
|
609
|
+
results.append(node)
|
|
610
|
+
|
|
611
|
+
if data[axis] >= top_left[axis] and node.left:
|
|
612
|
+
stack.append(node.left)
|
|
613
|
+
if data[axis] <= bottom_right[axis] and node.right:
|
|
614
|
+
stack.append(node.right)
|
|
615
|
+
|
|
616
|
+
return results
|
|
532
617
|
|
|
533
618
|
@require_axis
|
|
534
619
|
def is_valid(self):
|
|
@@ -606,8 +691,8 @@ def create(point_list=None, dimensions=None, axis=0, sel_axis=None):
|
|
|
606
691
|
median = len(point_list) // 2
|
|
607
692
|
|
|
608
693
|
loc = point_list[median]
|
|
609
|
-
left = create(point_list[:median], dimensions, sel_axis(axis))
|
|
610
|
-
right = create(point_list[median + 1:], dimensions, sel_axis(axis))
|
|
694
|
+
left = create(point_list[:median], dimensions, sel_axis(axis), sel_axis)
|
|
695
|
+
right = create(point_list[median + 1:], dimensions, sel_axis(axis), sel_axis)
|
|
611
696
|
return KDNode(loc, left, right, axis=axis, sel_axis=sel_axis, dimensions=dimensions)
|
|
612
697
|
|
|
613
698
|
|
|
@@ -651,7 +736,8 @@ def visualize(tree, max_level=100, node_width=10, left_padding=5):
|
|
|
651
736
|
in_level = 0
|
|
652
737
|
level = 0
|
|
653
738
|
|
|
654
|
-
|
|
739
|
+
# level_order() is infinite with include_all; the loop ends with break
|
|
740
|
+
for node in level_order(tree, include_all=True): # pragma: no branch
|
|
655
741
|
|
|
656
742
|
if in_level == 0:
|
|
657
743
|
print()
|
kdtree-1.0/kdtree.pyi
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Type stubs for the kdtree module
|
|
2
|
+
|
|
3
|
+
kdtree is distributed as a single-file module. Type information is therefore
|
|
4
|
+
shipped as this stub file, which is installed next to kdtree.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable, Collection, Iterable, Iterator, Sequence
|
|
8
|
+
from typing import Any, Generic, ParamSpec, Protocol, Self, TypeVar, overload
|
|
9
|
+
|
|
10
|
+
__author__: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__website__: str
|
|
13
|
+
__license__: str
|
|
14
|
+
|
|
15
|
+
class _Point(Protocol):
|
|
16
|
+
"""Anything that can be used as a point: it has a length and its
|
|
17
|
+
coordinates can be accessed by index"""
|
|
18
|
+
|
|
19
|
+
def __len__(self) -> int: ...
|
|
20
|
+
def __getitem__(self, index: int, /) -> Any: ...
|
|
21
|
+
|
|
22
|
+
_T = TypeVar("_T")
|
|
23
|
+
_P = TypeVar("_P", bound=_Point)
|
|
24
|
+
_Q = TypeVar("_Q", bound=_Point)
|
|
25
|
+
_P_contra = TypeVar("_P_contra", contravariant=True)
|
|
26
|
+
_Q_contra = TypeVar("_Q_contra", contravariant=True)
|
|
27
|
+
_D_co = TypeVar("_D_co", covariant=True)
|
|
28
|
+
_N = TypeVar("_N", bound=Node[Any])
|
|
29
|
+
_D = TypeVar("_D", bound=_Distance)
|
|
30
|
+
_R = TypeVar("_R")
|
|
31
|
+
_Params = ParamSpec("_Params")
|
|
32
|
+
|
|
33
|
+
class _Distance(Protocol):
|
|
34
|
+
"""A distance value: search_knn() negates distances and compares them"""
|
|
35
|
+
|
|
36
|
+
def __neg__(self) -> Self: ...
|
|
37
|
+
def __lt__(self, other: Any, /) -> bool: ...
|
|
38
|
+
def __gt__(self, other: Any, /) -> bool: ...
|
|
39
|
+
|
|
40
|
+
class _DistFunc(Protocol[_P_contra, _Q_contra, _D_co]):
|
|
41
|
+
"""A distance function, always called as dist(a, b, axis=axis)
|
|
42
|
+
|
|
43
|
+
a is a point stored in the tree, b is the point that is searched for."""
|
|
44
|
+
|
|
45
|
+
def __call__(
|
|
46
|
+
self, a: _P_contra, b: _Q_contra, /, *, axis: int | None
|
|
47
|
+
) -> _D_co: ...
|
|
48
|
+
|
|
49
|
+
class Node(Generic[_T]):
|
|
50
|
+
data: _T | None
|
|
51
|
+
left: Self | None
|
|
52
|
+
right: Self | None
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
data: _T | None = None,
|
|
56
|
+
left: Self | None = None,
|
|
57
|
+
right: Self | None = None,
|
|
58
|
+
) -> None: ...
|
|
59
|
+
@property
|
|
60
|
+
def is_leaf(self) -> bool: ...
|
|
61
|
+
def preorder(self) -> Iterator[Self]: ...
|
|
62
|
+
def inorder(self) -> Iterator[Self]: ...
|
|
63
|
+
def postorder(self) -> Iterator[Self]: ...
|
|
64
|
+
@property
|
|
65
|
+
def children(self) -> Iterator[tuple[Self, int]]: ...
|
|
66
|
+
def set_child(self, index: int, child: Self | None) -> None: ...
|
|
67
|
+
def height(self) -> int: ...
|
|
68
|
+
def get_child_pos(self, child: Node[Any]) -> int | None: ...
|
|
69
|
+
def __nonzero__(self) -> bool: ...
|
|
70
|
+
def __bool__(self) -> bool: ...
|
|
71
|
+
def __eq__(self, other: object) -> bool: ...
|
|
72
|
+
def __hash__(self) -> int: ...
|
|
73
|
+
|
|
74
|
+
def require_axis(f: Callable[_Params, _R]) -> Callable[_Params, _R]: ...
|
|
75
|
+
|
|
76
|
+
class KDNode(Node[_P]):
|
|
77
|
+
axis: int | None
|
|
78
|
+
sel_axis: Callable[[int], int] | None
|
|
79
|
+
dimensions: int | None
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
data: _P | None = None,
|
|
83
|
+
left: Self | None = None,
|
|
84
|
+
right: Self | None = None,
|
|
85
|
+
axis: int | None = None,
|
|
86
|
+
sel_axis: Callable[[int], int] | None = None,
|
|
87
|
+
dimensions: int | None = None,
|
|
88
|
+
) -> None: ...
|
|
89
|
+
def add(self, point: _P) -> Self: ...
|
|
90
|
+
def create_subnode(self, data: _P | None) -> Self: ...
|
|
91
|
+
def find_replacement(self) -> tuple[Self | None, Self]: ...
|
|
92
|
+
def should_remove(self, point: _Point, node: KDNode[Any] | None) -> bool: ...
|
|
93
|
+
def remove(self, point: _Point, node: KDNode[Any] | None = None) -> Self: ...
|
|
94
|
+
@property
|
|
95
|
+
def is_balanced(self) -> bool: ...
|
|
96
|
+
def rebalance(self) -> KDNode[_P]: ...
|
|
97
|
+
def axis_dist(self, point: _Point, axis: int) -> float: ...
|
|
98
|
+
def dist(self, point: _Point, axis: int | None = None) -> float: ...
|
|
99
|
+
@overload
|
|
100
|
+
def search_knn(
|
|
101
|
+
self, point: _Point, k: int, dist: None = None
|
|
102
|
+
) -> list[tuple[Self, float]]: ...
|
|
103
|
+
@overload
|
|
104
|
+
def search_knn(
|
|
105
|
+
self, point: _Q, k: int, dist: _DistFunc[_P, _Q, _D]
|
|
106
|
+
) -> list[tuple[Self, _D]]: ...
|
|
107
|
+
@overload
|
|
108
|
+
def search_nn(self, point: _Point, dist: None = None) -> tuple[Self, float]: ...
|
|
109
|
+
@overload
|
|
110
|
+
def search_nn(
|
|
111
|
+
self, point: _Q, dist: _DistFunc[_P, _Q, _D]
|
|
112
|
+
) -> tuple[Self, _D]: ...
|
|
113
|
+
def search_nn_dist(self, point: _Point, distance: float) -> list[_P]: ...
|
|
114
|
+
def range_search(self, top_left: _Point, bottom_right: _Point) -> list[Self]: ...
|
|
115
|
+
def is_valid(self) -> bool: ...
|
|
116
|
+
def extreme_child(
|
|
117
|
+
self,
|
|
118
|
+
sel_func: Callable[..., Any],
|
|
119
|
+
axis: int,
|
|
120
|
+
) -> tuple[Self, Self | None] | tuple[None, None]: ...
|
|
121
|
+
|
|
122
|
+
@overload
|
|
123
|
+
def create(
|
|
124
|
+
point_list: None = None,
|
|
125
|
+
*,
|
|
126
|
+
dimensions: int,
|
|
127
|
+
axis: int = 0,
|
|
128
|
+
sel_axis: Callable[[int], int] | None = None,
|
|
129
|
+
) -> KDNode[Any]: ...
|
|
130
|
+
@overload
|
|
131
|
+
def create(
|
|
132
|
+
point_list: None,
|
|
133
|
+
dimensions: int,
|
|
134
|
+
axis: int = 0,
|
|
135
|
+
sel_axis: Callable[[int], int] | None = None,
|
|
136
|
+
) -> KDNode[Any]: ...
|
|
137
|
+
@overload
|
|
138
|
+
def create(
|
|
139
|
+
point_list: Sequence[_P],
|
|
140
|
+
dimensions: int | None = None,
|
|
141
|
+
axis: int = 0,
|
|
142
|
+
sel_axis: Callable[[int], int] | None = None,
|
|
143
|
+
) -> KDNode[_P]: ...
|
|
144
|
+
@overload
|
|
145
|
+
def create(
|
|
146
|
+
# without dimensions, they are taken from point_list[0]. Iterators are
|
|
147
|
+
# not supported, as the dimensionality check consumes them
|
|
148
|
+
point_list: Collection[_P],
|
|
149
|
+
dimensions: int,
|
|
150
|
+
axis: int = 0,
|
|
151
|
+
sel_axis: Callable[[int], int] | None = None,
|
|
152
|
+
) -> KDNode[_P]: ...
|
|
153
|
+
@overload
|
|
154
|
+
def check_dimensionality(
|
|
155
|
+
point_list: Sequence[_Point], dimensions: int | None = None
|
|
156
|
+
) -> int: ...
|
|
157
|
+
@overload
|
|
158
|
+
def check_dimensionality(point_list: Iterable[_Point], dimensions: int) -> int: ...
|
|
159
|
+
def level_order(tree: _N, include_all: bool = False) -> Iterator[_N]: ...
|
|
160
|
+
def visualize(
|
|
161
|
+
tree: Node[Any],
|
|
162
|
+
max_level: int = 100,
|
|
163
|
+
node_width: int = 10,
|
|
164
|
+
left_padding: int = 5,
|
|
165
|
+
) -> None: ...
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
A simple kd-tree in Python [](https://github.com/stefankoegl/kdtree/actions/workflows/tests.yml)
|
|
2
2
|
==========================
|
|
3
3
|
|
|
4
4
|
The kdtree package can construct, modify and search
|
|
@@ -8,7 +8,7 @@ The kdtree package can construct, modify and search
|
|
|
8
8
|
* Repository: https://github.com/stefankoegl/kdtree.git
|
|
9
9
|
* Documentation: https://python-kdtree.readthedocs.org/
|
|
10
10
|
* PyPI: https://pypi.python.org/pypi/kdtree
|
|
11
|
-
*
|
|
11
|
+
* GitHub Actions: https://github.com/stefankoegl/kdtree/actions
|
|
12
12
|
* Coveralls: https://coveralls.io/r/stefankoegl/kdtree
|
|
13
13
|
|
|
14
14
|
|
|
@@ -55,7 +55,7 @@ Usage
|
|
|
55
55
|
|
|
56
56
|
# Find the nearest node to the location (1, 2, 3)
|
|
57
57
|
>>> tree.search_nn( (1, 2, 3) )
|
|
58
|
-
<KDNode - (2, 3, 4)
|
|
58
|
+
(<KDNode - (2, 3, 4)>, 3.0)
|
|
59
59
|
|
|
60
60
|
# Add a point to make the tree more interesting
|
|
61
61
|
>>> tree.add( (10, 2, 1) )
|
|
@@ -73,7 +73,7 @@ Usage
|
|
|
73
73
|
# Take the right subtree of the root
|
|
74
74
|
>>> subtree = tree.right
|
|
75
75
|
|
|
76
|
-
# and
|
|
76
|
+
# and detach it
|
|
77
77
|
>>> tree.right = None
|
|
78
78
|
>>> kdtree.visualize(tree)
|
|
79
79
|
|
|
@@ -122,6 +122,21 @@ Usage
|
|
|
122
122
|
|
|
123
123
|
(2, 3, 4)
|
|
124
124
|
|
|
125
|
+
### Type hints
|
|
126
|
+
|
|
127
|
+
Type information is provided in the stub file `kdtree.pyi`, which is installed
|
|
128
|
+
next to `kdtree.py` and, for mypy, as the stub-only package `kdtree-stubs`.
|
|
129
|
+
Trees are generic in the type of their points:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
tree = kdtree.create([(1, 2), (3, 4)]) # KDNode[tuple[int, int]]
|
|
133
|
+
node, dist = tree.search_nn((1, 1)) # KDNode[tuple[int, int]], float
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The classes are only generic for type checkers; at runtime write
|
|
137
|
+
annotations like `kdtree.KDNode[tuple[int, int]]` as strings or use
|
|
138
|
+
`from __future__ import annotations`.
|
|
139
|
+
|
|
125
140
|
### Adding a payload
|
|
126
141
|
|
|
127
142
|
Indexing a dict by a pair of floats is not a good idea, since there might be unexpected precision errors.
|