-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvp-tree.py
152 lines (123 loc) · 5.91 KB
/
vp-tree.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# Imports
import random
import matplotlib.pyplot as plt
import utilities
from utilities import Node, Point, DQueue
# Constants
POINTS_NUMBER = 1000 # Number of randomly generated points
NEIGHBORS_NUMBER = 5 # Number of neighbors to find
def divideAndConquer(points):
# Check to end recursion: if points array is of size 0 - we are returning a leaf
if len(points) == 1:
return Node([points[0], 0])
# Select a random point and remove it from the list
point = random.choice(points)
points.remove(point)
# Calculate median distance of point and add it with the point into the local node.
# Node data is in the form of [point, median]
distances = []
for p in points:
distances.append(p.distance(point))
distances.sort()
if len(distances) % 2 == 0:
median = 0.5 * (distances[int(len(distances) / 2) - 1] + distances[int(len(distances) / 2)])
else:
median = distances[int((len(distances) + 1) / 2) - 1]
localNode = Node([point, median])
# Add vantage points to plot
ax.add_patch(
plt.Circle((point.X, point.Y), median, color=(random.uniform(0, 1), random.uniform(0, 1), random.uniform(0, 1)),
alpha=0.1))
# Sort points into two new arrays: the ones within median range and ones outside.
# Those with distance less than median go on the left
leftPoints = []
rightPoints = []
for p in points:
if point.distance(p) < median:
leftPoints.append(p)
else:
rightPoints.append(p)
# Recursively call on left and right child of node
if len(leftPoints) == 0:
localNode.left = None
else:
localNode.left = divideAndConquer(leftPoints)
if len(rightPoints) == 0:
localNode.right = None
else:
localNode.right = divideAndConquer(rightPoints)
# Return local node
return localNode
# Call searchTree to find the query's neighbors
def searchTree(startingNode):
# This is the radius around the query defined by the distance of query-furthest current neighbor
if neighbors.length() is not 0:
searchRadius = neighbors.peek_distance()
else:
# This is just a big default radius just so it will be directly replaced the first time searchTree is called
searchRadius = 999999
# Check if startingNode can be a neighbor based on its distance to the query point and already recorded neighbors
nodePoint = startingNode.data[0]
distance = nodePoint.distance(query)
if distance < searchRadius:
neighbors.insert(nodePoint, distance)
# Update searchRadius after adding a neighbor
searchRadius = neighbors.peek_distance()
# First search by scaling down the tree following the left or right subtree each time according to the query's
# distance to the subtree root node.
# Also check if we should check the other subtree by checking if the other subtree root intersects with the maximum
# radius around the query. This decision is made after we have completely scaled down the tree.
# NodePointRadius is the radius around the nodePoint defined by the median of points around it
nodePointRadius = startingNode.data[1]
# If nodePointRadius is 0 it means we've reached a leaf in the binary tree so we end the recursion
if nodePointRadius == 0:
return
# In this case the query point is within the nodePointRadius so we scale down the left subtree and decide if we also
# want to search the right subtree of the startingNode.
if (distance < nodePointRadius) and (startingNode.left is not None):
searchTree(startingNode.left)
# If this is true, it means that part of the search area around our query point intersects with the area outside
# of the one covered by the left subtree. We refresh the searchRadius variable because it is possible that it
# has been updated while searchTree(startingNode.left). We also test that the other subtree exists.
searchRadius = neighbors.peek_distance()
if (nodePointRadius < (distance + searchRadius)) and (startingNode.right is not None):
searchTree(startingNode.right)
# In this case the query point is outside the nodePointRadius so we scale down the right subtree and decide if we
# also want to search the left subtree of the startingNode
elif (distance >= nodePointRadius) and (startingNode.right is not None):
searchTree(startingNode.right)
# If this is true, it means that part of the search area around our query point intersects with the area outside
# of the one covered by the right subtree. We refresh the searchRadius variable because it is possible that it
# has been updated while searchTree(startingNode.left). We also test that the other subtree exists.
searchRadius = neighbors.peek_distance()
if (distance < (nodePointRadius + searchRadius)) and (startingNode.left is not None):
searchTree(startingNode.left)
else:
return
# Main program
# Setup plot
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.set_aspect(aspect='equal')
# Generate random points on a two dimensional plane
points = utilities.random_points(POINTS_NUMBER)
# Nodes contain the point and its median distance from all other points in the format Node([ point, median])
# Call divideAndConquer to build the binary tree
root = divideAndConquer(points)
# Generate a random point to use as a query
query = Point(random.random(), random.random())
# Call searchTree to find the query's neighbors
neighbors = DQueue(NEIGHBORS_NUMBER)
searchTree(root)
# Print results and make the plot
print("Query Point: \n")
query.print()
ax.plot(query.X, query.Y, 'bo')
ax.add_patch(plt.Circle((query.X, query.Y), neighbors.peek_distance(), color='b', alpha=0.2))
print("Neighbors: ")
for point in points:
ax.plot(point.X, point.Y, 'ro')
for neighbor in neighbors.data:
neighbor[0].print()
ax.plot(neighbor[0].X, neighbor[0].Y, 'go')
plt.show()