Skip to content

Commit d91f2e9

Browse files
authored
Merge pull request #2 from kip-hart/dev
Update to v2.2
2 parents 2a09bf1 + ac91497 commit d91f2e9

File tree

4 files changed

+158
-44
lines changed

4 files changed

+158
-44
lines changed

aabbtree.py

Lines changed: 135 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def merge(cls, aabb1, aabb2):
105105

106106
@property
107107
def perimeter(self):
108-
r"""Perimeter of AABB
108+
r"""float: perimeter of AABB
109109
110110
The perimeter :math:`p_n` of an AABB with side lengths
111111
:math:`l_1 \ldots l_n` is:
@@ -132,6 +132,14 @@ def perimeter(self):
132132
perim += p_edge
133133
return 2 * perim
134134

135+
@property
136+
def volume(self):
137+
"""float: volume of AABB"""
138+
vol = 1
139+
for lb, ub in self:
140+
vol *= ub - lb
141+
return vol
142+
135143
def overlaps(self, aabb):
136144
"""Determine if two AABBs overlap
137145
@@ -153,6 +161,38 @@ def overlaps(self, aabb):
153161
return False
154162
return True
155163

164+
def overlap_volume(self, aabb):
165+
r"""Determine volume of overlap between AABBs
166+
167+
Let :math:`(x_i^l, x_i^u)` be the i-th dimension
168+
lower and upper bounds for AABB 1, and
169+
let :math:`(y_i^l, y_i^u)` be the lower and upper bounds for
170+
AABB 2. The volume of overlap is:
171+
172+
.. math::
173+
174+
V = \prod_{i=1}^n \text{max}(0, \text{min}(x_i^u, y_i^u) - \text{max}(x_i^l, y_i^l))
175+
176+
Args:
177+
aabb (AABB): The AABB to calculate for overlap volume
178+
179+
Returns:
180+
float: Volume of overlap
181+
""" # NOQA: E501
182+
183+
volume = 1
184+
for lims1, lims2 in zip(self, aabb):
185+
min1, max1 = lims1
186+
min2, max2 = lims2
187+
188+
overlap_min = max(min1, min2)
189+
overlap_max = min(max1, max2)
190+
if overlap_min >= overlap_max:
191+
return 0
192+
193+
volume *= overlap_max - overlap_min
194+
return volume
195+
156196

157197
class AABBTree(object):
158198
"""Python Implementation of the AABB Tree
@@ -233,20 +273,83 @@ def __eq__(self, aabbtree):
233273
def __ne__(self, aabbtree):
234274
return not self.__eq__(aabbtree)
235275

276+
def __len__(self):
277+
if self.is_leaf:
278+
return int(self.aabb != AABB())
279+
else:
280+
return len(self.left) + len(self.right)
281+
236282
@property
237283
def is_leaf(self):
238284
"""bool: returns True if is leaf node"""
239285
return (self.left is None) and (self.right is None)
240286

