From 56d9750b7e434cbe3c81151beb6691c434b78a38 Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Fri, 1 Sep 2017 10:48:45 -0700 Subject: [PATCH 1/8] QuadTree documentation Add documentation for quadtree (next up tests, then implementation) * docs/Data_Structure.rst - add quadtree * pygorithm/data_structures/quadtree.py - add skeleton, documentation * tests/test_data_structure.py - add skeleton for quadtree tests --- docs/Data_Structure.rst | 20 ++ pygorithm/data_structures/quadtree.py | 332 ++++++++++++++++++++++++++ tests/test_data_structure.py | 39 ++- 3 files changed, 390 insertions(+), 1 deletion(-) diff --git a/docs/Data_Structure.rst b/docs/Data_Structure.rst index 0e4f7d1..69b6631 100644 --- a/docs/Data_Structure.rst +++ b/docs/Data_Structure.rst @@ -41,6 +41,8 @@ Features - Check cycle in Undirected Graph (data_structures.graph.CheckCycleUndirectedGraph) - **Heap** - Heap (data_structures.heap.Heap) + - **QuadTree** + - QuadTree (data_structures.quadtree.QuadTree) * Get the code used for any of the implementation @@ -214,3 +216,21 @@ Trie ----- .. autoclass:: Trie :members: + +QuadTree +-------- + +.. automodule:: pygorithm.data_structures.quadtree + + QuadTreeEntity + -------------- + .. autoclass:: QuadTreeEntity + :members: + :special_members: + + QuadTree + -------- + .. autoclass:: QuadTree + :members: + :special_members: + diff --git a/pygorithm/data_structures/quadtree.py b/pygorithm/data_structures/quadtree.py index 7ce7be4..ad2470d 100644 --- a/pygorithm/data_structures/quadtree.py +++ b/pygorithm/data_structures/quadtree.py @@ -9,6 +9,66 @@ from pygorithm.geometry import (vector2, polygon2, rect2) +class QuadTreeEntity(object): + """ + This is the minimum information required for an object to + be usable in a quadtree as an entity. Entities are the + things that you are trying to compare in a quadtree. + + :ivar aabb: the axis-aligned bounding box of this entity + :type aabb: :class:`pygorithm.geometry.rect2.Rect2` + """ + def __init__(self, aabb): + """ + Create a new quad tree entity with the specified aabb + + :param aabb: axis-aligned bounding box + :type aabb: :class:`pygorithm.geometry.rect2.Rect2` + """ + pass + + def __repr__(self): + """ + Create an unambiguous representation of this entity. + + Example: + + .. code-block:: python + + from pygorithm.geometry import (vector2, rect2) + from pygorithm.data_structures import quadtree + + _ent = quadtree.QuadTreeEntity(rect2.Rect2(5, 5)) + + # prints quadtreeentity(aabb=rect2(width=5, height=5, mincorner=vector2(x=0, y=0))) + print(repr(_ent)) + + :returns: unambiguous representation of this quad tree entity + :rtype: string + """ + pass + + def __str__(self): + """ + Create a human readable representation of this entity + + Example: + + .. code-block:: python + + from pygorithm.geometry import (vector2, rect2) + from pygorithm.data_structures import quadtree + + _ent = quadtree.QuadTreeEntity(rect2.Rect2(5, 5)) + + # prints entity(at rect(5x5 at <0, 0>)) + print(str(_ent)) + + :returns: human readable representation of this entity + :rtype: string + """ + pass + class QuadTree(object): """ A quadtree is a sorting tool for two-dimensional space, most @@ -16,7 +76,279 @@ class QuadTree(object): calculations in a two-dimensional scene. In this context, the scene is stepped without collision detection, then a quadtree is constructed from all of the boundaries + + .. caution:: + + Just because a quad tree has split does not mean entities will be empty. Any + entities which overlay any of the lines of the split will be included in the + parent class of the quadtree. + + .. tip:: + + It is important to tweak bucket size and depth to the problem, but a common error + is too small a bucket size. It is typically not reasonable to have a bucket size + smaller than 16; A good starting point is 64, then modify as appropriate. Larger + buckets reduce the overhead of the quad tree which could easily exceed the improvement + from reduced collision checks. The max depth is typically just a sanity check since + depth greater than 4 or 5 would either indicate a badly performing quadtree (too + dense objects, use an r-tree or kd-tree) or a very large world (where an iterative + quadtree implementation would be appropriate). + + :ivar bucket_size: maximum number objects per bucket (before :py:attr:`.max_depth`) + :type bucket_size: int + :ivar max_depth: maximum depth of the quadtree + :type max_depth: int + :ivar depth: the depth of this node (0 being the topmost) + :type depth: int + :ivar location: where this quad tree node is situated + :type location: :class:`pygorithm.geometry.rect2.Rect2` + :ivar entities: the entities in this quad tree and in NO OTHER related quad tree + :type entities: list of :class:`.QuadTreeEntity` + :ivar children: either None or the 4 :class:`.QuadTree` children of this node + :type children: None or list of :class:`.QuadTree` """ + + def __init__(self, bucket_size, max_depth, location, depth = 0, entities = None): + """ + Initialize a new quad tree. + + .. warning:: + + Passing entities to this quadtree will NOT cause it to split automatically! + You must call :py:meth:`.think` for that. This allows for more predictable + performance per line. + + :param bucket_size: the number of entities in this quadtree + :type bucket_size: int + :param max_depth: the maximum depth for automatic splitting + :type max_depth: int + :param location: where this quadtree is located + :type location: :class:`pygorithm.geometry.rect2.Rect2` + :param depth: the depth of this node + :type depth: int + :param entities: the entities to initialize this quadtree with + :type entities: list of :class:`.QuadTreeEntity` or None for empty list + """ + pass + + def think(self, recursive = False): + """ + Call :py:meth:`.split` if appropriate + + Split this quad tree if it has not split already and it has more + entities than :py:attr:`.bucket_size` and :py:attr:`.depth` is + less than :py:attr:`.max_depth`. + + If `recursive` is True, think is called on the :py:attr:`.children` with + recursive set to True after splitting. + + :param recursive: if `think(True)` should be called on :py:attr:`.children` (if there are any) + :type recursive: bool + """ + pass + + def split(self): + """ + Split this quadtree. + + .. caution:: + + A call to split will always split the tree or raise an error. Use + :py:meth:`.think` if you want to ensure the quadtree is operating + efficiently. + + .. caution:: + + This function will not respect :py:attr:`.bucket_size` or + :py:attr:`.max_depth`. + + :raises ValueError: if :py:attr:`.children` is not empty + """ + pass + + def get_quadrant(self, entity): + """ + Calculate the quadrant that the specified entity belongs to. + + Quadrants are: + + - -1: None (it overlaps 2 or more quadrants) + - 0: Top-left + - 1: Top-right + - 2: Bottom-right + - 3: Bottom-left + + .. caution:: + + This function does not verify the entity is contained in this quadtree. + + This operation takes O(1) time. + + :param entity: the entity to place + :type entity: :class:`.QuadTreeEntity` + :returns: quadrant + :rtype: int + """ + pass + + def insert_and_think(self, entity): + """ + Insert the entity into this or the appropriate child. + + This also acts as thinking (recursively). Using insert_and_think + iteratively is slightly less efficient but more predictable performance, + whereas initializing with a large number of entities then thinking is slightly + faster but may hang. Both may exceed recursion depth if max_depth + is too large. + + :param entity: the entity to insert + :type entity: :class:`.QuadTreeEntity` + """ + pass + + def retrieve_collidables(self, entity, predicate = None): + """ + Find all entities that could collide with the specified entity. + + .. warning:: + + If entity is, itself, in the quadtree, it will be returned. The + predicate may be used to prevent this using your preferred equality + method. + + The predicate takes 1 positional argument (the entity being considered) + and returns `False` if the entity should never be returned, even if it + might collide with the entity. It should return `True` otherwise. + + :param entity: the entity to find collidables for + :type entity: :class:`.QuadTreeEntity` + :param predicate: the predicate + :type predicate: :class:`types.FunctionType` or None + :returns: potential collidables (never `None) + :rtype: list of :class:`.QuadTreeEntity` + """ + pass + + def find_entities_per_depth(self): + """ + Calculate the number of nodes and entities at each depth level in this + quad tree. Only returns for depth levels at or equal to this node. + + This is implemented iteratively. See :py:meth:`.__str__` for usage example. + + :returns: dict of depth level to (number of nodes, number of entities) + :rtype: dict int: (int, int) + """ + pass + + def sum_entities(self, entities_per_depth=None): + """ + Sum the number of entities in this quad tree and all lower quad trees. + + If entities_per_depth is not None, that array is used to calculate the sum + of entities rather than traversing the tree. + + :param entities_per_depth: the result of :py:meth:`.find_entities_per_depth` + :type entities_per_depth: `dict int: (int, int)` or None + :returns: number of entities in this and child nodes + :rtype: int + """ + pass + + def calculate_avg_ents_per_leaf(self): + """ + Calculate the average number of entities per leaf node on this and child + quad trees. + + In the ideal case, the average entities per leaf is equal to the bucket size, + implying maximum efficiency. + + This is implemented iteratively. See :py:meth:`.__str__` for usage example. + + :returns: average number of entities at each leaf node + :rtype: :class:`numbers.Number` + """ + pass + + def calculate_weight_misplaced_ents(self, sum_entities=None): + """ + Calculate a rating for misplaced entities. + + A misplaced entity is one that is not on a leaf node. That weight is multiplied + by 4*remaining maximum depth of that node, to indicate approximately how + many additional calculations are required. + + The result is then divided by the total number of entities on this node (either + calculated using :py:meth:`.sum_entities` or provided) to get the approximate + cost of the misplaced nodes in comparison with the placed nodes. A value greater + than 1 implies a different tree type (such as r-tree or kd-tree) should probably be + used. + + :param sum_entities: the number of entities on this node + :type sum_entities: int or None + :returns: weight of misplaced entities + :rtype: :class:`numbers.Number` + """ + pass + + def __repr__(self): + """ + Create an unambiguous, recursive representation of this quad tree. + + Example: + + .. code-block:: python + + from pygorithm.geometry import (vector2, rect2) + from pygorithm.data_structures import quadtree + + # create a tree with a up to 2 entities in a bucket that + # can have a depth of up to 5. + _tree = quadtree.QuadTree(2, 5, rect2.Rect2(100, 100)) + + # add a few entities to the tree + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) + + # prints quadtree(bucket_size=2, max_depth=5, location=rect2(width=100, height=100, mincorner=vector2(x=0, y=0)), depth=0, entities=[], children=[ quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=5, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=95, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=50)), depth=1, entities=[], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=50)), depth=1, entities=[], children=[]) ]) + print(repr(_tree)) + + :returns: unambiguous, recursive representation of this quad tree + :rtype: string + """ + pass + + def __str__(self): + """ + Create a human-readable representation of this quad tree + + .. caution:: + + Because of the complexity of quadtrees it takes a fair amount of calculation to + produce something somewhat legible. All returned statistics have paired functions + + Example: + + .. code-block:: python + + from pygorithm.geometry import (vector2, rect2) + from pygorithm.data_structures import quadtree + + # create a tree with a up to 2 entities in a bucket that + # can have a depth of up to 5. + _tree = quadtree.QuadTree(2, 5, rect2.Rect2(100, 100)) + + # add a few entities to the tree + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) + + # prints quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (max depth: 5), avg ent/leaf: 0.5 (target 2), misplaced weight = 0 (0 best, >1 bad)) + + :returns: human-readable representation of this quad tree + :rtype: string + """ + pass + @staticmethod def get_code(): """ diff --git a/tests/test_data_structure.py b/tests/test_data_structure.py index 5e99135..c1fe5cd 100644 --- a/tests/test_data_structure.py +++ b/tests/test_data_structure.py @@ -365,6 +365,43 @@ def test_stack(self): self.assertEqual(myTrie.search('flying'), True) self.assertEqual(myTrie.search('walking'), False) - +class TestQuadTreeNode(unittest.TestCase): + def test_constructor(self): + pass + def test_repr(self): + pass + def test_str(self): + pass + +class TestQuadTree(unittest.TestCase): + def test_constructor(self): + pass + def test_get_quadrant(self): + pass + def test_split(self): + pass + def test_think(self): + pass + def test_insert(self): + pass + def test_retrieve(self): + pass + def test_ents_per_depth(self): + pass + def test_sum_ents_noparam(self): + pass + def test_sum_ents_param(self): + pass + def test_avg_ents_per_leaf(self): + pass + def test_misplaced_ents_noparam(self): + pass + def test_misplaced_ents_param(self): + pass + def test_repr(self): + pass + def test_str(self): + pass + if __name__ == '__main__': unittest.main() From d4281b2e227bb5b74d3429cd2e01f1f4a5b700f3 Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Fri, 1 Sep 2017 10:52:15 -0700 Subject: [PATCH 2/8] Fix sphinx documentation * docs/Data_Structure.rst - special_members -> special-members * tests/test_data_structure.py - import quadtree (so compile checks) --- docs/Data_Structure.rst | 4 ++-- tests/test_data_structure.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/Data_Structure.rst b/docs/Data_Structure.rst index 69b6631..f412ea9 100644 --- a/docs/Data_Structure.rst +++ b/docs/Data_Structure.rst @@ -226,11 +226,11 @@ QuadTree -------------- .. autoclass:: QuadTreeEntity :members: - :special_members: + :special-members: QuadTree -------- .. autoclass:: QuadTree :members: - :special_members: + :special-members: diff --git a/tests/test_data_structure.py b/tests/test_data_structure.py index c1fe5cd..637d77f 100644 --- a/tests/test_data_structure.py +++ b/tests/test_data_structure.py @@ -8,7 +8,8 @@ tree, graph, heap, - trie) + trie, + quadtree) class TestStack(unittest.TestCase): From 64cbda0984ef4e8ebe3e39e1ff46884d98392de0 Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Fri, 1 Sep 2017 11:00:04 -0700 Subject: [PATCH 3/8] Minor quadtree documentation improvements * pygorithm/data_structures/quadtree.py - minor doc changes --- pygorithm/data_structures/quadtree.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pygorithm/data_structures/quadtree.py b/pygorithm/data_structures/quadtree.py index ad2470d..6d391d0 100644 --- a/pygorithm/data_structures/quadtree.py +++ b/pygorithm/data_structures/quadtree.py @@ -81,7 +81,7 @@ class QuadTree(object): Just because a quad tree has split does not mean entities will be empty. Any entities which overlay any of the lines of the split will be included in the - parent class of the quadtree. + parent of the quadtree. .. tip:: @@ -178,7 +178,7 @@ def get_quadrant(self, entity): - 2: Bottom-right - 3: Bottom-left - .. caution:: + .. caution:: This function does not verify the entity is contained in this quadtree. @@ -195,10 +195,10 @@ def insert_and_think(self, entity): """ Insert the entity into this or the appropriate child. - This also acts as thinking (recursively). Using insert_and_think - iteratively is slightly less efficient but more predictable performance, - whereas initializing with a large number of entities then thinking is slightly - faster but may hang. Both may exceed recursion depth if max_depth + This also acts as thinking (recursively). Using :py:meth:`.insert_and_think` + iteratively is slightly less efficient but has more predictable performance + than initializing with a large number of entities then thinking is slightly + faster but may hang. Both may exceed recursion depth if :py:attr:`.max_depth` is too large. :param entity: the entity to insert @@ -245,8 +245,9 @@ def sum_entities(self, entities_per_depth=None): """ Sum the number of entities in this quad tree and all lower quad trees. - If entities_per_depth is not None, that array is used to calculate the sum - of entities rather than traversing the tree. + If `entities_per_depth` is not None, that array is used to calculate the sum + of entities rather than traversing the tree. Either way, this is implemented + iteratively. See :py:meth:`.__str__` for usage example. :param entities_per_depth: the result of :py:meth:`.find_entities_per_depth` :type entities_per_depth: `dict int: (int, int)` or None @@ -284,6 +285,8 @@ def calculate_weight_misplaced_ents(self, sum_entities=None): than 1 implies a different tree type (such as r-tree or kd-tree) should probably be used. + This is implemented iteratively. See :py:meth:`.__str__` for usage example. + :param sum_entities: the number of entities on this node :type sum_entities: int or None :returns: weight of misplaced entities @@ -325,7 +328,8 @@ def __str__(self): .. caution:: Because of the complexity of quadtrees it takes a fair amount of calculation to - produce something somewhat legible. All returned statistics have paired functions + produce something somewhat legible. All returned statistics have paired functions. + This uses only iterative algorithms to calculate statistics. Example: From 817937103fa51ab6ae3bb883012fd8d03f6caa28 Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Sat, 2 Sep 2017 09:34:44 -0700 Subject: [PATCH 4/8] Finish quadtree tests * pygorithm/data_structures/quadtree.py - minor documentation changes * tests/test_data_structure.py - implement quadtree tests --- pygorithm/data_structures/quadtree.py | 3 +- tests/test_data_structure.py | 389 ++++++++++++++++++++++++-- 2 files changed, 369 insertions(+), 23 deletions(-) diff --git a/pygorithm/data_structures/quadtree.py b/pygorithm/data_structures/quadtree.py index 6d391d0..4b2dd35 100644 --- a/pygorithm/data_structures/quadtree.py +++ b/pygorithm/data_structures/quadtree.py @@ -262,7 +262,8 @@ def calculate_avg_ents_per_leaf(self): quad trees. In the ideal case, the average entities per leaf is equal to the bucket size, - implying maximum efficiency. + implying maximum efficiency. Note that, as always with averages, this might + be misleading if this tree has reached its max depth. This is implemented iteratively. See :py:meth:`.__str__` for usage example. diff --git a/tests/test_data_structure.py b/tests/test_data_structure.py index 637d77f..60769cd 100644 --- a/tests/test_data_structure.py +++ b/tests/test_data_structure.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import unittest +import random from pygorithm.data_structures import ( stack, @@ -367,42 +368,386 @@ def test_stack(self): self.assertEqual(myTrie.search('walking'), False) class TestQuadTreeNode(unittest.TestCase): + def setUp(self): + self.rect1 = rect2.Rect2(1, 1, vector2.Vector2(2, 2)) + def test_constructor(self): - pass + ent = quadtree.QuadTreeEntity(rect1) + + self.assertIsNotNone(ent.aabb) + self.assertEqual(1, ent.aabb.width) + self.assertEqual(1, ent.aabb.height) + self.assertEqual(2, ent.aabb.mincorner.x) + self.assertEqual(2, ent.aabb.mincorner.y) + def test_repr(self): - pass + ent = quadtree.QuadTreeEntity(rect1) + + exp = "quadtreeentity(aabb=rect2(width=1, height=1, mincorner=vector2(x=2, y=2)))" + self.assertEqual(exp, repr(ent)) + def test_str(self): - pass + ent = quadtree.QuadTreeEntity(rect1) + + exp = "entity(at rect(1x1 at <2, 2>))" + self.assertEqual(exp, str(ent)) class TestQuadTree(unittest.TestCase): + def setUp(self): + self.big_rect = rect2.Rect2(1000, 1000) + self.big_rect_sub_1 = rect2.Rect2(500, 500) + self.big_rect_sub_2 = rect2.Rect2(500, 500, vector2.Vector2(500, 0)) + self.big_rect_sub_3 = rect2.Rect2(500, 500, vector2.Vector2(500, 500)) + self.big_rect_sub_4 = rect2.Rect2(500, 500, vector2.Vector2(0, 500)) + random.seed() + + def test_constructor(self): - pass + _tree = quadtree.QuadTree(64, 5, self.big_rect) + + self.assertEqual(64, _tree.bucket_size) + self.assertEqual(5, _tree.max_depth) + self.assertEqual(1000, _tree.location.width) + self.assertEqual(1000, _tree.location.height) + self.assertEqual(0, _tree.location.mincorner.x) + self.assertEqual(0, _tree.location.mincorner.y) + self.assertEqual(0, _tree.depth) + self.assertIsNotNone(_tree.entities) + self.assertEqual(0, len(_tree.entities)) + self.assertIsNone(_tree.children) + def test_get_quadrant(self): - pass - def test_split(self): - pass + _tree = quadtree.QuadTree(64, 5, self.big_rect) + + ent1 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(320, 175)) + quad1 = _tree.get_quadrant(ent1) + self.assertEqual(0, quad1) + + ent2 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(600, 450)) + quad2 = _tree.get_quadrant(ent2) + self.assertEqual(1, quad2) + + ent3 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(700, 950)) + quad3 = _tree.get_quadrant(ent3) + self.assertEqual(2, quad3) + + ent4 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(0, 495)) + quad4 = _tree.get_quadrant(ent4) + self.assertEqual(3, quad4) + + def test_get_quadrant_none(self): + _tree = quadtree.QuadTree(64, 5, self.big_rect) + + ent1 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(497, 150)) + self.assertEqual(-1, _tree.get_quadrant(ent1)) + + ent2 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(800, 499)) + self.assertEqual(-1, _tree.get_quadrant(ent2)) + + ent3 = quadtree.QuadTreeEntity(15, 15, vector2.Vector2(481, 505)) + self.assertEqual(-1, _tree.get_quadrant(ent3)) + + ent4 = quadtree.QuadTreeEntity(5, 20, vector2.Vector2(15, 490)) + self.assertEqual(-1, _tree.get_quadrant(ent4)) + + ent5 = quadtree.QuadTreeEntity(17, 34, vector2.Vector2(485, 470)) + self.assertEqual(-1, _tree.get_quadrant(ent5)) + + def test_get_quadrant_shifted(self): + _tree = quadtree.QuadTree(64, 5, self.big_rect_sub_3) + + ent1 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(515, 600)) + self.assertEqual(0, _tree.get_quadrant(ent1)) + + ent2 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(800, 550)) + self.assertEqual(1, _tree.get_quadrant(ent2)) + + ent3 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(950, 650)) + self.assertEqual(2, _tree.get_quadrant(ent3)) + + ent4 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(15, 551)) + self.assertEqual(3, _tree.get_quadrant(ent4)) + + def test_get_quadrant_0_shifted(self): + _tree = quadtree.QuadTree(64, 5, rect2.Rect2(500, 800, vector2.Vector2(200, 200))) + + ent1 = quadtree.QuadTreeEntity(5, 10, vector2.Vector2(445, 224)) + self.assertEqual(-1, _tree.get_quadrant(ent1)) + + ent2 = quadtree.QuadTreeEntity(11, 17, vector2.Vector2(515, 585)) + self.assertEqual(-1, _tree.get_quadrant(ent2)) + + ent3 = quadtree.QuadTreeEntity(20, 20, vector2.Vector2(440, 700)) + self.assertEqual(-1, _tree.get_quadrant(ent3)) + + ent4 = quadtree.QuadTreeEntity(15, 15, vector2.Vector2(215, 590)) + self.assertEqual(-1, _tree.get_quadrant(ent4)) + + ent5 = quadtree.QuadTreeEntity(7, 12, vector2.Vector2(449, 589)) + self.assertEqual(-1, _tree.get_quadrant(ent5)) + + def test_split_empty(self): + _tree1 = quadtree.QuadTree(64, 5, self.big_rect) + self.assertIsNone(_tree1.children) + _tree1.split() + self.assertIsNotNone(_tree1.children) + self.assertEqual(4, len(_tree1.children)) + + self.assertEqual(500, _tree1.children[0].width) + self.assertEqual(500, _tree1.children[0].height) + self.assertEqual(0, _tree1.children[0].mincorner.x) + self.assertEqual(0, _tree1.children[0].mincorner.y) + self.assertEqual(1, _tree1.children[0].depth) + self.assertEqual(64, _tree1.children[0].bucket_size) + self.assertEqual(5, _tree1.children[0].max_depth) + + self.assertEqual(500, _tree1.children[1].width) + self.assertEqual(500, _tree1.children[1].height) + self.assertEqual(500, _tree1.children[1].mincorner.x) + self.assertEqual(0, _tree1.children[1].mincorner.y) + + self.assertEqual(500, _tree1.children[2].width) + self.assertEqual(500, _tree1.children[2].height) + self.assertEqual(500, _tree1.children[2].mincorner.x) + self.assertEqual(500, _tree1.children[2].mincorner.y) + + self.assertEqual(500, _tree1.children[3].width) + self.assertEqual(500, _tree1.children[3].height) + self.assertEqual(0, _tree1.children[3].mincorner.x) + self.assertEqual(500, _tree1.children[3].mincorner.y) + + + _tree2 = _tree1.children[3] + _tree2.split() + + self.assertEqual(250, _tree2.children[0].width) + self.assertEqual(250, _tree2.children[0].height) + self.assertEqual(500, _tree2.children[0].mincorner.x) + self.assertEqual(500, _tree2.children[0].mincorner.y) + self.assertEqual(2, _tree2.children[0].depth) + + self.assertEqual(250, _tree2.children[1].width) + self.assertEqual(250, _tree2.children[1].height) + self.assertEqual(750, _tree2.children[1].mincorner.x) + self.assertEqual(500, _tree2.children[1].mincorner.y) + + self.assertEqual(250, _tree2.children[2].width) + self.assertEqual(250, _tree2.children[2].height) + self.assertEqual(750, _tree2.children[2].mincorner.x) + self.assertEqual(750, _tree2.children[2].mincorner.y) + + self.assertEqual(250, _tree2.children[3].width) + self.assertEqual(250, _tree2.children[3].height) + self.assertEqual(500, _tree2.children[3].mincorner.x) + self.assertEqual(750, _tree2.children[3].mincorner.y) + + def test_split_entities(self): + + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(50, 50))) + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(550, 75))) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(565, 585))) + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(95, 900))) + ent5 = quadtree.QuadTreeEntity(rect2.Rect2(10, 10, vector2.Vector2(495, 167))) + + _tree = quadtree.QuadTree(64, 5, self.big_rect, entities = [ ent1, ent2, ent3, ent4, ent5 ]) + _tree.split() + + self.assertEqual(1, len(_tree.children[0].entities)) + self.assertEqual(50, _tree.children[0].entities[0].mincorner.x) + self.assertEqual(50, _tree.children[0].entities[0].mincorner.y) + + self.assertEqual(1, len(_tree.children[1].entities)) + self.assertEqual(550, _tree.children[1].entities[0].mincorner.x) + self.assertEqual(75, _tree.children[1].entities[0].mincorner.y) + + self.assertEqual(1, len(_tree.children[2].entities)) + self.assertEqual(565, _tree.children[2].entities[0].mincorner.x) + self.assertEqual(585, _tree.children[2].entities[0].mincorner.y) + + self.assertEqual(1, len(_tree.children[3].entities)) + self.assertEqual(95, _tree.children[3].entities[0].mincorner.x) + self.assertEqual(900, _tree.children[3].entities[0].mincorner.y) + + self.assertEqual(1, len(_tree.entities)) + self.assertEqual(495, _tree.entities[0].mincorner.x) + self.assertEqual(167, _tree.entities[0].mincorner.y) + + _tree2 = _tree.children[3] + _tree2.split() + + for i in range(3): + self.assertEqual(0, len(_tree2.children[i].entities), msg="i={}".format(i)) + + self.assertEqual(1, len(_tree2.children[3].entities)) + self.assertEqual(95, _tree2.children[3].entities[0].mincorner.x) + self.assertEqual(900, _tree2.children[3].entities[0].mincorner.y) + + # note for test_think and test_insert we're testing the worst-case scenario + # for a quad tree (everythings all bunched up in a corner) hence the instant + # flow to max depth. this case is why max_depth is necessary. To see why you + # don't need that much max_depth, the rect sizes are + # 1000 (depth 0), 500 (depth 1), 250 (depth 2), 125 (depth 3), 62.5 (depth 4), + # 31.25 (depth 5), 15.625 (depth 6), etc. As you can see, they would have to be + # extremely bunched (or stacked) and tiny to actually cause a stack overflow (in the + # examples it's only 6 deep), but the quadtree isn't improving anything + # (even at 1000x1000 world!) past depth 5 or so. + def test_think(self): - pass + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(15, 15))) + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(20, 20))) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(0, 0))) + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(5, 0))) + ent5 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(0, 5))) + _tree = quadtree.QuadTree(2, 2, self.big_rect, entities = [ ent1, ent2, ent3, ent4, ent5 ]) + _tree.think(True) + + self.assertIsNotNone(_tree.children) # depth 0 + self.assertIsNotNone(_tree.children[0].children) # depth 1 + self.assertIsNotNone(_tree.children[0].children[0].children) # depth 2 + self.assertIsNone(_tree.children[0].children[0].children[0].children) # depth 3 shouldn't happen because + self.assertEqual(5, len(_tree.children[0].children[0].children[0].entities)) # max_depth reached + def test_insert(self): - pass + _tree = quadtree.QuadTree(2, 2, self.big_rect) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(15, 15)))) + self.assertIsNone(_tree.children) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(20, 20)))) + self.assertIsNone(_tree.children) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(0, 0)))) + self.assertIsNotNone(_tree.children) # depth 0 + self.assertIsNotNone(_tree.children[0].children) # depth 1 + self.assertIsNotNone(_tree.children[0].children[0].children) # depth 2 + self.assertIsNone(_tree.children[0].children[0].children[0].children) # depth 3 shouldn't happen because + self.assertEqual(3, len(_tree.children[0].children[0].entities)) # max_depth reached + def test_retrieve(self): - pass + _tree = quadtree.QuadTree(2, 2, self.big_rect) + + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(25, 25))) + _tree.insert_and_think(ent1) + + retr = _tree.retrieve_collidables(ent1) + self.assertIsNotNone(retr) + self.assertEqual(1, len(retr)) + self.assertEqual(25, retr.mincorner.x) + self.assertEqual(25, retr.mincorner.y) + + # note this is not nicely in a quadrant + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(20, 10, vector2.Vector2(490, 300))) + _tree.insert_and_think(ent2) + + retr = _tree.retrieve_collidables(ent1) + self.assertIsNotNone(retr) + self.assertEqual(2, len(retr)) # both ent1 and ent2 are "collidable" in this quad tree + + # this should cause a split (bucket_size) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(15, 10, vector2.Vector2(700, 450))) + _tree.insert_and_think(ent3) + + # ent1 should collide with ent1 or ent2, + # ent2 with ent1, ent2, or ent3 + # ent3 with ent2 or ent3 + retr = _tree.retrieve_collidables(ent1) + self.assertIsNotNone(retr) + self.assertEqual(2, len(retr)) + self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 25), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 490), None), str(retr)) + + retr = _tree.retrieve_collidables(ent2) + self.assertEqual(3, len(retr)) + self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 25), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 490), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 700), None), str(retr)) + + retr = _tree.retrieve_collidables(ent3) + self.assertEqual(2, len(retr)) + self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 490), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 700), None), str(retr)) + def test_ents_per_depth(self): - pass - def test_sum_ents_noparam(self): - pass - def test_sum_ents_param(self): - pass + _tree = quadtree.QuadTree(3, 5, self.big_rect) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75, 35)))) + self.assertDictEqual({ 0: 1 }, _tree.find_entities_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(300, 499)))) + self.assertDictEqual({ 0: 2 }, _tree.find_entities_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 600)))) + self.assertDictEqual({ 0: 1, 1: 2 }, _tree.find_entities_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 300)))) + self.assertDictEqual({ 0: 1, 1: 3 }, _tree.find_entities_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(150, 100)))) + self.assertDictEqual({ 0: 1, 1: 1, 2: 3 }, _tree.find_entities_per_depth()) + + def test_sum_ents(self): + # it shouldn't matter where we put entities in, adding entities + # to a quadtree should increment this number by 1. So lets fuzz! + + _tree = quadtree.QuadTree(64, 5, self.big_rect) + for i in range(1000): + w = random.randrange(1, 10) + h = random.randrange(1, 10) + x = random.uniform(0, 1000 - w) + y = random.uniform(0, 1000 - h) + ent = quadtree.QuadTreeEntity(w, h, vector2.Vector2(x, y)) + _tree.insert_and_think(ent) + + # avoid calculating sum every loop which would take way too long. + # on average, try to sum about 50 times total (5% of the time), + # evenly split between both ways of summing + rnd = random.random() + if rnd > 0.95 and rnd <= 0.975: + _sum = _tree.sum_entities() + self.assertEqual(i+1, _sum) + elif rnd > 0.975: + _sum = _tree.sum_entities(_tree.find_entities_per_depth()) + self.assertEqual(i+1, _sum) + def test_avg_ents_per_leaf(self): - pass - def test_misplaced_ents_noparam(self): - pass - def test_misplaced_ents_param(self): - pass + _tree = quadtree.QuadTree(3, 5, self.big_rect) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75, 35)))) + self.assertEqual(1, _tree.calculate_avg_ents_per_leaf()) # 1 ent on 1 leaf + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(300, 499)))) + self.asserttEqual(2, _tree.calculate_avg_ents_per_leaf()) # 2 ents 1 leaf + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 600)))) + self.asserttEqual(0.5, _tree.calculate_avg_ents_per_leaf()) # 2 ents 4 leafs (1 misplaced) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 300)))) + self.asserttEqual(0.75, _tree.calculate_avg_ents_per_leaf()) # 3 ents 4 leafs (1 misplaced) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(150, 100)))) + self.asserttEqual(0.5, _tree.calculate_avg_ents_per_leaf()) # 4 ents 8 leafs (1 misplaced) + + def test_misplaced_ents(self): + _tree = quadtree.QuadTree(3, 5, self.big_rect) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75, 35)))) + self.assertEqual(0, _tree.calculate_weight_misplaced_ents()) # 0 misplaced, 1 total + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(300, 499)))) + self.asserttEqual(0, _tree.calculate_weight_misplaced_ents()) # 0 misplaced, 2 total + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 600)))) + self.assertAlmostEqual(4/3, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (1 deep), 3 total + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 300)))) + self.assertAlmostEqual(1, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (1 deep), 4 total + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(150, 100)))) + self.assertAlmostEqual(8/5, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (2 deep), 5 total + def test_repr(self): - pass + _tree = quadtree.QuadTree(2, 5, rect2.Rect2(100, 100)) + + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) + + _olddiff = self.maxDiff + def cleanup(self2): + self2.maxDiff = _olddiff + + self.addCleanup(cleanup) + self.maxDiff = None + self.assertEqual("quadtree(bucket_size=2, max_depth=5, location=rect2(width=100, height=100, mincorner=vector2(x=0, y=0)), depth=0, entities=[], children=[ quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=5, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=95, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=50)), depth=1, entities=[], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=50)), depth=1, entities=[], children=[]) ])", repr(_tree)) + def test_str(self): - pass + _tree = quadtree.QuadTree(2, 5, rect2.Rect2(100, 100)) + + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) + + self.assertEqual("quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (max depth: 5), avg ent/leaf: 0.5 (target 2), misplaced weight = 0 (0 best, >1 bad))", str(_tree)) if __name__ == '__main__': unittest.main() From b785d6f94e2524176ccee6917ba05750a24869ae Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Sun, 3 Sep 2017 09:55:56 -0700 Subject: [PATCH 5/8] Implement quadtree All tests passing. Minor changes to some function definitions, split up find_entities_per_depth (to fit tests). * pygorithm/data_structures/quadtree.py - implement * tests/test_data_structure.py - fix and expand quadtree tests --- pygorithm/data_structures/quadtree.py | 237 ++++++++++++++++++++--- tests/test_data_structure.py | 261 +++++++++++++++----------- 2 files changed, 365 insertions(+), 133 deletions(-) diff --git a/pygorithm/data_structures/quadtree.py b/pygorithm/data_structures/quadtree.py index 4b2dd35..f0e0fe3 100644 --- a/pygorithm/data_structures/quadtree.py +++ b/pygorithm/data_structures/quadtree.py @@ -6,6 +6,8 @@ depth and bucket size. """ import inspect +import math +from collections import deque from pygorithm.geometry import (vector2, polygon2, rect2) @@ -25,7 +27,7 @@ def __init__(self, aabb): :param aabb: axis-aligned bounding box :type aabb: :class:`pygorithm.geometry.rect2.Rect2` """ - pass + self.aabb = aabb def __repr__(self): """ @@ -46,7 +48,7 @@ def __repr__(self): :returns: unambiguous representation of this quad tree entity :rtype: string """ - pass + return "quadtreeentity(aabb={})".format(repr(self.aabb)) def __str__(self): """ @@ -67,7 +69,7 @@ def __str__(self): :returns: human readable representation of this entity :rtype: string """ - pass + return "entity(at {})".format(str(self.aabb)) class QuadTree(object): """ @@ -129,7 +131,12 @@ def __init__(self, bucket_size, max_depth, location, depth = 0, entities = None) :param entities: the entities to initialize this quadtree with :type entities: list of :class:`.QuadTreeEntity` or None for empty list """ - pass + self.bucket_size = bucket_size + self.max_depth = max_depth + self.location = location + self.depth = depth + self.entities = entities if entities is not None else [] + self.children = None def think(self, recursive = False): """ @@ -145,7 +152,13 @@ def think(self, recursive = False): :param recursive: if `think(True)` should be called on :py:attr:`.children` (if there are any) :type recursive: bool """ - pass + if not self.children and self.depth < self.max_depth and len(self.entities) > self.bucket_size: + self.split() + + if recursive: + if self.children: + for child in self.children: + child.think(True) def split(self): """ @@ -164,12 +177,43 @@ def split(self): :raises ValueError: if :py:attr:`.children` is not empty """ - pass + if self.children: + raise ValueError("cannot split twice") + + _cls = type(self) + def _cstr(r): + return _cls(self.bucket_size, self.max_depth, r, self.depth + 1) + + _halfwidth = self.location.width / 2 + _halfheight = self.location.height / 2 + _x = self.location.mincorner.x + _y = self.location.mincorner.y + + self.children = [ + _cstr(rect2.Rect2(_halfwidth, _halfheight, vector2.Vector2(_x, _y))), + _cstr(rect2.Rect2(_halfwidth, _halfheight, vector2.Vector2(_x + _halfwidth, _y))), + _cstr(rect2.Rect2(_halfwidth, _halfheight, vector2.Vector2(_x + _halfwidth, _y + _halfheight))), + _cstr(rect2.Rect2(_halfwidth, _halfheight, vector2.Vector2(_x, _y + _halfheight))) ] + + _newents = [] + for ent in self.entities: + quad = self.get_quadrant(ent) + + if quad < 0: + _newents.append(ent) + else: + self.children[quad].entities.append(ent) + self.entities = _newents + + def get_quadrant(self, entity): """ Calculate the quadrant that the specified entity belongs to. + Touching a line is considered overlapping a line. Touching is + determined using :py:meth:`math.isclose` + Quadrants are: - -1: None (it overlaps 2 or more quadrants) @@ -189,7 +233,48 @@ def get_quadrant(self, entity): :returns: quadrant :rtype: int """ - pass + + _aabb = entity.aabb + _halfwidth = self.location.width / 2 + _halfheight = self.location.height / 2 + _x = self.location.mincorner.x + _y = self.location.mincorner.y + + if math.isclose(_aabb.mincorner.x, _x + _halfwidth): + return -1 + if math.isclose(_aabb.mincorner.x + _aabb.width, _x + _halfwidth): + return -1 + if math.isclose(_aabb.mincorner.y, _y + _halfheight): + return -1 + if math.isclose(_aabb.mincorner.y + _aabb.height, _y + _halfheight): + return -1 + + _leftside_isleft = _aabb.mincorner.x < _x + _halfwidth + _rightside_isleft = _aabb.mincorner.x + _aabb.width < _x + _halfwidth + + if _leftside_isleft != _rightside_isleft: + return -1 + + _topside_istop = _aabb.mincorner.y < _y + _halfheight + _botside_istop = _aabb.mincorner.y + _aabb.height < _y + _halfheight + + if _topside_istop != _botside_istop: + return -1 + + _left = _leftside_isleft + _top = _topside_istop + + if _left: + if _top: + return 0 + else: + return 3 + else: + if _top: + return 1 + else: + return 2 + def insert_and_think(self, entity): """ @@ -204,7 +289,14 @@ def insert_and_think(self, entity): :param entity: the entity to insert :type entity: :class:`.QuadTreeEntity` """ - pass + if not self.children and len(self.entities) == self.bucket_size and self.depth < self.max_depth: + self.split() + + quad = self.get_quadrant(entity) if self.children else -1 + if quad < 0: + self.entities.append(entity) + else: + self.children[quad].insert_and_think(entity) def retrieve_collidables(self, entity, predicate = None): """ @@ -227,8 +319,40 @@ def retrieve_collidables(self, entity, predicate = None): :returns: potential collidables (never `None) :rtype: list of :class:`.QuadTreeEntity` """ - pass + result = list(filter(predicate, self.entities)) + quadrant = self.get_quadrant(entity) if self.children else -1 + if quadrant >= 0: + result.extend(self.children[quadrant].retrieve_collidables(entity, predicate)) + elif self.children: + for child in self.children: + touching, overlapping, alwaysNone = rect2.Rect2.find_intersection(entity.aabb, child.location, find_mtv=False) + if touching or overlapping: + result.extend(child.retrieve_collidables(entity, predicate)) + + return result + + def _iter_helper(self, pred): + """ + Calls pred on each child and childs child, iteratively. + + pred takes one positional argument (the child). + + :param pred: function to call + :type pred: `types.FunctionType` + """ + + _stack = deque() + _stack.append(self) + + while _stack: + curr = _stack.pop() + if curr.children: + for child in curr.children: + _stack.append(child) + + pred(curr) + def find_entities_per_depth(self): """ Calculate the number of nodes and entities at each depth level in this @@ -236,10 +360,30 @@ def find_entities_per_depth(self): This is implemented iteratively. See :py:meth:`.__str__` for usage example. - :returns: dict of depth level to (number of nodes, number of entities) - :rtype: dict int: (int, int) + :returns: dict of depth level to number of entities + :rtype: dict int: int + """ + + container = { 'result': {} } + def handler(curr, container=container): + container['result'][curr.depth] = container['result'].get(curr.depth, 0) + len(curr.entities) + self._iter_helper(handler) + + return container['result'] + + def find_nodes_per_depth(self): + """ + Calculate the number of nodes at each depth level. + + This is implemented iteratively. See :py:meth:`.__str__` for usage example. + + :returns: dict of depth level to number of nodes + :rtype: dict int: int """ - pass + + nodes_per_depth = {} + self._iter_helper(lambda curr, d=nodes_per_depth: d.update({ (curr.depth, d.get(curr.depth, 0) + 1) })) + return nodes_per_depth def sum_entities(self, entities_per_depth=None): """ @@ -254,7 +398,15 @@ def sum_entities(self, entities_per_depth=None): :returns: number of entities in this and child nodes :rtype: int """ - pass + if entities_per_depth is not None: + return sum(entities_per_depth.values()) + + container = { 'result': 0 } + def handler(curr, container=container): + container['result'] += len(curr.entities) + self._iter_helper(handler) + + return container['result'] def calculate_avg_ents_per_leaf(self): """ @@ -270,7 +422,13 @@ def calculate_avg_ents_per_leaf(self): :returns: average number of entities at each leaf node :rtype: :class:`numbers.Number` """ - pass + container = { 'leafs': 0, 'total': 0 } + def handler(curr, container=container): + if not curr.children: + container['leafs'] += 1 + container['total'] += len(curr.entities) + self._iter_helper(handler) + return container['total'] / container['leafs'] def calculate_weight_misplaced_ents(self, sum_entities=None): """ @@ -293,8 +451,35 @@ def calculate_weight_misplaced_ents(self, sum_entities=None): :returns: weight of misplaced entities :rtype: :class:`numbers.Number` """ - pass + # this iteration requires more context than _iter_helper provides. + # we must keep track of parents as well in order to correctly update + # weights + + nonleaf_to_max_child_depth_dict = {} + + # stack will be (quadtree, list (of parents) or None) + _stack = deque() + _stack.append((self, None)) + while _stack: + curr, parents = _stack.pop() + if parents: + for p in parents: + nonleaf_to_max_child_depth_dict[p] = max(nonleaf_to_max_child_depth_dict.get(p, 0), curr.depth) + + if curr.children: + new_parents = list(parents) if parents else [] + new_parents.append(curr) + for child in curr.children: + _stack.append((child, new_parents)) + + _weight = 0 + for nonleaf, maxchilddepth in nonleaf_to_max_child_depth_dict.items(): + _weight += len(nonleaf.entities) * 4 * (maxchilddepth - nonleaf.depth) + + _sum = self.sum_entities() if sum_entities is None else sum_entities + return _weight / _sum + def __repr__(self): """ Create an unambiguous, recursive representation of this quad tree. @@ -308,19 +493,18 @@ def __repr__(self): # create a tree with a up to 2 entities in a bucket that # can have a depth of up to 5. - _tree = quadtree.QuadTree(2, 5, rect2.Rect2(100, 100)) + _tree = quadtree.QuadTree(1, 5, rect2.Rect2(100, 100)) # add a few entities to the tree _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) - # prints quadtree(bucket_size=2, max_depth=5, location=rect2(width=100, height=100, mincorner=vector2(x=0, y=0)), depth=0, entities=[], children=[ quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=5, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=95, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=50)), depth=1, entities=[], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=50)), depth=1, entities=[], children=[]) ]) - print(repr(_tree)) + # prints quadtree(bucket_size=1, max_depth=5, location=rect2(width=100, height=100, mincorner=vector2(x=0, y=0)), depth=0, entities=[], children=[quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=0, y=0)), depth=1, entities=[quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=5, y=5)))], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=50.0, y=0)), depth=1, entities=[quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=95, y=5)))], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=50.0, y=50.0)), depth=1, entities=[], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=0, y=50.0)), depth=1, entities=[], children=None)]) :returns: unambiguous, recursive representation of this quad tree :rtype: string """ - pass + return "quadtree(bucket_size={}, max_depth={}, location={}, depth={}, entities={}, children={})".format(self.bucket_size, self.max_depth, repr(self.location), self.depth, self.entities, self.children) def __str__(self): """ @@ -347,12 +531,23 @@ def __str__(self): _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) - # prints quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (max depth: 5), avg ent/leaf: 0.5 (target 2), misplaced weight = 0 (0 best, >1 bad)) + # prints quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (allowed max depth: 1, actual: 5), avg ent/leaf: 0.5 (target 1), misplaced weight 0.0 (0 best, >1 bad) + print(_tree) :returns: human-readable representation of this quad tree :rtype: string """ - pass + + nodes_per_depth = self.find_nodes_per_depth() + _ents_per_depth = self.find_entities_per_depth() + + _nodes_ents_per_depth_str = "[ {} ]".format(', '.join("{}: ({}, {})".format(dep, nodes_per_depth[dep], _ents_per_depth[dep]) for dep in nodes_per_depth.keys())) + + _sum = self.sum_entities(entities_per_depth=_ents_per_depth) + _max_depth = max(_ents_per_depth.keys()) + _avg_ent_leaf = self.calculate_avg_ents_per_leaf() + _mispl_weight = self.calculate_weight_misplaced_ents(sum_entities=_sum) + return "quadtree(at {} with {} entities here ({} in total); (nodes, entities) per depth: {} (allowed max depth: {}, actual: {}), avg ent/leaf: {} (target {}), misplaced weight {} (0 best, >1 bad)".format(self.location, len(self.entities), _sum, _nodes_ents_per_depth_str, _max_depth, self.max_depth, _avg_ent_leaf, self.bucket_size, _mispl_weight) @staticmethod def get_code(): diff --git a/tests/test_data_structure.py b/tests/test_data_structure.py index 60769cd..8854f4f 100644 --- a/tests/test_data_structure.py +++ b/tests/test_data_structure.py @@ -12,6 +12,7 @@ trie, quadtree) +from pygorithm.geometry import (vector2, rect2) class TestStack(unittest.TestCase): def test_stack(self): @@ -372,7 +373,7 @@ def setUp(self): self.rect1 = rect2.Rect2(1, 1, vector2.Vector2(2, 2)) def test_constructor(self): - ent = quadtree.QuadTreeEntity(rect1) + ent = quadtree.QuadTreeEntity(self.rect1) self.assertIsNotNone(ent.aabb) self.assertEqual(1, ent.aabb.width) @@ -381,13 +382,13 @@ def test_constructor(self): self.assertEqual(2, ent.aabb.mincorner.y) def test_repr(self): - ent = quadtree.QuadTreeEntity(rect1) + ent = quadtree.QuadTreeEntity(self.rect1) exp = "quadtreeentity(aabb=rect2(width=1, height=1, mincorner=vector2(x=2, y=2)))" self.assertEqual(exp, repr(ent)) def test_str(self): - ent = quadtree.QuadTreeEntity(rect1) + ent = quadtree.QuadTreeEntity(self.rect1) exp = "entity(at rect(1x1 at <2, 2>))" self.assertEqual(exp, str(ent)) @@ -419,71 +420,71 @@ def test_constructor(self): def test_get_quadrant(self): _tree = quadtree.QuadTree(64, 5, self.big_rect) - ent1 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(320, 175)) + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(320, 175))) quad1 = _tree.get_quadrant(ent1) self.assertEqual(0, quad1) - ent2 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(600, 450)) + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(600, 450))) quad2 = _tree.get_quadrant(ent2) self.assertEqual(1, quad2) - ent3 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(700, 950)) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(700, 950))) quad3 = _tree.get_quadrant(ent3) self.assertEqual(2, quad3) - ent4 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(0, 495)) + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(0, 505))) quad4 = _tree.get_quadrant(ent4) self.assertEqual(3, quad4) def test_get_quadrant_none(self): _tree = quadtree.QuadTree(64, 5, self.big_rect) - ent1 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(497, 150)) + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(497, 150))) self.assertEqual(-1, _tree.get_quadrant(ent1)) - ent2 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(800, 499)) + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 499))) self.assertEqual(-1, _tree.get_quadrant(ent2)) - ent3 = quadtree.QuadTreeEntity(15, 15, vector2.Vector2(481, 505)) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(15, 15, vector2.Vector2(486, 505))) self.assertEqual(-1, _tree.get_quadrant(ent3)) - ent4 = quadtree.QuadTreeEntity(5, 20, vector2.Vector2(15, 490)) + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 20, vector2.Vector2(15, 490))) self.assertEqual(-1, _tree.get_quadrant(ent4)) - ent5 = quadtree.QuadTreeEntity(17, 34, vector2.Vector2(485, 470)) + ent5 = quadtree.QuadTreeEntity(rect2.Rect2(17, 34, vector2.Vector2(485, 470))) self.assertEqual(-1, _tree.get_quadrant(ent5)) def test_get_quadrant_shifted(self): _tree = quadtree.QuadTree(64, 5, self.big_rect_sub_3) - ent1 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(515, 600)) + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(515, 600))) self.assertEqual(0, _tree.get_quadrant(ent1)) - ent2 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(800, 550)) + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 550))) self.assertEqual(1, _tree.get_quadrant(ent2)) - ent3 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(950, 650)) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(950, 850))) self.assertEqual(2, _tree.get_quadrant(ent3)) - ent4 = quadtree.QuadTreeEntity(5, 5, vector2.Vector2(15, 551)) + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(515, 751))) self.assertEqual(3, _tree.get_quadrant(ent4)) def test_get_quadrant_0_shifted(self): _tree = quadtree.QuadTree(64, 5, rect2.Rect2(500, 800, vector2.Vector2(200, 200))) - ent1 = quadtree.QuadTreeEntity(5, 10, vector2.Vector2(445, 224)) + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 10, vector2.Vector2(445, 224))) self.assertEqual(-1, _tree.get_quadrant(ent1)) - ent2 = quadtree.QuadTreeEntity(11, 17, vector2.Vector2(515, 585)) + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(11, 17, vector2.Vector2(515, 585))) self.assertEqual(-1, _tree.get_quadrant(ent2)) - ent3 = quadtree.QuadTreeEntity(20, 20, vector2.Vector2(440, 700)) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(20, 20, vector2.Vector2(440, 700))) self.assertEqual(-1, _tree.get_quadrant(ent3)) - ent4 = quadtree.QuadTreeEntity(15, 15, vector2.Vector2(215, 590)) + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(15, 15, vector2.Vector2(215, 590))) self.assertEqual(-1, _tree.get_quadrant(ent4)) - ent5 = quadtree.QuadTreeEntity(7, 12, vector2.Vector2(449, 589)) + ent5 = quadtree.QuadTreeEntity(rect2.Rect2(7, 12, vector2.Vector2(449, 589))) self.assertEqual(-1, _tree.get_quadrant(ent5)) def test_split_empty(self): @@ -493,53 +494,53 @@ def test_split_empty(self): self.assertIsNotNone(_tree1.children) self.assertEqual(4, len(_tree1.children)) - self.assertEqual(500, _tree1.children[0].width) - self.assertEqual(500, _tree1.children[0].height) - self.assertEqual(0, _tree1.children[0].mincorner.x) - self.assertEqual(0, _tree1.children[0].mincorner.y) + self.assertEqual(500, _tree1.children[0].location.width) + self.assertEqual(500, _tree1.children[0].location.height) + self.assertEqual(0, _tree1.children[0].location.mincorner.x) + self.assertEqual(0, _tree1.children[0].location.mincorner.y) self.assertEqual(1, _tree1.children[0].depth) self.assertEqual(64, _tree1.children[0].bucket_size) self.assertEqual(5, _tree1.children[0].max_depth) - self.assertEqual(500, _tree1.children[1].width) - self.assertEqual(500, _tree1.children[1].height) - self.assertEqual(500, _tree1.children[1].mincorner.x) - self.assertEqual(0, _tree1.children[1].mincorner.y) + self.assertEqual(500, _tree1.children[1].location.width) + self.assertEqual(500, _tree1.children[1].location.height) + self.assertEqual(500, _tree1.children[1].location.mincorner.x) + self.assertEqual(0, _tree1.children[1].location.mincorner.y) - self.assertEqual(500, _tree1.children[2].width) - self.assertEqual(500, _tree1.children[2].height) - self.assertEqual(500, _tree1.children[2].mincorner.x) - self.assertEqual(500, _tree1.children[2].mincorner.y) + self.assertEqual(500, _tree1.children[2].location.width) + self.assertEqual(500, _tree1.children[2].location.height) + self.assertEqual(500, _tree1.children[2].location.mincorner.x) + self.assertEqual(500, _tree1.children[2].location.mincorner.y) - self.assertEqual(500, _tree1.children[3].width) - self.assertEqual(500, _tree1.children[3].height) - self.assertEqual(0, _tree1.children[3].mincorner.x) - self.assertEqual(500, _tree1.children[3].mincorner.y) + self.assertEqual(500, _tree1.children[3].location.width) + self.assertEqual(500, _tree1.children[3].location.height) + self.assertEqual(0, _tree1.children[3].location.mincorner.x) + self.assertEqual(500, _tree1.children[3].location.mincorner.y) - - _tree2 = _tree1.children[3] + # bottom-right + _tree2 = _tree1.children[2] _tree2.split() - self.assertEqual(250, _tree2.children[0].width) - self.assertEqual(250, _tree2.children[0].height) - self.assertEqual(500, _tree2.children[0].mincorner.x) - self.assertEqual(500, _tree2.children[0].mincorner.y) + self.assertEqual(250, _tree2.children[0].location.width) + self.assertEqual(250, _tree2.children[0].location.height) + self.assertEqual(500, _tree2.children[0].location.mincorner.x) + self.assertEqual(500, _tree2.children[0].location.mincorner.y) self.assertEqual(2, _tree2.children[0].depth) - self.assertEqual(250, _tree2.children[1].width) - self.assertEqual(250, _tree2.children[1].height) - self.assertEqual(750, _tree2.children[1].mincorner.x) - self.assertEqual(500, _tree2.children[1].mincorner.y) + self.assertEqual(250, _tree2.children[1].location.width) + self.assertEqual(250, _tree2.children[1].location.height) + self.assertEqual(750, _tree2.children[1].location.mincorner.x) + self.assertEqual(500, _tree2.children[1].location.mincorner.y) - self.assertEqual(250, _tree2.children[2].width) - self.assertEqual(250, _tree2.children[2].height) - self.assertEqual(750, _tree2.children[2].mincorner.x) - self.assertEqual(750, _tree2.children[2].mincorner.y) + self.assertEqual(250, _tree2.children[2].location.width) + self.assertEqual(250, _tree2.children[2].location.height) + self.assertEqual(750, _tree2.children[2].location.mincorner.x) + self.assertEqual(750, _tree2.children[2].location.mincorner.y) - self.assertEqual(250, _tree2.children[3].width) - self.assertEqual(250, _tree2.children[3].height) - self.assertEqual(500, _tree2.children[3].mincorner.x) - self.assertEqual(750, _tree2.children[3].mincorner.y) + self.assertEqual(250, _tree2.children[3].location.width) + self.assertEqual(250, _tree2.children[3].location.height) + self.assertEqual(500, _tree2.children[3].location.mincorner.x) + self.assertEqual(750, _tree2.children[3].location.mincorner.y) def test_split_entities(self): @@ -553,24 +554,24 @@ def test_split_entities(self): _tree.split() self.assertEqual(1, len(_tree.children[0].entities)) - self.assertEqual(50, _tree.children[0].entities[0].mincorner.x) - self.assertEqual(50, _tree.children[0].entities[0].mincorner.y) + self.assertEqual(50, _tree.children[0].entities[0].aabb.mincorner.x) + self.assertEqual(50, _tree.children[0].entities[0].aabb.mincorner.y) self.assertEqual(1, len(_tree.children[1].entities)) - self.assertEqual(550, _tree.children[1].entities[0].mincorner.x) - self.assertEqual(75, _tree.children[1].entities[0].mincorner.y) + self.assertEqual(550, _tree.children[1].entities[0].aabb.mincorner.x) + self.assertEqual(75, _tree.children[1].entities[0].aabb.mincorner.y) self.assertEqual(1, len(_tree.children[2].entities)) - self.assertEqual(565, _tree.children[2].entities[0].mincorner.x) - self.assertEqual(585, _tree.children[2].entities[0].mincorner.y) + self.assertEqual(565, _tree.children[2].entities[0].aabb.mincorner.x) + self.assertEqual(585, _tree.children[2].entities[0].aabb.mincorner.y) self.assertEqual(1, len(_tree.children[3].entities)) - self.assertEqual(95, _tree.children[3].entities[0].mincorner.x) - self.assertEqual(900, _tree.children[3].entities[0].mincorner.y) + self.assertEqual(95, _tree.children[3].entities[0].aabb.mincorner.x) + self.assertEqual(900, _tree.children[3].entities[0].aabb.mincorner.y) self.assertEqual(1, len(_tree.entities)) - self.assertEqual(495, _tree.entities[0].mincorner.x) - self.assertEqual(167, _tree.entities[0].mincorner.y) + self.assertEqual(495, _tree.entities[0].aabb.mincorner.x) + self.assertEqual(167, _tree.entities[0].aabb.mincorner.y) _tree2 = _tree.children[3] _tree2.split() @@ -579,8 +580,8 @@ def test_split_entities(self): self.assertEqual(0, len(_tree2.children[i].entities), msg="i={}".format(i)) self.assertEqual(1, len(_tree2.children[3].entities)) - self.assertEqual(95, _tree2.children[3].entities[0].mincorner.x) - self.assertEqual(900, _tree2.children[3].entities[0].mincorner.y) + self.assertEqual(95, _tree2.children[3].entities[0].aabb.mincorner.x) + self.assertEqual(900, _tree2.children[3].entities[0].aabb.mincorner.y) # note for test_think and test_insert we're testing the worst-case scenario # for a quad tree (everythings all bunched up in a corner) hence the instant @@ -593,31 +594,35 @@ def test_split_entities(self): # (even at 1000x1000 world!) past depth 5 or so. def test_think(self): - ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(15, 15))) - ent2 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(20, 20))) - ent3 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(0, 0))) - ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(5, 0))) - ent5 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(0, 5))) + ent1 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(15, 15))) + ent2 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(20, 20))) + ent3 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(0, 0))) + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(5, 0))) + ent5 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(0, 5))) _tree = quadtree.QuadTree(2, 2, self.big_rect, entities = [ ent1, ent2, ent3, ent4, ent5 ]) + _tree.think(True) - self.assertIsNotNone(_tree.children) # depth 0 - self.assertIsNotNone(_tree.children[0].children) # depth 1 - self.assertIsNotNone(_tree.children[0].children[0].children) # depth 2 - self.assertIsNone(_tree.children[0].children[0].children[0].children) # depth 3 shouldn't happen because - self.assertEqual(5, len(_tree.children[0].children[0].children[0].entities)) # max_depth reached + self.assertIsNotNone(_tree.children) # depth 1 + self.assertIsNotNone(_tree.children[0].children) # depth 2 + self.assertIsNone(_tree.children[0].children[0].children) # depth 3 shouldn't happen because + self.assertEqual(5, len(_tree.children[0].children[0].entities)) # max_depth reached + + + _tree2 = quadtree.QuadTree(2, 2, self.big_rect, entities = [ ent1, ent2 ]) + _tree2.think(True) + self.assertIsNone(_tree2.children) def test_insert(self): _tree = quadtree.QuadTree(2, 2, self.big_rect) - _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(15, 15)))) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(15, 15)))) self.assertIsNone(_tree.children) - _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(20, 20)))) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(20, 20)))) self.assertIsNone(_tree.children) - _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, rect2.Rect2(0, 0)))) - self.assertIsNotNone(_tree.children) # depth 0 - self.assertIsNotNone(_tree.children[0].children) # depth 1 - self.assertIsNotNone(_tree.children[0].children[0].children) # depth 2 - self.assertIsNone(_tree.children[0].children[0].children[0].children) # depth 3 shouldn't happen because + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(0, 0)))) + self.assertIsNotNone(_tree.children) # depth 1 + self.assertIsNotNone(_tree.children[0].children) # depth 2 + self.assertIsNone(_tree.children[0].children[0].children) # depth 3 shouldn't happen because self.assertEqual(3, len(_tree.children[0].children[0].entities)) # max_depth reached def test_retrieve(self): @@ -629,8 +634,8 @@ def test_retrieve(self): retr = _tree.retrieve_collidables(ent1) self.assertIsNotNone(retr) self.assertEqual(1, len(retr)) - self.assertEqual(25, retr.mincorner.x) - self.assertEqual(25, retr.mincorner.y) + self.assertEqual(25, retr[0].aabb.mincorner.x) + self.assertEqual(25, retr[0].aabb.mincorner.y) # note this is not nicely in a quadrant ent2 = quadtree.QuadTreeEntity(rect2.Rect2(20, 10, vector2.Vector2(490, 300))) @@ -644,25 +649,34 @@ def test_retrieve(self): ent3 = quadtree.QuadTreeEntity(rect2.Rect2(15, 10, vector2.Vector2(700, 450))) _tree.insert_and_think(ent3) - # ent1 should collide with ent1 or ent2, - # ent2 with ent1, ent2, or ent3 + ent4 = quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(900, 900))) + _tree.insert_and_think(ent4) + + # ent1 should collide with ent1 or ent2 + # ent2 with ent1 or ent2, or ent3 # ent3 with ent2 or ent3 + # ent4 with ent2 or ent4 retr = _tree.retrieve_collidables(ent1) self.assertIsNotNone(retr) self.assertEqual(2, len(retr)) - self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 25), None), str(retr)) - self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 490), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 25), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 490), None), str(retr)) retr = _tree.retrieve_collidables(ent2) self.assertEqual(3, len(retr)) - self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 25), None), str(retr)) - self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 490), None), str(retr)) - self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 700), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 25), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 490), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 700), None), str(retr)) retr = _tree.retrieve_collidables(ent3) self.assertEqual(2, len(retr)) - self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 490), None), str(retr)) - self.assertIsNotNone(next((e for e in retr if e.mincorner.x == 700), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 490), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 700), None), str(retr)) + + retr = _tree.retrieve_collidables(ent4) + self.assertEqual(2, len(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 900), None), str(retr)) + self.assertIsNotNone(next((e for e in retr if e.aabb.mincorner.x == 490), None), str(retr)) def test_ents_per_depth(self): _tree = quadtree.QuadTree(3, 5, self.big_rect) @@ -671,11 +685,24 @@ def test_ents_per_depth(self): _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(300, 499)))) self.assertDictEqual({ 0: 2 }, _tree.find_entities_per_depth()) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 600)))) - self.assertDictEqual({ 0: 1, 1: 2 }, _tree.find_entities_per_depth()) + self.assertDictEqual({ 0: 3 }, _tree.find_entities_per_depth()) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 300)))) self.assertDictEqual({ 0: 1, 1: 3 }, _tree.find_entities_per_depth()) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(150, 100)))) - self.assertDictEqual({ 0: 1, 1: 1, 2: 3 }, _tree.find_entities_per_depth()) + self.assertDictEqual({ 0: 1, 1: 4 }, _tree.find_entities_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(80, 40)))) + self.assertDictEqual({ 0: 1, 1: 1, 2: 4 }, _tree.find_entities_per_depth()) + + def test_nodes_per_depth(self): + _tree = quadtree.QuadTree(1, 5, self.big_rect) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(50, 50)))) + self.assertDictEqual({ 0: 1 }, _tree.find_nodes_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 450)))) + self.assertDictEqual({ 0: 1, 1: 4, 2: 4 }, _tree.find_nodes_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(550, 550)))) + self.assertDictEqual({ 0: 1, 1: 4, 2: 4 }, _tree.find_nodes_per_depth()) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(850, 550)))) + self.assertDictEqual({ 0: 1, 1: 4, 2: 8 }, _tree.find_nodes_per_depth()) def test_sum_ents(self): # it shouldn't matter where we put entities in, adding entities @@ -687,7 +714,7 @@ def test_sum_ents(self): h = random.randrange(1, 10) x = random.uniform(0, 1000 - w) y = random.uniform(0, 1000 - h) - ent = quadtree.QuadTreeEntity(w, h, vector2.Vector2(x, y)) + ent = quadtree.QuadTreeEntity(rect2.Rect2(w, h, vector2.Vector2(x, y))) _tree.insert_and_think(ent) # avoid calculating sum every loop which would take way too long. @@ -706,48 +733,58 @@ def test_avg_ents_per_leaf(self): _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75, 35)))) self.assertEqual(1, _tree.calculate_avg_ents_per_leaf()) # 1 ent on 1 leaf _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(300, 499)))) - self.asserttEqual(2, _tree.calculate_avg_ents_per_leaf()) # 2 ents 1 leaf + self.assertEqual(2, _tree.calculate_avg_ents_per_leaf()) # 2 ents 1 leaf _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 600)))) - self.asserttEqual(0.5, _tree.calculate_avg_ents_per_leaf()) # 2 ents 4 leafs (1 misplaced) + self.assertEqual(3, _tree.calculate_avg_ents_per_leaf()) # 3 ents 1 leaf _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 300)))) - self.asserttEqual(0.75, _tree.calculate_avg_ents_per_leaf()) # 3 ents 4 leafs (1 misplaced) + self.assertEqual(0.75, _tree.calculate_avg_ents_per_leaf()) # 3 ents 4 leafs (1 misplaced) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(150, 100)))) - self.asserttEqual(0.5, _tree.calculate_avg_ents_per_leaf()) # 4 ents 8 leafs (1 misplaced) + self.assertEqual(1, _tree.calculate_avg_ents_per_leaf()) # 4 ents 4 leafs (1 misplaced) + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 450)))) + self.assertAlmostEqual(5/7, _tree.calculate_avg_ents_per_leaf()) # 5 ents 7 leafs (1 misplaced) def test_misplaced_ents(self): _tree = quadtree.QuadTree(3, 5, self.big_rect) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(75, 35)))) self.assertEqual(0, _tree.calculate_weight_misplaced_ents()) # 0 misplaced, 1 total _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(300, 499)))) - self.asserttEqual(0, _tree.calculate_weight_misplaced_ents()) # 0 misplaced, 2 total + self.assertEqual(0, _tree.calculate_weight_misplaced_ents()) # 0 misplaced, 2 total _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(800, 600)))) - self.assertAlmostEqual(4/3, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (1 deep), 3 total - _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(450, 300)))) + self.assertEqual(0, _tree.calculate_weight_misplaced_ents()) # 0 misplaced 3 total + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(550, 700)))) self.assertAlmostEqual(1, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (1 deep), 4 total - _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(150, 100)))) - self.assertAlmostEqual(8/5, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (2 deep), 5 total + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(900, 900)))) + self.assertAlmostEqual(4/5, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (1 deep), 5 total + _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(5, 5, vector2.Vector2(950, 950)))) + self.assertAlmostEqual(8/6, _tree.calculate_weight_misplaced_ents()) # 1 misplaced (2 deep), 6 total def test_repr(self): - _tree = quadtree.QuadTree(2, 5, rect2.Rect2(100, 100)) + _tree = quadtree.QuadTree(1, 5, rect2.Rect2(100, 100)) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) _olddiff = self.maxDiff - def cleanup(self2): + def cleanup(self2=self): self2.maxDiff = _olddiff self.addCleanup(cleanup) self.maxDiff = None - self.assertEqual("quadtree(bucket_size=2, max_depth=5, location=rect2(width=100, height=100, mincorner=vector2(x=0, y=0)), depth=0, entities=[], children=[ quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=5, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=0)), depth=1, entities=[ quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=95, y=5))) ], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=50, y=50)), depth=1, entities=[], children=[]), quadtree(bucket_size=2, max_depth=5, location=rect2(width=50, height=50, mincorner=vector2(x=0, y=50)), depth=1, entities=[], children=[]) ])", repr(_tree)) + self.assertEqual("quadtree(bucket_size=1, max_depth=5, location=rect2(width=100, height=100, mincorner=vector2(x=0, y=0)), depth=0, entities=[], children=[quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=0, y=0)), depth=1, entities=[quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=5, y=5)))], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=50.0, y=0)), depth=1, entities=[quadtreeentity(aabb=rect2(width=2, height=2, mincorner=vector2(x=95, y=5)))], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=50.0, y=50.0)), depth=1, entities=[], children=None), quadtree(bucket_size=1, max_depth=5, location=rect2(width=50.0, height=50.0, mincorner=vector2(x=0, y=50.0)), depth=1, entities=[], children=None)])", repr(_tree)) def test_str(self): - _tree = quadtree.QuadTree(2, 5, rect2.Rect2(100, 100)) + _tree = quadtree.QuadTree(1, 5, rect2.Rect2(100, 100)) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) - self.assertEqual("quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (max depth: 5), avg ent/leaf: 0.5 (target 2), misplaced weight = 0 (0 best, >1 bad))", str(_tree)) + _olddiff = self.maxDiff + def cleanup(self2=self): + self2.maxDiff = _olddiff + + self.addCleanup(cleanup) + self.maxDiff = None + self.assertEqual("quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (allowed max depth: 1, actual: 5), avg ent/leaf: 0.5 (target 1), misplaced weight 0.0 (0 best, >1 bad)", str(_tree)) if __name__ == '__main__': unittest.main() From 43d24e5ac5a165c0cb2b19917360b7c571444750 Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Sun, 3 Sep 2017 10:01:38 -0700 Subject: [PATCH 6/8] Fix format order issue in quadtree __str__ The text was incorrect (allowed and actual depth were swapped) * pygorithm/data_structures/quadtree.py - fix __str__ * tests/test_data_structure.py - fix quadtree __str__ tests --- pygorithm/data_structures/quadtree.py | 2 +- tests/test_data_structure.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygorithm/data_structures/quadtree.py b/pygorithm/data_structures/quadtree.py index f0e0fe3..67fd8ca 100644 --- a/pygorithm/data_structures/quadtree.py +++ b/pygorithm/data_structures/quadtree.py @@ -547,7 +547,7 @@ def __str__(self): _max_depth = max(_ents_per_depth.keys()) _avg_ent_leaf = self.calculate_avg_ents_per_leaf() _mispl_weight = self.calculate_weight_misplaced_ents(sum_entities=_sum) - return "quadtree(at {} with {} entities here ({} in total); (nodes, entities) per depth: {} (allowed max depth: {}, actual: {}), avg ent/leaf: {} (target {}), misplaced weight {} (0 best, >1 bad)".format(self.location, len(self.entities), _sum, _nodes_ents_per_depth_str, _max_depth, self.max_depth, _avg_ent_leaf, self.bucket_size, _mispl_weight) + return "quadtree(at {} with {} entities here ({} in total); (nodes, entities) per depth: {} (allowed max depth: {}, actual: {}), avg ent/leaf: {} (target {}), misplaced weight {} (0 best, >1 bad)".format(self.location, len(self.entities), _sum, _nodes_ents_per_depth_str, self.max_depth, _max_depth, _avg_ent_leaf, self.bucket_size, _mispl_weight) @staticmethod def get_code(): diff --git a/tests/test_data_structure.py b/tests/test_data_structure.py index 8854f4f..9e26609 100644 --- a/tests/test_data_structure.py +++ b/tests/test_data_structure.py @@ -784,7 +784,7 @@ def cleanup(self2=self): self.addCleanup(cleanup) self.maxDiff = None - self.assertEqual("quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (allowed max depth: 1, actual: 5), avg ent/leaf: 0.5 (target 1), misplaced weight 0.0 (0 best, >1 bad)", str(_tree)) + self.assertEqual("quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (allowed max depth: 5, actual: 1), avg ent/leaf: 0.5 (target 1), misplaced weight 0.0 (0 best, >1 bad)", str(_tree)) if __name__ == '__main__': unittest.main() From b660302058b5899f23ff1500b9f3e343a05eb0c1 Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Sun, 3 Sep 2017 10:04:54 -0700 Subject: [PATCH 7/8] Quadtree documentation improvement Documentation now matches actual result * pygorithm/data_structures/quadtree.py - __str__ documentation change --- pygorithm/data_structures/quadtree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygorithm/data_structures/quadtree.py b/pygorithm/data_structures/quadtree.py index 67fd8ca..a2d3936 100644 --- a/pygorithm/data_structures/quadtree.py +++ b/pygorithm/data_structures/quadtree.py @@ -531,7 +531,7 @@ def __str__(self): _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(5, 5)))) _tree.insert_and_think(quadtree.QuadTreeEntity(rect2.Rect2(2, 2, vector2.Vector2(95, 5)))) - # prints quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (allowed max depth: 1, actual: 5), avg ent/leaf: 0.5 (target 1), misplaced weight 0.0 (0 best, >1 bad) + # prints quadtree(at rect(100x100 at <0, 0>) with 0 entities here (2 in total); (nodes, entities) per depth: [ 0: (1, 0), 1: (4, 2) ] (allowed max depth: 5, actual: 1), avg ent/leaf: 0.5 (target 1), misplaced weight 0.0 (0 best, >1 bad) print(_tree) :returns: human-readable representation of this quad tree From ddc9f57018c2a9f634ff328a238421971ec078d5 Mon Sep 17 00:00:00 2001 From: Timothy Moore Date: Sun, 3 Sep 2017 10:08:39 -0700 Subject: [PATCH 8/8] Minor quadtree documentation Minor update to __repr__ documentation for quadtree * pygorithm/data_structures/quadtree.py - change __repr__ documentation --- pygorithm/data_structures/quadtree.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pygorithm/data_structures/quadtree.py b/pygorithm/data_structures/quadtree.py index a2d3936..e04c73b 100644 --- a/pygorithm/data_structures/quadtree.py +++ b/pygorithm/data_structures/quadtree.py @@ -482,7 +482,9 @@ def calculate_weight_misplaced_ents(self, sum_entities=None): def __repr__(self): """ - Create an unambiguous, recursive representation of this quad tree. + Create an unambiguous representation of this quad tree. + + This is implemented iteratively. Example: