codex/rl-current-and-voltage.py

rl-current-and-voltage.py
"""Plot a series RL response and the positive terminal voltage drops.

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
    final_current = voltage / resistance
    time = np.linspace(0.0, 5.0 * tau, 1001)
    decay = np.exp(-time / tau)
    current = final_current * (1.0 - decay)
    current_rate = final_current * decay / tau
    resistor_drop = resistance * current
    inductor_drop = inductance * current_rate
    current_tau = final_current * (1.0 - np.exp(-1.0))
    drops_tau = [resistance * current_tau, voltage * np.exp(-1.0)]
    np.testing.assert_allclose(resistor_drop + inductor_drop, voltage)
    np.testing.assert_allclose([tau, final_current, current[0], inductor_drop[0]],
                               [0.25, 3.0, 0.0, 6.0])
    np.testing.assert_allclose([current_tau, *drops_tau],
                               [1.896361676485673, 3.792723352971346,
                                2.207276647028654], rtol=1e-14)
    # The analytic solution also satisfies the integrated inductor law.
    np.testing.assert_allclose(inductance * current,
                               voltage * tau * (1.0 - decay))

    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, current, color=blue, lw=2.6,
                 label=r"$I=3(1-e^{-t/\tau})$")
    axes[0].axhline(final_current, color=grey, ls=":", lw=1.2)
    axes[0].text(0.76, 3.08, "Final current: 3 A", color=grey)
    axes[0].scatter([tau], [current_tau], color=blue, zorder=5)
    axes[0].annotate("1.89636 A", (tau, current_tau), xytext=(0.49, 1.3),
                     color=blue, arrowprops={"arrowstyle": "->", "color": blue})
    axes[0].set(title="Current rises continuously", ylabel="Current I (A)",
                ylim=(0, 3.45))
    axes[0].legend(loc="lower right", frameon=False)

    axes[1].plot(time, resistor_drop, color=orange, lw=2.6,
                 label=r"$v_R=RI$")
    axes[1].plot(time, inductor_drop, color=green, lw=2.6,
                 label=r"$v_L=L\,dI/dt$")
    axes[1].axhline(voltage, color=grey, ls=":", lw=1.2,
                    label=r"$v_R+v_L=6$ V")
    for value, colour, label, label_position in [
        (drops_tau[0], orange, "3.79272 V", (0.50, 4.2)),
        (drops_tau[1], green, "2.20728 V", (0.50, 2.1)),
    ]:
        axes[1].scatter([tau], [value], color=colour, zorder=5)
        axes[1].annotate(label, (tau, value), xytext=label_position,
                         color=colour,
                         arrowprops={"arrowstyle": "->", "color": colour})
    axes[1].set(title="Terminal drops share the 6 V", ylabel="Terminal drop (V)",
                ylim=(0, 6.6))
    axes[1].legend(loc="center right", bbox_to_anchor=(1.0, 0.57),
                    frameon=False, fontsize=10)
    for ax in axes:
        ax.axvline(tau, color=grey, ls="--", lw=1.1, zorder=0)
        ax.text(tau, -0.20, r"$\tau$", transform=ax.get_xaxis_transform(),
                ha="center", color=grey)
        ax.set(xlim=(0, 5 * tau), xlabel="Time t (s)")
        ax.set_xticks(np.arange(6) * tau)
        ax.grid(alpha=0.16)
    fig.suptitle("Series RL response: R = 2 Ω, L = 0.5 H, V = 6 V", fontsize=15)
    fig.text(0.5, 0.03,
             "Initial current: 0 A    •    Time constant: 0.25 s    •    "
             "Positive current enters the positive-drop terminal",
             ha="center", fontsize=10, color=grey)
    fig.subplots_adjust(left=0.07, right=0.98, top=0.82, bottom=0.22, wspace=0.27)
    fig.savefig(Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()