Algorithm Search⌘K

Fast wavelet transform in Python

Computes the DWT in O(n) by repeatedly filtering and downsampling with a pair of filters.

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

import math

SQRT2 = math.sqrt(2.0)
SQRT3 = math.sqrt(3.0)

HAAR = [1 / SQRT2, 1 / SQRT2]
DAUBECHIES4 = [
    (1 + SQRT3) / (4 * SQRT2),
    (3 + SQRT3) / (4 * SQRT2),
    (3 - SQRT3) / (4 * SQRT2),
    (1 - SQRT3) / (4 * SQRT2),
]


def _check(n: int, h: list[float], levels: int) -> None:
    if len(h) < 2 or len(h) % 2:
        raise ValueError("the filter length must be even and at least 2")
    if levels < 0 or n < 1 or n % (1 << levels):
        raise ValueError("the length must be divisible by 2**levels")


def fwt(x: list[float], h: list[float], levels: int) -> list[float]:
    """Return the periodic orthogonal wavelet transform of x with `levels` levels.

    h is an orthonormal low-pass filter of even length L, that is
    sum(h[l] * h[l + 2s]) is 1 for s = 0 and 0 otherwise; the high-pass filter
    is g[l] = (-1)**l * h[L - 1 - l]. len(x) must be divisible by 2**levels.
    The output is [a_J, d_J, d_(J-1), ..., d_1], coarsest first.
    Cost: at most 2Ln multiplications and 2(L - 1)n additions, so O(n).
    """
    n, big_l = len(x), len(h)  # @line L1
    _check(n, h, levels)
    g = [(-1) ** l * h[big_l - 1 - l] for l in range(big_l)]  # @line L2
    a, m = [float(v) for v in x], n  # @line L3
    y = [0.0] * n
    for _ in range(levels):  # @line L4
        half = m // 2
        s = [0.0] * half
        d = [0.0] * half
        for i in range(half):  # @line L5
            s[i] = sum(h[l] * a[(2 * i + l) % m] for l in range(big_l))  # @line L6
            d[i] = sum(g[l] * a[(2 * i + l) % m] for l in range(big_l))  # @line L7
        y[half:m], a, m = d, s, half  # @line L8
    y[0:m] = a  # @line L9
    return y  # @line L10


def inverse_fwt(y: list[float], h: list[float], levels: int) -> list[float]:
    """Return x with fwt(x, h, levels) == y, by the transposed filter bank.

    Same preconditions as fwt. Cost: the same as fwt.
    """
    n, big_l = len(y), len(h)
    _check(n, h, levels)
    g = [(-1) ** l * h[big_l - 1 - l] for l in range(big_l)]
    m = n >> levels  # @line L11
    a = [float(v) for v in y[0:m]]
    for _ in range(levels):  # @line L12
        b = [0.0] * (2 * m)  # @line L13
        for i in range(m):  # @line L14
            for l in range(big_l):
                b[(2 * i + l) % (2 * m)] += h[l] * a[i] + g[l] * y[m + i]
        a, m = b, 2 * m  # @line L15
    return a  # @line L16
test_fast_wavelet_transform.py 140 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/fast-wavelet-transform/. The authors are listed in prov/H8RGV6.jsonl.

import math
import random

import pytest

from fast_wavelet_transform import DAUBECHIES4, HAAR, fwt, inverse_fwt

S2 = math.sqrt(2.0)
FILTERS = [HAAR, DAUBECHIES4]


