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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ Stefan K�gl <stefan@skoegl.net>
2
+ Rafael K�ng <rafi.kueng@gmx.ch>
@@ -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()