Algorithm Search⌘K

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
priority_r_tree.pyRawGitHub
# 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

Read the full page about Priority R-tree