codex/series-rc-impedance-locus-and-frequency.py

series-rc-impedance-locus-and-frequency.py
"""Draw impedance locus, modulus and phase; Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7."""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt


def main():
    resistance, capacitance = 100.0, 1e-6
    frequencies = np.geomspace(100, 100000, 700)
    impedances = resistance - 1j / (2 * np.pi * frequencies * capacitance)
    crossover = 1 / (2 * np.pi * resistance * capacitance)
    blue, orange, green = '#1565a8', '#c55d14', '#247650'
    plt.rcParams.update({'font.size': 11, 'axes.titlesize': 12, 'figure.facecolor': 'white'})
    fig = plt.figure(figsize=(10.4, 5.3))
    grid = fig.add_gridspec(2, 2, width_ratios=[1, 1.65], left=.12, right=.97, bottom=.18, top=.79, wspace=.43, hspace=.36)
    locus = fig.add_subplot(grid[:, 0]); magnitude = fig.add_subplot(grid[0, 1]); phase = fig.add_subplot(grid[1, 1], sharex=magnitude)
    fig.suptitle('How the ideal series RC impedance changes with frequency', fontsize=15, y=.97)
    fig.text(.5, .885, r'$R=100\ \Omega$, $C=1\ \mu\mathrm{F}$: predictions of the stipulated model', ha='center')
    locus.axhline(0, color='.5', lw=1)
    locus.plot(impedances.real, impedances.imag, color=blue, lw=2.5)
    locus.annotate('', (100, -500), (100, -950), arrowprops={'arrowstyle': '-|>', 'color': blue, 'lw': 2.5})
    locus.text(113, -730, 'Increasing\nfrequency', color=blue, va='center')
    locus.scatter([100], [0], marker='o', s=48, facecolor='white', edgecolor=green, lw=1.7, zorder=5)
    locus.text(110, 48, r'$Z\to R$', color=green, fontsize=11)
    samples = [(100, '100 Hz', blue), (1000, '1000 Hz', orange), (crossover, r'$f_*$', green)]
    for f, label, color in samples:
        z = resistance - 1j / (2 * np.pi * f * capacitance)
        locus.scatter([z.real], [z.imag], color=color, s=40, zorder=6)
        if f == 100:
            locus.text(87, z.imag, label, ha='right', va='center', color=color)
        elif f == 1000:
            locus.annotate(label, (z.real, z.imag), xytext=(22, -355), color=color, arrowprops={'arrowstyle': '-', 'color': color})
        else:
            locus.annotate(label, (z.real, z.imag), xytext=(42, -48), color=color, arrowprops={'arrowstyle': '-', 'color': color})
        magnitude.scatter([f], [abs(z)], color=color, s=32, zorder=6)
        phase.scatter([f], [np.degrees(np.angle(z))], color=color, s=32, zorder=6)
    locus.set(xlim=(0, 190), ylim=(-1750, 190), xlabel=r'Real part ($\Omega$)', ylabel=r'Imaginary part ($\Omega$)', title='Locus: fixed real part')
    locus.set_xticks([0, 100]); locus.set_yticks([-1500, -1000, -500, 0])
    fig.text(.12, .025, 'Locus axes use different scales; the entire locus is vertical.', fontsize=9, color='.35')
    magnitude.loglog(frequencies, np.abs(impedances), color=blue, lw=2.3)
    magnitude.axhline(resistance, color=green, ls='--', lw=1)
    magnitude.text(14000, 122, r'$|Z|\to100\ \Omega$', color=green, fontsize=10)
    magnitude.annotate(r'$\sqrt{2}R$ at $f_*$', (crossover, np.sqrt(2)*resistance), xytext=(3300, 420), color=green, arrowprops={'arrowstyle': '->', 'color': green}, fontsize=10)
    magnitude.set(ylabel=r'$|Z|$ ($\Omega$)', ylim=(85, 2200), title='Magnitude and signed phase of impedance')
    magnitude.tick_params(labelbottom=False)
    phase.semilogx(frequencies, np.degrees(np.angle(impedances)), color=blue, lw=2.3)
    phase.axhline(-45, color=green, ls=':', lw=1)
    phase.text(8000, -57, r'$\phi=-45^\circ$ at $f_*$', color=green, fontsize=10)
    phase.text(220, -31, '1000 Hz: −57.858°', color=orange, fontsize=10)
    phase.set(xlim=(100, 100000), ylim=(-93, 3), xlabel='Frequency (Hz)', ylabel=r'$\phi$ (degrees)', yticks=[-90, -45, 0])
    for ax in [magnitude, phase]:
        ax.axvline(crossover, color=green, ls=':', lw=1)
        ax.grid(alpha=.16)
    for ax in [locus, magnitude, phase]:
        ax.spines[['top', 'right']].set_visible(False)
    fig.savefig(Path(__file__).stem + '.png', dpi=110, facecolor='white')
    plt.close(fig)


if __name__ == '__main__':
    main()