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
# 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