241-
def add(self, aabb, value=None):
242-
"""Add node to tree
287+
@property
288+
def depth(self):
289+
"""int: Depth of the tree"""
290+
if self.is_leaf:
291+
return 0
292+
else:
293+
return 1 + max(self.left.depth, self.right.depth)
294+
295+
def add(self, aabb, value=None, method='volume'):
296+
r"""Add node to tree
243297
244298
This function inserts a node into the AABB tree.
299+
The function chooses one of three options for adding the node to
300+
the tree:
301+
302+
* Add it to the left side
303+
* Add it to the right side
304+
* Become a leaf node
305+
306+
The cost of each option is calculated based on the *method* keyword,
307+
and the option with the lowest cost is chosen.
245308
246309
Args:
247310
aabb (AABB): The AABB to add.
248311
value: The value associated with the AABB. Defaults to None.
249-
"""
312+
method (str): The method for deciding how to build the tree.
313+
Should be one of the following:
314+
315+
* 'volume'
316+
317+
**'volume'**
318+
*Costs based on total bounding volume and overlap volume*
319+
320+
Let :math:`b` denote the tree, :math:`l` denote the left
321+
branch, :math:`r` denote the right branch, :math:`x` denote
322+
the AABB to add, and math:`V` be the volume of an AABB.
323+
The cost associated with each of these options is:
324+
325+
.. math::
326+
327+
C(\text{add left}) &= V(b \cup x) - V(b) + V(l \cup x) - V(l) + V((l \cup x) \cap r) \\
328+
C(\text{add right}) &= V(b \cup x) - V(b) + V(r \cup x) - V(r) + V((r \cup x) \cap l) \\
329+
C(\text{leaf}) &= V(b \cup x) + V(b \cap x)
330+
331+
The first two terms in the 'add left' cost represent the change
332+
in volume for the tree. The next two terms give the change in
333+
volume for the left branch specifically (right branch is
334+
unchanged). The final term is the amount of overlap that would
335+
be between the new left branch and the right branch.
336+
337+
This cost function includes the increases in bounding volumes and
338+
the amount of overlap- two values a balanced AABB tree should minimize.
339+
340+
The 'add right' cost is a mirror opposite of the 'add left cost'.
341+
The 'leaf' cost is the added bounding volume plus a penalty for
342+
overlapping with the existing tree.
343+
344+
These costs suit the author's current needs.
345+
Other applications, such as raytracing, are more concerned
346+
with surface area than volume. Please visit the
347+
`AABBTree repository`_ if you are interested in implementing
348+
another cost function.
349+
350+
.. _`AABBTree repository`: https://github.com/kip-hart/AABBTree
351+
352+
""" # NOQA: E501
250353
if self.aabb == AABB():
251354
self.aabb = aabb
252355
self.value = value
@@ -258,27 +361,38 @@ def add(self, aabb, value=None):
258361
self.aabb = AABB.merge(self.aabb, aabb)
259362
self.value = None
260363
else:
261-
tree_p = self.aabb.perimeter
262-
tree_merge_p = AABB.merge(self.aabb, aabb).perimeter
263-
264-
new_parent_cost = 2 * tree_merge_p
265-
min_pushdown_cost = 2 * (tree_merge_p - tree_p)
266-
267-
left_merge_p = AABB.merge(self.left.aabb, aabb).perimeter
268-
cost_left = left_merge_p + min_pushdown_cost
269-
if not self.left.is_leaf:
270-
cost_left -= self.left.aabb.perimeter
271-
272-
right_merge_p = AABB.merge(self.right.aabb, aabb).perimeter
273-
cost_right = right_merge_p + min_pushdown_cost
274-
if not self.right.is_leaf:
275-
cost_right -= self.right.aabb.perimeter
364+
if method == 'volume':
365+
# Define merged AABBs
366+
branch_merge = AABB.merge(self.aabb, aabb)
367+
left_merge = AABB.merge(self.left.aabb, aabb)
368+
right_merge = AABB.merge(self.right.aabb, aabb)
369+
370+
# Calculate the change in the sum of the bounding volumes
371+
branch_bnd_cost = branch_merge.volume
372+
373+
left_bnd_cost = branch_merge.volume - self.aabb.volume
374+
left_bnd_cost += left_merge.volume - self.left.aabb.volume
375+
376+
right_bnd_cost = branch_merge.volume - self.aabb.volume
377+
right_bnd_cost += right_merge.volume - self.right.aabb.volume
378+
379+
# Calculate amount of overlap
380+
branch_olap_cost = self.aabb.overlap_volume(aabb)
381+
left_olap_cost = left_merge.overlap_volume(self.right.aabb)
382+
right_olap_cost = right_merge.overlap_volume(self.left.aabb)
383+
384+
# Calculate total cost
385+
branch_cost = branch_bnd_cost + branch_olap_cost
386+
left_cost = left_bnd_cost + left_olap_cost
387+
right_cost = right_bnd_cost + right_olap_cost
388+
else:
389+
raise ValueError('Unrecognized method: ' + str(method))
276390

277-
if new_parent_cost < min(cost_left, cost_right):
391+
if branch_cost < left_cost and branch_cost < right_cost:
278392
self.left = copy.deepcopy(self)
279393
self.right = AABBTree(aabb, value)
280394
self.value = None
281-
elif cost_left < cost_right:
395+
elif left_cost < right_cost:
282396
self.left.add(aabb, value)
283397
else:
284398
self.right.add(aabb, value)

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
author = 'Kenneth Hart'
2525

