codex/frequency-modulation-waveform-and-instantaneous-frequency.py

frequency-modulation-waveform-and-instantaneous-frequency.py
"""Illustrate a specified FM phase; emit the matching PNG in the current directory.

Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
Run from _media with MPLBACKEND=Agg and a writable MPLCONFIGDIR.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    t = np.linspace(0.0, 2.0, 10001)
    fc, fm, deviation = 20.0, 1.0, 5.0
    beta = deviation / fm
    phase = 2 * np.pi * fc * t + beta * np.sin(2 * np.pi * fm * t)
    waveform = np.cos(phase)
    instantaneous_frequency = fc + deviation * np.cos(2 * np.pi * fm * t)

    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axes = plt.subplots(2, 1, figsize=(9.8, 6.4), dpi=110,
                             sharex=True, layout="constrained",
                             gridspec_kw={"height_ratios": [1.45, 1]})
    fig.set_facecolor("white")
    fig.suptitle("Frequency modulation: constant amplitude, changing timing", fontsize=15)
    axes[0].set_title(r"Original model: $f_c=20$ Hz, $f_m=1$ Hz, $\Delta f=5$ Hz, $\beta=5$",
                      fontsize=11)
    for ax in axes:
        ax.set_facecolor("white")
        ax.set_xlim(0, 2)
        ax.set_xticks(np.arange(0, 2.01, 0.25))
        ax.grid(axis="x", color="#e4e7eb", linewidth=0.8)
        for center in (0.0, 1.0, 2.0):
            ax.axvspan(max(0, center - 0.12), min(2, center + 0.12),
                       color="#e2f0fa", zorder=0)
        for center in (0.5, 1.5):
            ax.axvspan(center - 0.12, center + 0.12, color="#fff0db", zorder=0)
    axes[0].plot(t, waveform, color="#205a8c", lw=1.25)
    for bound in (-1, 1):
        axes[0].axhline(bound, color="#59646e", ls="--", lw=1)
    axes[0].set_ylim(-1.45, 1.55)
    axes[0].set_yticks([-1, 0, 1])
    axes[0].set_ylabel(r"Real waveform $s(t)/A$")
    axes[0].text(0.04, 1.15, r"Envelope bounds: $\pm A$", color="#434b53")
    axes[0].text(0.78, 1.15, "closer cycles", color="#205a8c")
    axes[0].text(1.29, 1.15, "wider cycles", color="#91520e")
    axes[1].plot(t, instantaneous_frequency, color="#217865", lw=2.1)
    axes[1].axhline(fc, color="#59646e", ls="--", lw=1)
    axes[1].set_ylim(13.5, 27.7)
    axes[1].set_yticks([15, 20, 25])
    axes[1].set_ylabel(r"$f_i(t)=\phi'(t)/(2\pi)$  [Hz]")
    axes[1].set_xlabel("Time [s]")
    axes[1].text(0.57, 26.1, r"$f_i(t)=20+5\cos(2\pi t)$ Hz", color="#217865")
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()