codex/capacitor-charge-and-energy.py

capacitor-charge-and-energy.py
"""Compare capacitor charge and field energy for C and twice C.

Tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5 as in pyproject.toml.
Write the opaque PNG with this script's basename to the current directory.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    epsilon, area, gap = 8.854e-12, 0.01, 0.001
    capacitance = epsilon * area / gap
    reference_voltage = 10.0
    charge = capacitance * reference_voltage
    energy = 0.5 * capacitance * reference_voltage**2
    np.testing.assert_allclose([capacitance, charge, energy],
                               [8.854e-11, 8.854e-10, 4.427e-9], rtol=1e-14)
    np.testing.assert_allclose(energy, charge**2 / (2 * capacitance))
    np.testing.assert_allclose(energy, 0.5 * epsilon * (reference_voltage / gap)**2 * area * gap)
    # Independent comparisons used in the accompanying article.
    np.testing.assert_allclose(epsilon * (2 * area) / gap, 2 * capacitance)
    np.testing.assert_allclose(epsilon * area / (gap / 2), 2 * capacitance)
    np.testing.assert_allclose(charge**2 / (2 * (2 * capacitance)), energy / 2)
    np.testing.assert_allclose(0.5 * (2 * capacitance) * reference_voltage**2, 2 * energy)
    np.testing.assert_allclose(0.5 * capacitance * (2 * reference_voltage)**2, 4 * energy)

    blue, orange, grey = "#176997", "#b65a23", "#52616c"
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans",
                         "axes.spines.top": False, "axes.spines.right": False})
    fig, axes = plt.subplots(1, 2, figsize=(10.0, 5.0), dpi=120, facecolor="white")
    voltage = np.linspace(0, 20, 401)
    for multiplier, colour, style, label in [
        (1, blue, "-", "C = 88.54 pF"),
        (2, orange, "--", "2C = 177.08 pF"),
    ]:
        q_nano = multiplier * capacitance * voltage * 1e9
        u_nano = 0.5 * multiplier * capacitance * voltage**2 * 1e9
        axes[0].plot(voltage, q_nano, color=colour, ls=style, lw=2.4, label=label)
        axes[1].plot(voltage, u_nano, color=colour, ls=style, lw=2.4, label=label)
        axes[0].scatter([reference_voltage], [multiplier * charge * 1e9], color=colour, zorder=5, s=36)
        axes[1].scatter([reference_voltage], [multiplier * energy * 1e9], color=colour, zorder=5, s=36)

    axes[0].annotate("0.8854 nC", (10, charge * 1e9), xytext=(12.0, 0.35),
                     arrowprops={"arrowstyle": "->", "color": blue}, color=blue)
    axes[0].annotate("1.7708 nC", (10, 2 * charge * 1e9), xytext=(2.2, 2.30),
                     arrowprops={"arrowstyle": "->", "color": orange}, color=orange)
    axes[1].annotate("4.427 nJ", (10, energy * 1e9), xytext=(12.3, 1.5),
                     arrowprops={"arrowstyle": "->", "color": blue}, color=blue)
    axes[1].annotate("8.854 nJ", (10, 2 * energy * 1e9), xytext=(2.0, 17.0),
                     arrowprops={"arrowstyle": "->", "color": orange}, color=orange)

    for ax in axes:
        ax.axvline(reference_voltage, color="#8998a1", lw=1, ls=":", zorder=0)
        ax.set(xlim=(0, 20), xlabel="Terminal potential difference V (V)")
        ax.set_xticks([0, 5, 10, 15, 20])
        ax.grid(alpha=0.15)
        ax.legend(loc="upper left", fontsize=10, frameon=False)
    axes[0].set(ylim=(0, 3.9), ylabel="Plate charge magnitude Q (nC)")
    axes[1].set(ylim=(0, 39), ylabel="Stored field energy U (nJ)")
    axes[0].set_title(r"Linear: $Q=CV$", pad=12, fontsize=13)
    axes[1].set_title(r"Quadratic: $U=\frac{1}{2}CV^2$", pad=12, fontsize=13)

    fig.suptitle("Charge grows linearly; capacitor energy grows quadratically", fontsize=15, y=0.97)
    fig.text(0.5, 0.89, "Same vacuum example: A = 0.01 m², d = 1 mm; ε₀ ≈ 8.854 × 10⁻¹² F/m",
             ha="center", color=grey, fontsize=10)
    fig.text(0.5, 0.08, "Dots mark V = 10 V. At the same V, twice the capacitance stores twice the charge and energy.",
             ha="center", fontsize=10)
    fig.text(0.5, 0.035, "At fixed Q instead, twice the capacitance halves stored energy: U = Q²/(2C).",
             ha="center", color=grey, fontsize=10)
    fig.subplots_adjust(left=0.08, right=0.97, top=0.77, bottom=0.24, wspace=0.28)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()