Skip to content

Commit 189d704

Browse files
authored
Merge pull request #7 from hsorby/octree
Add a simple octree implementation.
2 parents cef9032 + 71ff7c1 commit 189d704

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

src/cmlibs/maths/octree.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Octree for searching for objects by coordinates
3+
"""
4+
import math
5+
6+
from cmlibs.maths.vectorops import sub, dot
7+
8+
9+
class Octree:
10+
"""
11+
Octree for searching for objects by coordinates
12+
"""
13+
14+
def __init__(self, tolerance=None):
15+
"""
16+
:param tolerance: If supplied, tolerance to use, or None to compute as 1.0E-12.
17+
"""
18+
self._tolerance = 1.0E-12 if tolerance is None else tolerance
19+
self._coordinates = None
20+
# Octree is either leaf with _object, or has 2**self._dimension children
21+
self._object = None
22+
# exactly 2^self._dimension children, cycling in lowest x index fastest
23+
self._children = None
24+
25+
def _location_match(self, x):
26+
return all([math.fabs(x[i] - self._coordinates[i]) < self._tolerance for i in range(3)])
27+
28+
def _child_index_lookup(self, x):
29+
switch = [0 if x[i] < self._coordinates[i] else 1 for i in range(3)]
30+
return switch[0] + 2 * switch[1] + 4 * switch[2]
31+
32+
def _sq_distance(self, x):
33+
diff = sub(x, self._coordinates)
34+
return dot(diff, diff)
35+
36+
def _find_object_by_coordinates(self, x, nearest=False):
37+
"""
38+
Find the closest existing object with |x - ox| < tolerance.
39+
:param x: 3 coordinates in a list.
40+
:return: nearest distance, nearest object or None, None if none found.
41+
"""
42+
if self._coordinates is not None and self._location_match(x):
43+
return 0.0, self._object
44+
45+
if not nearest and self._children is None:
46+
return None, None
47+
48+
if nearest and self._coordinates is None:
49+
return math.inf, None
50+
51+
if nearest and self._children is None:
52+
return 0.0, self._object
53+
54+
index = self._child_index_lookup(x)
55+
sq_distance_, object_ = self._children[index]._find_object_by_coordinates(x, nearest)
56+
57+
if nearest:
58+
sq_distance = self._sq_distance(x)
59+
if sq_distance_ < sq_distance:
60+
return sq_distance_, object_
61+
else:
62+
return sq_distance, self._object
63+
64+
return None, object_
65+
66+
def tolerance(self):
67+
return self._tolerance
68+
69+
def find_object_by_coordinates(self, x):
70+
"""
71+
Find the closest existing object with |x - ox| < tolerance.
72+
:param x: 3 coordinates in a list.
73+
:return: nearest object or None if not found.
74+
"""
75+
distance, nearest_object = self._find_object_by_coordinates(x)
76+
return nearest_object
77+
78+
def find_nearest_object_by_coordinates(self, x):
79+
distance, nearest_object = self._find_object_by_coordinates(x, True)
80+
return nearest_object
81+
82+
def insert_object_at_coordinates(self, x, obj):
83+
"""
84+
Add object at coordinates to octree.
85+
86+
:param x: 3 coordinates in a list.
87+
:param obj: object to store with coordinates.
88+
"""
89+
if self._coordinates is None:
90+
self._coordinates = x
91+
self._object = obj
92+
else:
93+
if self._location_match(x):
94+
self._object = obj
95+
else:
96+
index = self._child_index_lookup(x)
97+
if self._children is None:
98+
self._children = [Octree() for _ in range(8)]
99+
100+
self._children[index].insert_object_at_coordinates(x, obj)
101+
102+
def __repr__(self):
103+
if self._children is None:
104+
return f'\tleaf {self._coordinates}\n'
105+
106+
return f'{self._coordinates} - \n' + ''.join([f'{c}' for c in self._children])

tests/test_octree.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import unittest
2+
3+
from cmlibs.maths.octree import Octree
4+
5+
6+
class OctTreeTestCase(unittest.TestCase):
7+
8+
def test_oct_tree_1(self):
9+
pts = [[0.5002720342505129, 0.049999999999999996, -0.08501700857389402],
10+
[0.2645697738549972, -0.35824829046386303, 0.2483163247594392]]
11+
12+
o = Octree()
13+
o.insert_object_at_coordinates(pts[0], 1)
14+
o.insert_object_at_coordinates(pts[1], 2)
15+
16+
self.assertEqual(1, o.find_object_by_coordinates(pts[0]))
17+
self.assertEqual(2, o.find_object_by_coordinates(pts[1]))
18+
19+
def test_oct_tree_2(self):
20+
pts = [[-0.2934372873144786, -0.40824829046386296, -0.08501700857389399],
21+
[-0.30539112847838207, -0.47895555285030167, -0.15471008125537822],
22+
[-0.24765910625478924, -0.4789555528487444, -0.23635549006530096],
23+
[-0.17796723347655352, -0.40824829046386296, -0.24831632475943927],
24+
[-0.16601339231265005, -0.3375410280774243, -0.178623252077955],
25+
[-0.22374541453624286, -0.33754102807898145, -0.09697784326803233],
26+
[0.17796723347655338, -0.408248290463863, 0.2483163247594392],
27+
[0.24765910625759371, -0.47895555285030167, 0.2363554900632418],
28+
[0.3053911284793884, -0.4789555528487444, 0.15471008125204752],
29+
[0.29343728731447843, -0.408248290463863, 0.08501700857389394],
30+
[0.22374541453343808, -0.3375410280774243, 0.09697784327009142],
31+
[0.16601339231164347, -0.33754102807898156, 0.17862325208128554]]
32+
33+
o = Octree()
34+
for i, pt in enumerate(pts):
35+
o.insert_object_at_coordinates(pt, i + 1)
36+
37+
for i, pt in enumerate(pts):
38+
self.assertEqual(i + 1, o.find_object_by_coordinates(pt))
39+
40+
nd1 = [[-0.04936372890878149, -0.08550048652106605, -0.03490542745605377],
41+
[0.027648784957331453, -0.04277813171414004, -0.0914935840944021],
42+
[0.07704471667898151, 0.04277813171695018, -0.05656538580855996],
43+
[0.04936372890878152, 0.08550048652106605, 0.03490542745605376],
44+
[-0.027648784957331436, 0.04277813171413982, 0.09149358409440221],
45+
[-0.07704471667898151, -0.042778131716949955, 0.05656538580856002],
46+
[0.04936372890878148, -0.08550048652106605, 0.03490542745605377],
47+
[0.07704471667850743, -0.04277813171413998, -0.05656538581133083],
48+
[0.027648784954560728, 0.04277813171695016, -0.09149358409392554],
49+
[-0.04936372890878149, 0.0855004865210661, -0.03490542745605379],
50+
[-0.07704471667850739, 0.04277813171413971, 0.05656538581133116],
51+
[-0.027648784954560936, -0.04277813171694988, 0.09149358409392555]]
52+
53+
self.assertIsNone(o.find_object_by_coordinates(nd1[0]))
54+
self.assertEqual(11, o.find_nearest_object_by_coordinates([0.22374541453343808, -0.337541028078, 0.09697784327009142]))
55+
self.assertEqual(6, o.find_nearest_object_by_coordinates([-0.22374541453343808, -0.337541028078, -0.09697784327009142]))
56+
57+
def test_oct_tree_insert_existing(self):
58+
pts = [[0.5002720342505129, 0.049999999999999996, -0.08501700857389402],
59+
[0.2645697738549972, -0.35824829046386303, 0.2483163247594392]]
60+
61+
o = Octree()
62+
o.insert_object_at_coordinates(pts[0], 1)
63+
o.insert_object_at_coordinates(pts[1], 2)
64+
65+
self.assertEqual(1, o.find_object_by_coordinates(pts[0]))
66+
self.assertEqual(2, o.find_object_by_coordinates(pts[1]))
67+
68+
o.insert_object_at_coordinates(pts[1], 3)
69+
70+
self.assertEqual(3, o.find_object_by_coordinates(pts[1]))
71+
72+
def test_oct_tree_nearest(self):
73+
pts = [[0.5002720342505129, 0.049999999999999996, -0.08501700857389402],
74+
[0.2645697738549972, -0.35824829046386303, 0.2483163247594392]]
75+
find_pts = [[0.5002720342506, 0.049999999999999996, -0.08501700857389402],
76+
[0.2645697738549972, -0.35824829046386303, 0.24831632475944]]
77+
78+
o = Octree()
79+
o.insert_object_at_coordinates(pts[0], 1)
80+
o.insert_object_at_coordinates(pts[1], 2)
81+
82+
self.assertEqual(1, o.find_nearest_object_by_coordinates(find_pts[0]))
83+
self.assertEqual(2, o.find_nearest_object_by_coordinates(find_pts[1]))
84+
85+
86+
if __name__ == "__main__":
87+
unittest.main()

0 commit comments

Comments
 (0)