codex/coupled-rl-voltage-pulse-and-energy.py

coupled-rl-voltage-pulse-and-energy.py
"""Closed-return coupled RL model: exactly specified currents and energy budget."""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

L = .1  # H, each self-inductance
M = .06  # H, positive mutual inductance in the stated current convention
R = 10.  # ohm, each loop resistance
V = 1.  # V, applied to loop 1 during the pulse
DURATION = .12  # s; the primary circuit stays closed when the drive becomes zero


def step_currents(time):
    t = np.maximum(np.asarray(time), 0.)
    plus = np.exp(-t / ((L + M) / R))
    minus = np.exp(-t / ((L - M) / R))
    return V / (2 * R) * np.array([2 - plus - minus, minus - plus])


def pulse_currents(time):
    return step_currents(time) - step_currents(np.asarray(time) - DURATION)


def energy_budget(time):
    t_on = np.clip(np.asarray(time), 0., DURATION)
    t_after = np.maximum(np.asarray(time) - DURATION, 0.)
    tau_p, tau_m = (L + M) / R, (L - M) / R
    supplied = V**2 / R * (t_on - .5 * tau_p * (-np.expm1(-t_on / tau_p))
                          - .5 * tau_m * (-np.expm1(-t_on / tau_m)))
    def integral_squared_step(t, tau):
        return t - 2 * tau * (-np.expm1(-t / tau)) + .5 * tau * (-np.expm1(-2 * t / tau))
    heat = V**2 / (2 * R) * (integral_squared_step(t_on, tau_p)
                             + integral_squared_step(t_on, tau_m))
    s_end = V / R * (-np.expm1(-DURATION / tau_p))
    d_end = V / R * (-np.expm1(-DURATION / tau_m))
    heat += R / 4 * (s_end**2 * tau_p * (-np.expm1(-2 * t_after / tau_p))
                    + d_end**2 * tau_m * (-np.expm1(-2 * t_after / tau_m)))
    i1, i2 = pulse_currents(time)
    stored = .5 * L * (i1**2 + i2**2) + M * i1 * i2
    return supplied, heat, stored


def main():
    t = np.linspace(-.01, .25, 4001)
    i1, i2 = pulse_currents(t)
    supplied, heat, stored = energy_budget(t)
    fig, axes = plt.subplots(2, 2, figsize=(10.5, 6.6), layout="constrained")
    a, b, c, d = axes.ravel()
    a.plot(t * 1000, np.where((t >= 0) & (t < DURATION), V, 0), color="#555555")
    a.set(title="Voltage pulse; primary return stays closed", xlabel="Time (ms)", ylabel="Drive (V)", ylim=(-.08, 1.15))
    b.plot(t * 1000, i1 * 1000, label="Primary current", color="#d55e00")
    b.plot(t * 1000, i2 * 1000, label="Secondary current", color="#0072b2")
    b.set(title="Receiver responds during changes", xlabel="Time (ms)", ylabel="Current (mA)")
    b.legend(fontsize=9)
    short = np.linspace(0, .045, 1000)
    secondary = step_currents(short)[1]
    tp, tm = (L + M) / R, (L - M) / R
    peak_time = tp * tm / (tp - tm) * np.log(tp / tm)
    c.plot(short * 1000, secondary * 1000, color="#0072b2")
    c.scatter([peak_time * 1000], [step_currents(peak_time)[1] * 1000], color="#333333", zorder=4)
    c.set(title="Step response has two decay times", xlabel="Time after voltage step (ms)", ylabel="Secondary current (mA)")
    c.text(.95, .12, r"$\tau_+=16$ ms, $\tau_-=4$ ms", transform=c.transAxes, ha="right", fontsize=10)
    d.plot(t * 1000, stored * 1000, label="Stored magnetic energy", color="#009e73", lw=2.7)
    d.plot(t * 1000, (supplied - heat) * 1000, "--", label="Source work minus heat", color="#cc79a7", lw=1.6)
    d.set(title="Independent energy expressions agree", xlabel="Time (ms)", ylabel="Energy (mJ)")
    d.legend(fontsize=9)
    for ax in axes.ravel():
        ax.grid(alpha=.2)
        ax.axhline(0, color="#555555", lw=.6)
    fig.suptitle(r"Chosen linear model: $L_1=L_2=0.1$ H, $M=0.06$ H, $R_1=R_2=10\,\Omega$", fontsize=13)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=140, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()