codex/inductor-energy-storage-and-release.py

inductor-energy-storage-and-release.py
"""Show magnetic energy buildup and release into a closed resistive loop.

Tested with Python 3.14.4, numpy 2.3.5 and matplotlib 3.10.7.
Write an 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():
    resistance, inductance, voltage = 2.0, 0.5, 6.0
    tau = inductance / resistance
    initial_current = voltage / resistance
    initial_energy = 0.5 * inductance * initial_current**2
    initial_power = resistance * initial_current**2
    time = np.linspace(0.0, 5.0 * tau, 2001)
    decay = np.exp(-time / tau)
    stored_charge = 0.5 * inductance * (initial_current * (1.0 - decay))**2
    stored_decay = initial_energy * decay**2
    dissipated = initial_energy * (1.0 - decay**2)
    power = initial_power * decay**2
    energy_tau = initial_energy * (1.0 - np.exp(-1.0))**2
    full_integral = initial_power * tau / 2.0
    np.testing.assert_allclose([initial_energy, initial_power, full_integral],
                               [2.25, 18.0, 2.25])
    np.testing.assert_allclose(energy_tau, 0.8990469020108881, rtol=1e-14)
    np.testing.assert_allclose(stored_decay + dissipated, initial_energy)
    np.testing.assert_allclose(2.0 * stored_decay / tau, power)
    np.testing.assert_allclose(inductance * (-initial_current * decay / tau)
                               + resistance * initial_current * decay, 0.0,
                               atol=1e-14)
    # Compare sampled power's integral with the exact finite-time heat transfer.
    np.testing.assert_allclose(np.trapezoid(power, time), dissipated[-1],
                               rtol=3e-6)

    blue, orange, green, grey = "#176997", "#b85d24", "#287d59", "#65717b"
    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.5, 5.3), dpi=120,
                             facecolor="white")
    axes[0].plot(time, stored_charge, color=blue, lw=2.6,
                 label="Stored during buildup (starts at 0 A)")
    axes[0].plot(time, stored_decay, color=orange, lw=2.6,
                 label="Stored during decay (starts at 3 A)")
    axes[0].plot(time, dissipated, color=green, ls="--", lw=2.2,
                 label="Heat accumulated during decay")
    axes[0].axhline(initial_energy, color=grey, ls=":", lw=1.1)
    axes[0].axvline(tau, color=grey, ls="--", lw=1.1, zorder=0)
    axes[0].scatter([tau], [energy_tau], color=blue, zorder=5)
    axes[0].annotate("U(τ) = 0.899047 J", (tau, energy_tau),
                     xytext=(0.43, 0.68), color=blue, fontsize=10,
                     arrowprops={"arrowstyle": "->", "color": blue})
    axes[0].text(0.76, 2.31, "Total available: 2.25 J", color=grey, fontsize=10)
    axes[0].text(tau, -0.20, r"$\tau$", ha="center", color=grey,
                 transform=axes[0].get_xaxis_transform())
    axes[0].set(title="Two experiments, the same inductor", ylabel="Energy (J)",
                ylim=(0, 2.6))
    axes[0].legend(loc="center right", bbox_to_anchor=(1.03, 0.54),
                    frameon=False, fontsize=8.5)

    axes[1].plot(time, power, color=orange, lw=2.6,
                 label=r"$P_R=18e^{-2t/\tau}$ W")
    axes[1].fill_between(time, 0, power, color=orange, alpha=0.20)
    axes[1].text(0.43, 12.5, r"$\int_0^\infty P_R\,dt=2.25$ J", fontsize=13)
    axes[1].text(0.43, 9.8, "Stored energy becomes heat", fontsize=11, color=grey)
    axes[1].text(0.43, 7.7, "Energy / power time constant: 0.125 s",
                 fontsize=9.5, color=grey)
    axes[1].scatter([tau], [initial_power * np.exp(-2.0)],
                    color=orange, zorder=5)
    axes[1].annotate("P at τ: 2.43604 W", (tau, initial_power * np.exp(-2.0)),
                     xytext=(0.46, 4.0), fontsize=10, color=orange,
                     arrowprops={"arrowstyle": "->", "color": orange})
    axes[1].set(title="Release: closed R–L loop, no battery", ylabel="Heating power (W)",
                ylim=(0, 20))
    axes[1].legend(loc="upper right", frameon=False, fontsize=10)
    for ax in axes:
        ax.set(xlim=(0, 5 * tau), xlabel="Time since each experiment starts (s)")
        ax.set_xticks(np.arange(6) * tau)
        ax.grid(alpha=0.16)
    fig.suptitle("Inductor energy: R = 2 Ω, L = 0.5 H, buildup supply = 6 V",
                 fontsize=15)
    fig.text(0.5, 0.035,
             f"Shaded area through 1.25 s: {dissipated[-1]:.5f} J    •    "
             "Integral over all time: 2.25 J    •    Current keeps a closed path",
             ha="center", fontsize=10, color=grey)
    fig.subplots_adjust(left=0.07, right=0.98, top=0.82, bottom=0.22, wspace=0.28)
    fig.savefig(Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()