Practical walkthrough of generating, characterising, and whitening \(1/f^\alpha\) noise. See index.qmd for the theory and whitening.py for the implementation.
Show the code
import numpy as npimport matplotlib.pyplot as pltfrom whitening import ( generate_power_law_noise_psd, kasdin_coefficients, whiten, acf, estimate_lag1_correlation,)rng = np.random.default_rng(42)plt.rcParams.update({"figure.dpi": 100, "axes.grid": True})
1. Generate coloured noiseWe use generate_power_law_noise_psd() to create white (\(\alpha=0\)), pink (\(\alpha=1\)), and brown (\(\alpha=2\)) noise by shaping the spectrum of a white noise sequence. Note: this function uses np.random internally, so we set the legacy seed for reproducibility here.
Show the code
np.random.seed(42)N =4096alphas= [0, 1, 2]labels = ["White (α=0)", "Pink (α=1)", "Brown (α=2)"]signals = {a: generate_power_law_noise_psd(N, alpha=a) for a in alphas}fig, axes = plt.subplots(len(alphas), 2, figsize=(10, 6), constrained_layout=True)for i, a inenumerate(alphas): x = signals[a] # Time series axes[i, 0].plot(x[:500], lw=0.5) axes[i, 0].set_ylabel(labels[i]) if i == 0: axes[i, 0].set_title("Time series (first 500 samples)") # PSD via Welch-like periodogram freqs = np.fft.rfftfreq(N, d=1.0)[1:] # skip DC psd = np.abs(np.fft.rfft(x))[1:] ** 2 / N axes[i, 1].loglog(freqs, psd, lw=0.5, alpha=0.7) axes[i, 1].set_ylabel("PSD") if i == 0: axes[i, 1].set_title("Power spectral density") if i == len(alphas) - 1: axes[i, 0].set_xlabel("Sample") axes[i, 1].set_xlabel("Frequency (normalised)")plt.show()
2. Kasdin coefficientsThe AR coefficients for \(1/f^\alpha\) noise follow a recurrence relation. For white noise (\(\alpha=0\)) there is only the trivial coefficient \([1]\). For Brownian noise (\(\alpha=2\)) the coefficients are \([1, -1]\) — a simple integrator. Pink noise (\(\alpha=1\)) has a long, slowly decaying tail.
Show the code
fig, ax = plt.subplots(figsize=(8, 3.5))for a, label inzip([0.5, 1.0, 1.5, 2.0], ["α=0.5", "α=1.0", "α=1.5", "α=2.0"]): c = kasdin_coefficients(a, min_magnitude=1e-4) ax.plot(c, "o-", markersize=3, label=f"{label} ({len(c)} coeffs)")ax.set_xlabel("Coefficient index")ax.set_ylabel("Value")ax.set_title("Kasdin AR coefficients for different α")ax.legend()plt.tight_layout()plt.show()# Print first few coefficients for brown noiseprint("Brown (α=2) coefficients:", kasdin_coefficients(2.0))
3. Whitening in actionTake brown noise (\(\alpha=2\)), apply the whitening filter, and compare PSD and autocorrelation before and after. The whitened signal should look approximately white.
4. Lag-1 correlation estimation (streaming)The recursive estimator from estimate_lag1_correlation() tracks the lag-1 autocorrelation sample by sample. Here we watch it converge on pink noise (\(\alpha=1\)).
5. Alpha estimation from lag-1 autocorrelationThe logistic relationship \(r_1 \approx 1/(1 + e^{-4\alpha+3})\) lets us estimate \(\alpha\) from a single autocorrelation coefficient. We generate noise at several known \(\alpha\) values and compare the estimate to ground truth.
Show the code
def estimate_alpha_from_r1(r1): """Invert the logistic fit to get alpha from lag-1 autocorrelation.""" r1 = np.clip(r1, 1e-6, 1-1e-6) # avoid log(0) return (3 - np.log(1 / r1 - 1)) / 4true_alphas = np.arange(0.2, 2.01, 0.2)estimated_alphas = []np.random.seed(42)for a in true_alphas: x = generate_power_law_noise_psd(8192, alpha=a) r1 = acf(x, max_lag=1)[1] estimated_alphas.append(estimate_alpha_from_r1(r1))fig, ax = plt.subplots(figsize=(6, 4))ax.plot([0, 2.2], [0, 2.2], "k--", lw=0.8, label="Ideal")ax.plot(true_alphas, estimated_alphas, "o-", markersize=6, label="Estimated")ax.set_xlabel("True α")ax.set_ylabel("Estimated α")ax.set_title("Alpha estimation via lag-1 autocorrelation")ax.legend()ax.set_xlim(0, 2.2)ax.set_ylim(0, 2.2)ax.set_aspect("equal")plt.tight_layout()plt.show()for a_true, a_est in zip(true_alphas, estimated_alphas): print(f" α = {a_true:.1f} → estimate = {a_est:.2f} (error = {a_est - a_true:+.2f})")
Key takeaways- Power law noise (\(1/f^\alpha\)) is common in practice. Its correlated samples break methods that assume white noise.- Kasdin’s recurrence gives AR coefficients that model the noise; inverting that AR filter gives a simple FIR whitening filter.- Whitening flattens the PSD and drives the autocorrelation toward a delta function, restoring the white-noise assumption.- The lag-1 autocorrelation provides a cheap, single-number estimate of \(\alpha\) via a logistic fit – useful for adaptive whitening in streaming applications.