Cover tree in Python
Leveled metric tree with provable query bounds depending on the expansion constant of the data.
- File
cover_tree.py, 114 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/cover-tree/. The authors are listed in prov/GE45W4.jsonl.
"""Cover tree with base 2: insertion and nearest neighbor search (Beygelzimer, Kakade and Langford)."""
import math
from typing import Callable, Optional, Sequence
Point = Sequence[float]
def euclidean(a: Point, b: Point) -> float:
return math.dist(a, b)
class CoverTree:
"""Points are kept by index. children[p][i] lists the points whose top level is i - 1
and whose parent at level i is p. Every point is its own child at each level below its top."""
def __init__(self, dist: Callable[[Point, Point], float] = euclidean) -> None:
self.dist = dist
self.points: list[Point] = []
self.children: list[dict[int, list[int]]] = []
self.dups: dict[int, list[int]] = {}
self.root: Optional[int] = None
self.top = 0 # level of the root
self.bottom = 0 # lowest level of any node
def _d(self, p: Point, i: int) -> float:
return self.dist(p, self.points[i])
def _kids(self, qs: Sequence[int], i: int) -> list[int]:
"""Children at level i - 1 of the nodes qs at level i, including the nodes themselves."""
out = list(qs)
for q in qs:
out.extend(self.children[q].get(i, ()))
return out
def insert(self, p: Point) -> int:
idx = len(self.points)
self.points.append(p)
self.children.append({})
if self.root is None:
self.root = idx
return idx
d0 = self._d(p, self.root)
if d0 == 0:
self.dups.setdefault(self.root, []).append(idx)
return idx
while d0 > 2.0 ** self.top: # raise the root until it covers p
self.top += 1
self._insert(idx, [self.root], self.top)
return idx
def _insert(self, idx: int, qi: list[int], i: int) -> bool:
"""Returns True for "parent found"."""
p = self.points[idx]
q_all = self._kids(qi, i) # @line L1
dists = {q: self._d(p, q) for q in q_all}
dmin = min(dists.values())
if dmin == 0: # a duplicate of an existing point
self.dups.setdefault(min(q_all, key=dists.__getitem__), []).append(idx)
return True
if dmin > 2.0 ** i: # @line L2
return False
q_next = [q for q in q_all if dists[q] <= 2.0 ** i] # @line L3
if self._insert(idx, q_next, i - 1): # @line L4
return True
near = [q for q in qi if dists[q] <= 2.0 ** i] # @line L5
if not near:
return False
parent = min(near, key=dists.__getitem__)
self.children[parent].setdefault(i, []).append(idx) # @line L6
self.bottom = min(self.bottom, i - 1)
return True
def nearest(self, p: Point) -> Optional[tuple[float, int]]:
"""(distance, index) of a nearest stored point, or None if empty."""
if self.root is None:
return None
qi = [self.root] # @line L7
for i in range(self.top, self.bottom, -1): # @line L8
q_all = self._kids(qi, i)
dists = {q: self._d(p, q) for q in q_all}
bound = min(dists.values()) + 2.0 ** i
qi = [q for q in q_all if dists[q] <= bound] # @line L9
return min((self._d(p, q), q) for q in qi) # @line L10
def levels(self) -> dict[int, list[int]]:
"""C_i for each level from top down to bottom (as sorted index lists)."""
out: dict[int, list[int]] = {}
if self.root is None:
return out
cur = [self.root]
for i in range(self.top, self.bottom - 1, -1):
out[i] = sorted(cur)
cur = self._kids(cur, i)
return out
def parent_at(self) -> dict[tuple[int, int], int]:
"""(point, level) -> parent at level + 1, for explicit and implicit nodes."""
par: dict[tuple[int, int], int] = {}
if self.root is None:
return par
cur = [self.root]
for i in range(self.top, self.bottom, -1):
nxt = []
for q in cur:
par[(q, i - 1)] = q
nxt.append(q)
for c in self.children[q].get(i, ()):
par[(c, i - 1)] = q
nxt.append(c)
cur = nxt
return par
test_cover_tree.py 76 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/cover-tree/. The authors are listed in prov/GE45W4.jsonl.
import math
import random
from cover_tree import CoverTree
LINE = [(0,), (8,), (4,), (2,), (6,), (1,), (7,), (3,)]
def check_invariants(t: CoverTree) -> None:
levels = t.levels()
par = t.parent_at()
for i, ci in levels.items():
# nesting
if i - 1 in levels:
assert set(ci) <= set(levels[i - 1])
# separation
for a in range(len(ci)):
for b in range(a + 1, len(ci)):
assert t.dist(t.points[ci[a]], t.points[ci[b]]) > 2.0 ** i
# covering
if i + 1 in levels:
for p in ci:
q = par[(p, i)]
assert q in levels[i + 1]
assert t.dist(t.points[p], t.points[q]) <= 2.0 ** (i + 1)
# every distinct point appears at the bottom level
stored = set(levels[min(levels)]) if levels else set()
dups = {d for v in t.dups.values() for d in v}
assert stored | dups == set(range(len(t.points)))
def test_example() -> None:
t = CoverTree()
for p in LINE:
t.insert(p)
assert (t.top, t.bottom) == (3, -1)
assert t.levels() == {3: [0], 2: [0, 1], 1: [0, 1, 2], 0: [0, 1, 2, 3, 4],
-1: [0, 1, 2, 3, 4, 5, 6, 7]}
assert t.children[0] == {3: [1], 2: [2], 1: [3], 0: [5]}
d, i = t.nearest((5.4,))
assert i == 4 and abs(d - 0.6) < 1e-12
check_invariants(t)
def test_edge_cases() -> None:
t = CoverTree()
assert t.nearest((1.0,)) is None
t.insert((1.0,))
assert t.nearest((5.0,)) == (4.0, 0)
t.insert((1.0,))
assert t.dups == {0: [1]}
t.insert((1.25,))
check_invariants(t)
assert t.nearest((1.2,))[1] == 2
def test_random_against_brute_force() -> None:
rng = random.Random(45)
for _ in range(500):
dim = rng.randint(1, 3)
n = rng.randint(1, 25)
grid = rng.random() < 0.3
pts = [tuple(float(rng.randint(0, 5)) if grid else rng.uniform(-10, 10)
for _ in range(dim)) for _ in range(n)]
t = CoverTree()
for p in pts:
t.insert(p)
check_invariants(t)
for _ in range(3):
q = tuple(rng.uniform(-12, 12) for _ in range(dim))
d, i = t.nearest(q)
assert abs(d - min(math.dist(q, p) for p in pts)) < 1e-9
assert abs(math.dist(q, pts[i]) - d) < 1e-12
What the next level needs
- the tests did not run in this build