def level_matrix(m: int, h: list[float]) -> list[list[float]]:
    """The m x m one-level analysis matrix, built from its definition."""
    big_l = len(h)
    g = [(-1) ** l * h[big_l - 1 - l] for l in range(big_l)]
    w = [[0.0] * m for _ in range(m)]
    for i in range(m // 2):
        for l in range(big_l):
            w[i][(2 * i + l) % m] += h[l]
            w[m // 2 + i][(2 * i + l) % m] += g[l]
    return w


def naive_fwt(x: list[float], h: list[float], levels: int) -> list[float]:
    """O(n^2) oracle: multiply the leading block by the dense level matrix."""
    y = [float(v) for v in x]
    m = len(x)
    for _ in range(levels):
        w = level_matrix(m, h)
        head = [sum(w[r][c] * y[c] for c in range(m)) for r in range(m)]
        y[0:m] = head
        m //= 2
    return y


def close(a: list[float], b: list[float], scale: float) -> bool:
    n = max(len(a), 1)
    tol = 1e-13 * n * max(scale, 1.0)
    return len(a) == len(b) and all(abs(u - v) <= tol for u, v in zip(a, b))


def norm(v: list[float]) -> float:
    return math.sqrt(sum(t * t for t in v))


def test_filters_are_orthonormal() -> None:
    for h in FILTERS:
        big_l = len(h)
        for s in range(big_l // 2):
            dot = sum(h[l] * h[l + 2 * s] for l in range(big_l - 2 * s))
            assert abs(dot - (1.0 if s == 0 else 0.0)) < 1e-15


def test_haar_example() -> None:
    x = [4, 6, 10, 12, 8, 6, 5, 5]
    want = [14 * S2, 2 * S2, -6, 2, -S2, -S2, S2, 0]
    assert close(fwt(x, HAAR, 3), want, 30)
    assert close(inverse_fwt(want, HAAR, 3), [float(v) for v in x], 30)
    assert close(fwt(x, HAAR, 1), [10 / S2, 22 / S2, 14 / S2, 10 / S2, -S2, -S2, S2, 0], 30)


def test_daubechies4_kills_linear_data() -> None:
    x = list(range(8))
    y = fwt(x, DAUBECHIES4, 1)
    assert all(abs(t) < 1e-12 for t in y[4:7])
    assert abs(y[7]) > 1


def test_edge_cases() -> None:
    for h in FILTERS:
        assert fwt([3.5], h, 0) == [3.5]
        assert inverse_fwt([3.5], h, 0) == [3.5]
        assert close(fwt([1, 3], h, 1), naive_fwt([1, 3], h, 1), 4)
        assert close(inverse_fwt(fwt([1, 3], h, 1), h, 1), [1.0, 3.0], 4)
        for n in (2, 4, 8, 16, 32):
            j = n.bit_length() - 1
            assert fwt([0.0] * n, h, j) == [0.0] * n
            const = fwt([2.0] * n, h, j)
            assert abs(const[0] - 2.0 * math.sqrt(n)) < 1e-12
            assert all(abs(t) < 1e-12 for t in const[1:])
            for p in range(n):
                imp = [0.0] * n
                imp[p] = 1.0
                y = fwt(imp, h, j)
                assert close(y, naive_fwt(imp, h, j), 1)
                assert abs(norm(y) - 1.0) < 1e-12
                assert close(inverse_fwt(y, h, j), imp, 1)


def test_haar_basis_inner_products() -> None:
    n = 16
    rng = random.Random(7)
    x = [rng.uniform(-1, 1) for _ in range(n)]
    y = fwt(x, HAAR, 4)
    assert abs(y[0] - sum(x) / 4) < 1e-12
    for level in range(1, 5):
        width = 1 << level
        start = n >> level
        for i in range(n >> level):
            seg = x[i * width:(i + 1) * width]
            want = (sum(seg[: width // 2]) - sum(seg[width // 2:])) / math.sqrt(width)
            assert abs(y[start + i] - want) < 1e-12


def test_random_against_oracle() -> None:
    rng = random.Random(20261107)
    cases = 0
    for _ in range(300):
        h = rng.choice(FILTERS)
        m0 = rng.choice([1, 1, 2, 3, 5])
        levels = rng.randint(0, 5)
        n = m0 << levels
        if n > 96:
            continue
        x = [rng.uniform(-100, 100) for _ in range(n)]
        y = fwt(x, h, levels)
        assert close(y, naive_fwt(x, h, levels), 100)
        assert close(inverse_fwt(y, h, levels), x, 100)
        assert abs(norm(y) - norm(x)) <= 1e-12 * max(1, n) * max(norm(x), 1)
        cases += 1
    for _ in range(400):
        h = rng.choice(FILTERS)
        levels = rng.randint(0, 7)
        n = rng.choice([1, 2, 3]) << levels
        x = [rng.uniform(-1, 1) for _ in range(n)]
        y = fwt(x, h, levels)
        assert close(inverse_fwt(y, h, levels), x, 1)
        assert abs(norm(y) - norm(x)) <= 1e-13 * n * max(norm(x), 1)
        cases += 1
    assert cases >= 500


def test_bad_input() -> None:
    with pytest.raises(ValueError):
        fwt([1.0, 2.0, 3.0], HAAR, 1)
    with pytest.raises(ValueError):
        fwt([1.0, 2.0], [1.0, 2.0, 3.0], 1)

What the next level needs

  • the tests did not run in this build

Read the full page about Fast wavelet transform