Priority R-tree in Python
R-tree variant with worst-case query guarantees built by placing extreme-coordinate objects in priority nodes.
- File
priority_r_tree.py, 126 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/priority-r-tree/. The authors are listed in prov/8PZPPQ.jsonl.
"""The priority R-tree (PR-tree) of Arge, de Berg, Haverkort and Yi, in the plane.
A rectangle (xmin, ymin, xmax, ymax) is seen as a point of four
dimensions. A pseudo-PR-tree on a set of rectangles takes out four
priority leaves of B rectangles each: the B with the smallest xmin, then
of the rest the B with the smallest ymin, the B with the largest xmax and
the B with the largest ymax. The rest is split at the median of one of
the four coordinates, in turn, as in a four-dimensional k-d tree. The
PR-tree is built bottom up: its leaves are the leaves of a pseudo-PR-tree
on the data; the nodes of each next level are the leaves of a
pseudo-PR-tree on the bounding boxes of the level below; this goes on
until one node is left. The result is a static R-tree whose nodes hold at
most B entries and whose leaves are all at one depth.
"""
from __future__ import annotations
from typing import Hashable, List, Sequence, Tuple
Rect = Tuple[float, float, float, float]
# sort keys for the four priority leaves: smallest xmin, smallest ymin,
# largest xmax, largest ymax
KEYS = (lambda r: r[0], lambda r: r[1], lambda r: -r[2], lambda r: -r[3])
def intersects(a: Rect, b: Rect) -> bool:
return a[0] <= b[2] and b[0] <= a[2] and a[1] <= b[3] and b[1] <= a[3]
def mbr(rects: Sequence[Rect]) -> Rect:
return (min(r[0] for r in rects), min(r[1] for r in rects),
max(r[2] for r in rects), max(r[3] for r in rects))
def pseudo_pr_leaves(items: List[Tuple[Rect, object]], B: int, depth: int = 0) -> List[List[Tuple[Rect, object]]]:
"""The leaves of a pseudo-PR-tree on items (rect, payload), each a list of
at most B items, in the order of the tree."""
if len(items) <= B: # @line L1
return [items] if items else []
rest = items
out = []
for key in KEYS: # @line L2
if not rest:
break
rest = sorted(rest, key=lambda it: (key(it[0]), it[0]))
out.append(rest[:B]) # @line L3
rest = rest[B:]
if rest:
axis = depth % 4 # @line L4
rest = sorted(rest, key=lambda it: (it[0][axis], it[0]))
h = len(rest) // 2
out += pseudo_pr_leaves(rest[:h], B, depth + 1) # @line L5
out += pseudo_pr_leaves(rest[h:], B, depth + 1)
return out
class Node:
__slots__ = ("leaf", "entries")
def __init__(self, leaf: bool, entries: list) -> None:
self.leaf = leaf
self.entries = entries # list of (rect, id) or (rect, Node)
def rect(self) -> Rect:
return mbr([r for r, _ in self.entries])
class PRTree:
def __init__(self, data: Sequence[Tuple[Hashable, Rect]], B: int = 4) -> None:
if B < 2:
raise ValueError("B must be at least 2")
for _, r in data:
if r[0] > r[2] or r[1] > r[3]:
raise ValueError("empty rectangle")
self.B = B
self.n = len(data)
level = [Node(True, grp) for grp in
pseudo_pr_leaves([(tuple(r), oid) for oid, r in data], B)] # @line L6
self.levels = 1
while len(level) > 1: # @line L7
items = [(u.rect(), u) for u in level]
level = [Node(False, grp) for grp in pseudo_pr_leaves(items, B)] # @line L8
self.levels += 1
self.root = level[0] if level else None
def search(self, q: Rect) -> List[Hashable]:
out: List[Hashable] = []
if self.root is None:
return out
stack = [self.root]
self.visited = 0
while stack:
u = stack.pop()
self.visited += 1
for r, x in u.entries: # @line L9
if intersects(r, q): # @line L10
if u.leaf:
out.append(x)
else:
stack.append(x) # @line L11
return out
def height(self) -> int:
return self.levels - 1 if self.root is not None else -1
def check_invariant(self) -> None:
if self.root is None:
assert self.n == 0
return
seen = []
depths = set()
stack = [(self.root, 0)]
while stack:
u, d = stack.pop()
assert 1 <= len(u.entries) <= self.B
if u.leaf:
depths.add(d)
seen += [x for _, x in u.entries]
else:
for r, ch in u.entries:
assert r == ch.rect()
stack.append((ch, d + 1))
assert depths == {self.levels - 1}
assert len(seen) == self.n
test_priority_r_tree.py 68 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/priority-r-tree/. The authors are listed in prov/8PZPPQ.jsonl.
import random
import pytest
from priority_r_tree import PRTree, intersects, mbr, pseudo_pr_leaves
PTS = {"A": (1, 1), "B": (2, 9), "C": (3, 4), "D": (4, 7), "E": (5, 2), "F": (6, 6),
"G": (7, 10), "H": (8, 3), "I": (9, 8), "J": (10, 1), "K": (11, 5), "L": (12, 9),
"M": (13, 3), "N": (14, 7), "O": (15, 1), "P": (16, 6)}
DATA = [(k, (x, y, x + 1, y + 1)) for k, (x, y) in PTS.items()]
def test_page_example():
groups = pseudo_pr_leaves([(r, k) for k, r in DATA], 3)
names = ["".join(k for _, k in g) for g in groups]
assert names == ["ABC", "JOE", "PNM", "GLI", "DF", "HK"]
t = PRTree(DATA, 3)
t.check_invariant()
assert t.height() == 2
kids = [sorted("".join(k for _, k in leaf.entries) for _, leaf in u.entries)
for _, u in t.root.entries]
assert kids == [["ABC", "DF", "JOE"], ["GLI", "HK", "PNM"]]
assert sorted(t.search((5, 5, 10, 9))) == ["D", "F", "I"]
assert t.visited == 6
def test_small_and_errors():
t = PRTree([], 3)
assert t.search((0, 0, 1, 1)) == [] and t.height() == -1
t.check_invariant()
t = PRTree([("a", (0, 0, 1, 1))], 3)
assert t.search((1, 1, 2, 2)) == ["a"] and t.height() == 0
with pytest.raises(ValueError):
PRTree([], 1)
with pytest.raises(ValueError):
PRTree([("a", (1, 0, 0, 1))], 3)
def test_priority_leaves_hold_extremes():
rng = random.Random(3)
items = []
for k in range(40):
x, y = rng.randint(0, 50), rng.randint(0, 50)
items.append(((x, y, x + rng.randint(0, 9), y + rng.randint(0, 9)), k))
groups = pseudo_pr_leaves(items, 4)
first = groups[0]
assert max(r[0] for r, _ in first) <= min(r[0] for r, _ in items if (r, _) not in first)
assert sorted(k for g in groups for _, k in g) == list(range(40))
def test_random_against_brute_force():
rng = random.Random(11)
for trial in range(600):
B = rng.randint(2, 6)
data = []
for k in range(rng.randint(0, 60)):
x, y = rng.randint(0, 40), rng.randint(0, 40)
s = rng.choice([0, 2, 10])
data.append((k, (x, y, x + rng.randint(0, s), y + rng.randint(0, s))))
t = PRTree(data, B)
t.check_invariant()
for _ in range(4):
x, y = rng.randint(-5, 45), rng.randint(-5, 45)
q = (x, y, x + rng.randint(0, 15), y + rng.randint(0, 15))
want = sorted(k for k, r in data if intersects(q, r))
assert sorted(t.search(q)) == want
What the next level needs
- the tests did not run in this build