Algorithm Search⌘K

MX-CIF quadtree in Python

Quadtree that stores each rectangle at the smallest node that fully contains it, for rectangle data.

File
mx_cif_quadtree.py, 187 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
mx_cif_quadtree.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/mx-cif-quadtree/. The authors are listed in prov/RHN2XW.jsonl.

from typing import Optional

Rect = tuple[int, int, int, int]  # half-open [x1, x2) x [y1, y2), written (x1, y1, x2, y2)


class Node:
    """A square cell with corner (x, y) and side size.

    rects holds the rectangles whose smallest enclosing cell is this one.
    children[o] is the child for quadrant o = bx + 2*by (0 SW, 1 SE, 2 NW,
    3 NE), or None if no rectangle is stored below it.
    """

    __slots__ = ("x", "y", "size", "rects", "children")

    def __init__(self, x: int, y: int, size: int) -> None:
        self.x, self.y, self.size = x, y, size
        self.rects: list[Rect] = []
        self.children: list[Optional[Node]] = [None, None, None, None]

    def quadrant_of(self, r: Rect) -> int:
        """The quadrant that holds all of r, or -1 if r crosses a center line or the cell has side 1."""
        if self.size == 1:
            return -1
        cx, cy = self.x + self.size // 2, self.y + self.size // 2
        if r[2] <= cx:
            bx = 0
        elif r[0] >= cx:
            bx = 1
        else:
            return -1
        if r[3] <= cy:
            by = 0
        elif r[1] >= cy:
            by = 1
        else:
            return -1
        return bx + 2 * by

    def child_cell(self, o: int) -> "Node":
        h = self.size // 2
        return Node(self.x + (o & 1) * h, self.y + (o >> 1) * h, h)

    def meets(self, q: Rect) -> bool:
        return self.x < q[2] and q[0] < self.x + self.size and self.y < q[3] and q[1] < self.y + self.size


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]


class MxCifQuadtree:
    """An MX-CIF quadtree of rectangles with integer corners in [0, 2^depth)^2.

    Invariant: each rectangle is stored once, at the smallest cell of the
    quadtree that holds it, which is the first cell on the path from the root
    where it crosses a center line (or a cell of side 1). A node exists only if
    it or a node below it stores a rectangle, so the shape depends only on the
    set of rectangles.
    """

    def __init__(self, depth: int) -> None:
        if depth < 0:
            raise ValueError("need depth >= 0")
        self.depth = depth
        self.root = Node(0, 0, 1 << depth)
        self.size = 0

    def __len__(self) -> int:
        return self.size

    def _norm(self, r: Rect) -> Rect:
        x1, y1, x2, y2 = (int(v) for v in r)
        side = 1 << self.depth
        if not (0 <= x1 < x2 <= side and 0 <= y1 < y2 <= side):
            raise ValueError("need 0 <= x1 < x2 <= 2^depth and 0 <= y1 < y2 <= 2^depth")
        return x1, y1, x2, y2

    def insert(self, r: Rect) -> bool:
        """Store r. Returns False if it was already stored. O(depth + the list at its node)."""
        r = self._norm(r)
        u = self.root  # @line L1
        o = u.quadrant_of(r)
        while o >= 0:  # @line L2
            if u.children[o] is None:  # @line L3
                u.children[o] = u.child_cell(o)
            u = u.children[o]  # type: ignore[assignment]
            o = u.quadrant_of(r)  # @line L4
        if r in u.rects:
            return False
        u.rects.append(r)  # @line L5
        self.size += 1
        return True

    def window(self, q: Rect) -> list[Rect]:
        """All stored rectangles that meet the half-open window q."""
        q = self._norm(q)
        out: list[Rect] = []
        stack = [self.root]
        while stack:  # @line L6
            u = stack.pop()
            if not u.meets(q):  # @line L7
                continue
            for r in u.rects:  # @line L8
                if intersects(r, q):
                    out.append(r)  # @line L9
            for c in u.children:  # @line L10
                if c is not None:
                    stack.append(c)
        return out

    def stab(self, p: tuple[int, int]) -> list[Rect]:
        """All stored rectangles that hold the point p. Visits one path of the tree."""
        px, py = int(p[0]), int(p[1])
        side = 1 << self.depth
        if not (0 <= px < side and 0 <= py < side):
            raise ValueError("the point is outside the root square")
        out: list[Rect] = []
        u: Optional[Node] = self.root
        while u is not None:
            out.extend(r for r in u.rects if r[0] <= px < r[2] and r[1] <= py < r[3])
            if u.size == 1:
                break
            h = u.size // 2
            u = u.children[(1 if px >= u.x + h else 0) + (2 if py >= u.y + h else 0)]
        return out

    def delete(self, r: Rect) -> bool:
        """Remove r. Returns False if it was not stored. Removes the nodes left empty."""
        r = self._norm(r)
        path: list[tuple[Node, int]] = []
        u = self.root  # @line L11
        o = u.quadrant_of(r)
        while o >= 0:
            c = u.children[o]
            if c is None:
                return False
            path.append((u, o))
            u, o = c, c.quadrant_of(r)
        if r not in u.rects:  # @line L12
            return False
        u.rects.remove(r)
        self.size -= 1
        while path and not u.rects and all(c is None for c in u.children):  # @line L13
            parent, o = path.pop()
            parent.children[o] = None  # @line L14
            u = parent
        return True

    def rects(self) -> list[Rect]:
        out: list[Rect] = []
        stack = [self.root]
        while stack:
            u = stack.pop()
            out.extend(u.rects)
            stack.extend(c for c in u.children if c is not None)
        return out

    def node_count(self) -> int:
        n, stack = 0, [self.root]
        while stack:
            u = stack.pop()
            n += 1
            stack.extend(c for c in u.children if c is not None)
        return n

    def check_invariant(self) -> None:
        seen: set[Rect] = set()
        stack = [self.root]
        while stack:
            u = stack.pop()
            for r in u.rects:
                assert r not in seen, "rectangle stored twice"
                seen.add(r)
                assert u.x <= r[0] and r[2] <= u.x + u.size and u.y <= r[1] and r[3] <= u.y + u.size, "outside its cell"
                assert u.quadrant_of(r) < 0, "rectangle fits in a child"
            for o, c in enumerate(u.children):
                if c is None:
                    continue
                e = u.child_cell(o)
                assert u.size > 1 and (c.x, c.y, c.size) == (e.x, e.y, e.size), "child is not a quadrant"
                assert c.rects or any(cc is not None for cc in c.children), "empty node"
                stack.append(c)
        assert len(seen) == self.size, "size is wrong"
