Power Spectral Density Example — Multi-Tone Signal

This example generates a noisy signal composed of sinusoids at 10 Hz, 50 Hz, and 120 Hz, computes the one-sided power spectral density (PSD) using transforms.fft_real_forward() (magnitude squared normalised by sample rate and length), and identifies the three dominant frequency peaks.

Example Code

"""IMSL Power Spectral Density example: PSD of a multi-tone signal with noise.

Signal: sum of sinusoids at 10 Hz, 50 Hz, 120 Hz embedded in white noise.

Outputs:
- Table printed to stdout showing dominant frequencies
- SVG plot saved to test_output/example_imsl_power_spectrum.svg
"""

from __future__ import annotations

from pathlib import Path
from typing import Dict, List

import matplotlib.pyplot as plt
import numpy as np

from transforms import fft_real_forward


def run_demo_imsl_power_spectrum() -> Dict[str, object]:
    """Run IMSL power spectral density example.

    Generates a noisy multi-tone signal with components at 10 Hz, 50 Hz, and
    120 Hz, computes the one-sided power spectral density (PSD) using
    :func:`transforms.fft_real_forward`, then identifies the dominant peaks.

    Args:
        None

    Returns:
        Dict[str, object]: Result dict with keys ``dominant_freqs``
            (list of float), ``psd_peak_values`` (list of float), and
            ``plot_path`` (str).
    """
    rng = np.random.default_rng(42)
    fs = 1024.0
    N = 2048
    dt = 1.0 / fs
    t = np.arange(N) * dt

    signal = (
        2.0 * np.sin(2 * np.pi * 10 * t)
        + 1.5 * np.sin(2 * np.pi * 50 * t)
        + 1.0 * np.sin(2 * np.pi * 120 * t)
        + 0.5 * rng.standard_normal(N)
    )

    X = fft_real_forward(signal)
    freqs = np.fft.rfftfreq(N, d=dt)
    # Power spectral density: |X|^2 / (fs * N), scaled to one-sided
    psd = (np.abs(X) ** 2) / (fs * N)
    psd[1:-1] *= 2.0  # double non-DC, non-Nyquist bins for one-sided

    # Identify top-3 peaks
    sorted_idx = np.argsort(psd)[::-1]
    top_idx = sorted_idx[:3]
    top_freqs: List[float] = freqs[top_idx].tolist()
    top_psd: List[float] = psd[top_idx].tolist()

    print("\nIMSL Power Spectral Density Example")
    print("Signal: 2·sin(2π·10t) + 1.5·sin(2π·50t) + sin(2π·120t) + noise")
    print(f"  Sample rate: {fs:.0f} Hz   N={N}")
    print("-" * 55)
    print(f"{'Rank':<6} {'Frequency (Hz)':>16} {'PSD (V²/Hz)':>14}")
    print("-" * 55)
    for i, (f, p) in enumerate(zip(top_freqs, top_psd), 1):
        print(f"{i:<6} {f:>16.2f} {p:>14.6f}")
    print("-" * 55)

    output_dir = Path("test_output")
    output_dir.mkdir(parents=True, exist_ok=True)
    plot_path = output_dir / "example_imsl_power_spectrum.svg"

    fig, axes = plt.subplots(2, 1, figsize=(10, 7))

    axes[0].plot(t[:512], signal[:512], color="#6d28d9", linewidth=0.9)
    axes[0].set_xlabel("Time (s)")
    axes[0].set_ylabel("Amplitude")
    axes[0].set_title("Time-Domain Signal (first 0.5 s)")
    axes[0].grid(True, alpha=0.3)

    axes[1].semilogy(freqs, psd, color="#7c3aed", linewidth=1.2)
    peak_colors = ["#dc2626", "#d97706", "#059669"]
    labels = ["10 Hz", "50 Hz", "120 Hz"]
    for freq, color, label in zip([10, 50, 120], peak_colors, labels):
        axes[1].axvline(freq, color=color, linestyle="--", linewidth=1.2, label=label)
    axes[1].set_xlabel("Frequency (Hz)")
    axes[1].set_ylabel("PSD (V²/Hz, log scale)")
    axes[1].set_title("One-Sided Power Spectral Density")
    axes[1].set_xlim(0, 200)
    axes[1].legend()
    axes[1].grid(True, alpha=0.3, which="both")

    fig.tight_layout()
    fig.savefig(plot_path, format="svg")
    plt.close(fig)

    return {
        "dominant_freqs": top_freqs,
        "psd_peak_values": top_psd,
        "plot_path": str(plot_path),
    }


if __name__ == "__main__":
    run_demo_imsl_power_spectrum()

Plot Output

Time-domain signal and one-sided PSD on log scale

Top: time-domain signal showing the noisy composite waveform. Bottom: one-sided PSD (log scale) with vertical markers at 10 Hz, 50 Hz, and 120 Hz — the three injected sinusoidal components.

Console Output

IMSL Power Spectral Density Example
Signal: 2·sin(2π·10t) + 1.5·sin(2π·50t) + sin(2π·120t) + noise
  Sample rate: 1024 Hz   N=2048
-------------------------------------------------------
Rank     Frequency (Hz)    PSD (V┬▓/Hz)
-------------------------------------------------------
1                 10.00       4.060980
2                 50.00       2.276353
3                120.00       0.987153
-------------------------------------------------------