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