2626
# The short X.Y version
27-
version = '2.1'
27+
version = '2.2'
2828
# The full version, including alpha/beta/rc tags
29-
release = '2.1'
29+
release = '2.2'
3030

3131

3232
# -- General configuration ---------------------------------------------------

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def read(fname):
1515

1616
setup(
1717
name='aabbtree',
18-
version='2.1',
18+
version='2.2',
1919
license='MIT',
2020
description='Pure Python implementation of d-dimensional AABB tree.',
2121
long_description=read('README.rst'),

tests/test_aabbtree.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import itertools
22

3+
import pytest
4+
35
from aabbtree import AABB
46
from aabbtree import AABBTree
57

@@ -22,30 +24,12 @@ def test_init():
2224
assert tree2.right == tree
2325

2426

25-
def test_str():
27+
def test_empty_str():
2628
empty_str = 'AABB: None\nValue: None\nLeft: None\nRight: None'
2729
assert str(AABBTree()) == empty_str
2830

29-
aabb1, aabb2, aabb3, aabb4 = standard_aabbs()
30-
tree = AABBTree()
31-
tree.add(aabb1, 'x')
32-
tree.add(aabb2, 'y')
33-
tree.add(aabb3, 3.14)
34-
tree.add(aabb4)
35-
full_str = 'AABB: [(0, 8), (0, 6)]\nValue: None\nLeft:\n AABB: [(0, 1), (0, 1)]\n Value: x\n Left: None\n Right: None\nRight:\n AABB: [(3, 8), (0, 6)]\n Value: None\n Left:\n AABB: [(3, 4), (0, 1)]\n Value: y\n Left: None\n Right: None\n Right:\n AABB: [(5, 8), (5, 6)]\n Value: None\n Left:\n AABB: [(5, 6), (5, 6)]\n Value: 3.14\n Left: None\n Right: None\n Right:\n AABB: [(7, 8), (5, 6)]\n Value: None\n Left: None\n Right: None' # NOQA: E501
36-
assert str(tree) == full_str
37-
3831

39-
def test_repr():
40-
aabb1, aabb2, aabb3, aabb4 = standard_aabbs()
41-
42-
tree = AABBTree()
43-
tree.add(aabb1, 'x')
44-
tree.add(aabb2, 'y')
45-
tree.add(aabb3, 3.14)
46-
tree.add(aabb4)
47-
48-
assert repr(tree) == "AABBTree(aabb=AABB([(0, 8), (0, 6)]), left=AABBTree(aabb=AABB([(0, 1), (0, 1)]), value='x'), right=AABBTree(aabb=AABB([(3, 8), (0, 6)]), left=AABBTree(aabb=AABB([(3, 4), (0, 1)]), value='y'), right=AABBTree(aabb=AABB([(5, 8), (5, 6)]), left=AABBTree(aabb=AABB([(5, 6), (5, 6)]), value=3.14), right=AABBTree(aabb=AABB([(7, 8), (5, 6)])))))" # NOQA: E501
32+
def test_empty_repr():
4933
assert repr(AABBTree()) == 'AABBTree()'
5034

5135

@@ -73,6 +57,15 @@ def test_eq():
7357
assert not tree2 == tree
7458

7559

60+
def test_len():
61+
tree = AABBTree()
62+
assert len(tree) == 0
63+
64+
for i, aabb in enumerate(standard_aabbs()):
65+
tree.add(aabb)
66+
assert len(tree) == i + 1
67+
68+
7669
def test_is_leaf():
7770
assert AABBTree().is_leaf
7871
assert AABBTree(AABB([(2, 5)])).is_leaf
@@ -94,6 +87,13 @@ def test_add():
9487
assert AABBTree() != tree
9588

9689

90+
def test_add_raises():
91+
tree = AABBTree()
92+
with pytest.raises(ValueError):
93+
for aabb in standard_aabbs():
94+
tree.add(aabb, method=3.14)
95+
96+
9797
def test_add_merge():
9898
aabbs = standard_aabbs()
9999
for indices in itertools.permutations(range(4)):

0 commit comments

Comments
 (0)