codexhann-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()