Algorithm Search⌘K

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

Read the full page about Cover tree