codex/hann-periodogram-power-averaging.py

hann-periodogram-power-averaging.py
"""Exact variance of contiguous Hann-window periodogram-power averages.

Python 3.14, NumPy and Matplotlib; see pyproject.toml. Writes the PNG to cwd.
The model uses proper complex Gaussian white sample noise and the original
N=64 frequency grid. Blocks contain at most 32 bins, avoiding cyclic overlap.
"""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt


def normalized_mean_variance(size):
    size = np.asarray(size, dtype=float)
    return (size + 2 * np.maximum(size - 1, 0) * (4 / 9)
            + 2 * np.maximum(size - 2, 0) * (1 / 36)) / size**2


def main():
    count = np.arange(1, 33)
    variance = normalized_mean_variance(count)
    fig, axes = plt.subplots(1, 2, figsize=(9, 4.2), dpi=120, layout='constrained')
    fig.patch.set_facecolor('white')
    axes[0].plot(count, count, '--', color='#555555', label='Independent powers')
    axes[0].plot(count, 1 / variance, color='#176da5', linewidth=2.3,
                 label='Periodic Hann powers')
    axes[0].scatter([10], [1 / normalized_mean_variance(10)], color='#176da5', s=25)
    axes[0].annotate('10 bins: about 5.42 independent powers',
                     xy=(10, 1 / normalized_mean_variance(10)), xytext=(3, 20),
                     arrowprops={'arrowstyle': '->', 'color': '#333333'}, fontsize=8.5)
    axes[0].set(xlabel='Number M of neighboring bins averaged',
                ylabel='Effective independent-power count', xlim=(1, 32), ylim=(0, 33))
    axes[0].legend(loc='upper left', fontsize=8)
    axes[1].semilogy(count, 1 / count, '--', color='#555555', label='Independent powers')
    axes[1].semilogy(count, variance, color='#176da5', linewidth=2.3,
                     label='Periodic Hann powers')
    axes[1].set(xlabel='Number M of neighboring bins averaged',
                ylabel='Variance of mean power / single-power variance', xlim=(1, 32))
    axes[1].legend(fontsize=8)
    for axis in axes:
        axis.grid(alpha=0.2)
    fig.suptitle('Window-induced correlation reduces spectral averaging gain', fontsize=12.5)
    fig.savefig(Path('hann-periodogram-power-averaging.png'), facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()