codex/synthetic-dielectric-capacitance-frequency-response.py

synthetic-dielectric-capacitance-frequency-response.py
"""Draw synthetic static and frequency-dependent capacitance; Python 3.14.

Dependencies: matplotlib 3.10.7 and numpy 2.3.5, as in pyproject.toml.
Run from _media; this models hypothetical values, not measured material data.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    epsilon_0 = 8.854e-12  # Stipulated rounded value, F/m; not an exact SI constant.
    area = 0.010  # m²
    gap = 1.00e-3  # m
    c0_pf = epsilon_0 * area / gap * 1e12
    stray_pf = 20.0
    epsilon_r = np.linspace(1, 5, 200)
    frequency = np.logspace(1, 5, 401)  # Hz
    characteristic_frequency = 1000.0  # Hz
    epsilon_complex = 2.0 + 2.0 / (1.0 + 1j * frequency / characteristic_frequency)
    storage_pf = c0_pf * epsilon_complex.real
    loss_pf = c0_pf * (-epsilon_complex.imag)
    assert np.isclose(c0_pf, 88.54)
    assert np.all(loss_pf >= 0)
    plt.rcParams.update({"font.size": 11, "savefig.facecolor": "white"})
    fig, (static, dynamic) = plt.subplots(1, 2, figsize=(10.8, 4.8), dpi=110)
    fig.patch.set_facecolor("white")
    fig.suptitle("Synthetic dielectric capacitance response", fontsize=16, y=0.965)
    static.plot(epsilon_r, c0_pf * epsilon_r, color="#2866a3", lw=2.5,
                label=r"Ideal: $C=C_0\epsilon_r$")
    static.plot(epsilon_r, c0_pf * epsilon_r + stray_pf, color="#b57b13", lw=2,
                linestyle="--", label="With +20 pF stray capacitance")
    static.scatter([1, 4], [c0_pf, 4 * c0_pf], color="#2866a3", zorder=4)
    static.annotate("88.54 pF", (1, c0_pf), xytext=(1.3, 63), fontsize=10)
    static.annotate("354.16 pF", (4, 4 * c0_pf), xytext=(2.9, 315), fontsize=10)
    static.set(xlim=(0.9, 5.1), ylim=(0, 520), xticks=[1, 2, 3, 4, 5],
               xlabel=r"Static relative permittivity $\epsilon_r$", ylabel="Capacitance (pF)")
    static.set_title("Same area and gap", fontsize=13)
    static.legend(loc="upper left", frameon=False, fontsize=10)
    dynamic.semilogx(frequency, storage_pf, color="#2866a3", lw=2.5,
                     label=r"Storage: $C_p=C_0\epsilon_r'$ ")
    dynamic.semilogx(frequency, loss_pf, color="#b57b13", lw=2, linestyle="--",
                     label=r"Loss component: $C_0\epsilon_r''$")
    dynamic.axvline(characteristic_frequency, color="#a7a7a7", linestyle=":", lw=1.5)
    dynamic.scatter([1000, 1000], [3 * c0_pf, c0_pf], color=["#2866a3", "#b57b13"], zorder=4)
    dynamic.annotate("265.62 pF", (1000, 3 * c0_pf), xytext=(1700, 285), fontsize=10)
    dynamic.annotate("88.54 pF", (1000, c0_pf), xytext=(1600, 105), fontsize=10)
    dynamic.set(xlim=(10, 100000), ylim=(0, 430), xlabel="Frequency (Hz)",
                ylabel="Capacitance / loss component (pF)")
    dynamic.set_title(r"Assumed $\epsilon_r^*=2+2/(1+if/f_0)$", fontsize=13)
    dynamic.legend(loc="upper right", frameon=False, fontsize=10)
    for ax in [static, dynamic]:
        ax.grid(alpha=0.2)
        ax.spines[["top", "right"]].set_visible(False)
    fig.text(0.5, 0.055,
             "A = 0.010 m², d = 1.00 mm, ε₀ ≈ 8.854 × 10⁻¹² F/m; C₀ = 88.54 pF; f₀ = 1 kHz.",
             ha="center", fontsize=10)
    fig.text(0.5, 0.015, "All curves are model calculations, not experimental observations.",
             ha="center", fontsize=10, color="#4b515b")
    fig.subplots_adjust(left=0.075, right=0.98, bottom=0.2, top=0.84, wspace=0.29)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()