test_mx_cif_quadtree.py 109 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/mx-cif-quadtree/. The authors are listed in prov/RHN2XW.jsonl.

import random

import pytest

from mx_cif_quadtree import MxCifQuadtree, Node, intersects


def shape(u: Node) -> object:
    kids = {o: shape(c) for o, c in enumerate(u.children) if c is not None}
    return (sorted(u.rects), kids)


def smallest_cell(depth: int, r: tuple[int, int, int, int]) -> tuple[int, int, int]:
    """Oracle: the smallest aligned cell that holds r, by trying sizes from small to large."""
    for k in range(depth + 1):
        s = 1 << k
        x, y = (r[0] // s) * s, (r[1] // s) * s
        if r[2] <= x + s and r[3] <= y + s:
            return x, y, s
    raise AssertionError


def test_page_example() -> None:
    t = MxCifQuadtree(3)
    A, B, C, D, E = (1, 2, 7, 5), (5, 5, 7, 7), (0, 0, 1, 1), (1, 6, 3, 8), (4, 0, 8, 4)
    for r in [A, B, C, D, E]:
        assert t.insert(r)
        t.check_invariant()
    assert not t.insert(A)
    assert shape(t.root) == (
        [A],
        {
            0: ([], {0: ([], {0: ([C], {})})}),
            1: ([E], {}),
            2: ([D], {}),
            3: ([B], {}),
        },
    )
    assert t.node_count() == 7
    assert sorted(t.window((5, 1, 6, 6))) == sorted([A, B, E])
    assert t.stab((2, 7)) == [D]
    assert sorted(t.stab((5, 3))) == sorted([A, E])
    assert t.delete(C) and not t.delete(C)
    t.check_invariant()
    assert t.node_count() == 4


def test_edges() -> None:
    t = MxCifQuadtree(0)
    assert t.insert((0, 0, 1, 1)) and t.node_count() == 1
    assert t.stab((0, 0)) == [(0, 0, 1, 1)]
    assert t.delete((0, 0, 1, 1)) and len(t) == 0
    with pytest.raises(ValueError):
        t.insert((0, 0, 0, 1))
    with pytest.raises(ValueError):
        t.insert((0, 0, 2, 1))
    with pytest.raises(ValueError):
        MxCifQuadtree(-1)
    u = MxCifQuadtree(10)
    assert u.insert((0, 0, 1, 1)) and u.node_count() == 11
    assert u.insert((511, 0, 513, 1)) and u.root.rects == [(511, 0, 513, 1)]
    assert not u.delete((5, 5, 6, 6))
    assert u.delete((0, 0, 1, 1)) and u.node_count() == 1


def test_random_against_oracle() -> None:
    rng = random.Random(20261104)
    for case in range(600):
        depth = rng.randint(0, 5)
        side = 1 << depth
        t = MxCifQuadtree(depth)
        live: set[tuple[int, int, int, int]] = set()
        for _ in range(rng.randint(1, 40)):
            x1, x2 = sorted(rng.sample(range(side + 1), 2))
            y1, y2 = sorted(rng.sample(range(side + 1), 2))
            if rng.random() < 0.5:
                w = rng.randint(1, max(1, side // 4))
                h = rng.randint(1, max(1, side // 4))
                x1, y1 = rng.randrange(side - w + 1), rng.randrange(side - h + 1)
                x2, y2 = x1 + w, y1 + h
            r = (x1, y1, x2, y2)
            if live and rng.random() < 0.3:
                d = rng.choice(sorted(live))
                assert t.delete(d)
                live.discard(d)
            assert t.insert(r) == (r not in live)
            live.add(r)
        t.check_invariant()
        assert sorted(t.rects()) == sorted(live)
        stack = [t.root]
        while stack:
            u = stack.pop()
            for r in u.rects:
                assert smallest_cell(depth, r) == (u.x, u.y, u.size)
            stack.extend(c for c in u.children if c is not None)
        u2 = MxCifQuadtree(depth)
        for r in rng.sample(sorted(live), len(live)):
            u2.insert(r)
        assert shape(u2.root) == shape(t.root)
        x1, x2 = sorted(rng.sample(range(side + 1), 2))
        y1, y2 = sorted(rng.sample(range(side + 1), 2))
        q = (x1, y1, x2, y2)
        assert sorted(t.window(q)) == sorted(r for r in live if intersects(r, q))
        p = (rng.randrange(side), rng.randrange(side))
        want = [r for r in live if r[0] <= p[0] < r[2] and r[1] <= p[1] < r[3]]
        assert sorted(t.stab(p)) == sorted(want)

What the next level needs

  • the tests did not run in this build

Read the full page about MX-CIF quadtree