Algorithm Search⌘K

Sparse vector in Python

Vector stored as a symbol table of its nonzero entries, making dot products cost proportional to the nonzero count.

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

import math
from collections.abc import Iterator, Sequence


class SparseVector:
    """A vector of dimension d stored as a hash table of its nonzero entries.

    get and put take O(1) expected time, dot with another sparse vector takes
    O(1 + min(nnz(u), nnz(w))) expected time, dot with a dense list takes
    O(1 + nnz), plus takes O(1 + nnz(u) + nnz(w)) expected time. Space is
    Theta(1 + nnz). No stored value is ever 0.
    """

    def __init__(self, d: int, entries: dict[int, float] | None = None) -> None:
        if d < 0:
            raise ValueError("dimension must be nonnegative")
        self.d = d
        self.table: dict[int, float] = {}
        if entries:
            for i, x in entries.items():
                self.put(i, x)

    def _check_index(self, i: int) -> None:
        if not 0 <= i < self.d:
            raise IndexError(f"index {i} out of range for dimension {self.d}")

    def put(self, i: int, x: float) -> None:
        """Set entry i to x; a zero value removes the index from the table."""
        self._check_index(i)
        if x == 0:  # @line L1
            self.table.pop(i, None)
        else:
            self.table[i] = x  # @line L2

    def get(self, i: int) -> float:
        """Return entry i, which is 0 when i is not stored."""
        self._check_index(i)
        return self.table.get(i, 0.0)  # @line L3

    def nnz(self) -> int:
        """Return the number of nonzero entries."""
        return len(self.table)

    def items(self) -> Iterator[tuple[int, float]]:
        """Yield the pairs (index, value) of the nonzero entries in index order."""
        for i in sorted(self.table):
            yield i, self.table[i]

    def dot(self, other: "SparseVector") -> float:
        """Return the dot product with another sparse vector of the same dimension."""
        if self.d != other.d:
            raise ValueError("dimensions differ")
        u, w = self, other
        if len(u.table) > len(w.table):  # @line L4
            u, w = w, u
        s = 0.0  # @line L5
        for i, x in u.table.items():  # @line L6
            s += x * w.table.get(i, 0.0)  # @line L7
        return s  # @line L8

    def dot_dense(self, a: Sequence[float]) -> float:
        """Return the dot product with a dense sequence of length d."""
        if len(a) != self.d:
            raise ValueError("dimensions differ")
        s = 0.0
        for i, x in self.table.items():
            s += x * a[i]
        return s

    def plus(self, other: "SparseVector") -> "SparseVector":
        """Return the sum as a new sparse vector; entries that cancel are dropped."""
        if self.d != other.d:
            raise ValueError("dimensions differ")
        r = SparseVector(self.d)
        r.table = dict(self.table)
        for i, x in other.table.items():
            r.put(i, r.get(i) + x)
        return r

    def scale(self, alpha: float) -> "SparseVector":
        """Return alpha times this vector as a new sparse vector."""
        r = SparseVector(self.d)
        if alpha != 0:
            for i, x in self.table.items():
                r.put(i, alpha * x)
        return r

    def norm(self) -> float:
        """Return the Euclidean norm, the square root of the dot product with itself."""
        return math.sqrt(self.dot(self))

    def to_dense(self) -> list[float]:
        """Return the vector as a dense list of length d."""
        a = [0.0] * self.d
        for i, x in self.table.items():
            a[i] = x
        return a

    def check_invariant(self) -> None:
        """Check that every key is in range and every stored value is nonzero."""
        for i, x in self.table.items():
            assert isinstance(i, int) and 0 <= i < self.d, i
            assert x != 0, (i, x)
test_sparse_vector.py 76 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/sparse-vector/. The authors are listed in prov/E8QPZ3.jsonl.

import math
import random

import pytest

from sparse_vector import SparseVector


def test_example() -> None:
    u = SparseVector(10, {1: 0.5, 4: 2.0, 7: -1.0})
    w = SparseVector(10, {0: 3.0, 4: 1.5, 7: 2.0, 9: 1.0})
    assert u.get(5) == 0
    assert u.dot(w) == 1.0 and w.dot(u) == 1.0
    assert dict(u.plus(w).items()) == {0: 3.0, 1: 0.5, 4: 3.5, 7: 1.0, 9: 1.0}
    z = SparseVector(10, {7: 1.0})
    s = u.plus(z)
    assert dict(s.items()) == {1: 0.5, 4: 2.0}
    s.check_invariant()
    assert math.isclose(u.norm(), 2.29128784747792)
    u.put(4, 0)
    assert dict(u.items()) == {1: 0.5, 7: -1.0}
    u.check_invariant()


def test_edges() -> None:
    e = SparseVector(0)
    assert e.nnz() == 0 and e.norm() == 0.0 and e.dot(SparseVector(0)) == 0.0
    v = SparseVector(3)
    with pytest.raises(IndexError):
        v.put(3, 1.0)
    with pytest.raises(IndexError):
        v.get(-1)
    with pytest.raises(ValueError):
        v.dot(SparseVector(4))
    with pytest.raises(ValueError):
        v.plus(SparseVector(4))
    with pytest.raises(ValueError):
        v.dot_dense([1.0, 2.0])
    v.put(0, 0)
    assert v.nnz() == 0
    v.put(2, 5.0)
    assert v.scale(0).nnz() == 0
    assert v.scale(-2).to_dense() == [0.0, 0.0, -10.0]
    assert v.dot_dense([1.0, 1.0, 3.0]) == 15.0


def test_random_against_dense() -> None:
    rng = random.Random(20261104)
    for _ in range(600):
        d = rng.randint(1, 40)
        u, w = SparseVector(d), SparseVector(d)
        du, dw = [0] * d, [0] * d
        for _ in range(rng.randint(0, 60)):
            vec, dense = (u, du) if rng.random() < 0.5 else (w, dw)
            i = rng.randrange(d)
            x = rng.choice([0, 0, rng.randint(-5, 5)])
            vec.put(i, x)
            dense[i] = x
            vec.check_invariant()
        assert u.to_dense() == du and w.to_dense() == dw
        assert u.nnz() == sum(1 for x in du if x != 0)
        assert u.dot(w) == sum(a * b for a, b in zip(du, dw))
        assert u.dot_dense(dw) == u.dot(w)
        r = u.plus(w)
        r.check_invariant()
        assert r.to_dense() == [a + b for a, b in zip(du, dw)]
        alpha = rng.randint(-3, 3)
        sc = u.scale(alpha)
        sc.check_invariant()
        assert sc.to_dense() == [alpha * a for a in du]
        assert math.isclose(u.norm(), math.sqrt(sum(a * a for a in du)))
        for i in range(d):
            assert u.get(i) == du[i]

What the next level needs

  • the tests did not run in this build

Read the full page about Sparse vector