codex/impulse-force-pulses.py

impulse-force-pulses.py
"""Draw three stipulated positive net-force pulses with equal impulse.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Write impulse-force-pulses.png to the current working directory.
"""

import matplotlib.pyplot as plt
import numpy as np


def main():
    triangle_t = np.linspace(0.0, 0.5, 501)
    triangle_f = 24.0 * (1.0 - np.abs(triangle_t - 0.25) / 0.25)
    impulses = np.array([12.0 * 0.5, np.trapezoid(triangle_f, triangle_t), 3.0 * 2.0])
    delta_v = impulses / 2.0
    np.testing.assert_allclose(impulses, [6.0, 6.0, 6.0])
    np.testing.assert_allclose(delta_v, [3.0, 3.0, 3.0])
    print("Pulse impulses (N s):", impulses.tolist())
    print("Velocity changes for 2 kg (m/s):", delta_v.tolist())

    plt.rcParams.update({"font.size": 12, "savefig.facecolor": "white"})
    fig, axes = plt.subplots(1, 3, figsize=(11.5, 5.6), facecolor="white",
                             sharex=True, sharey=True)
    fig.subplots_adjust(left=0.07, right=0.985, bottom=0.27,
                        top=0.73, wspace=0.17)
    fig.suptitle("Equal impulse does not mean equal peak force", fontsize=18, y=0.965)
    fig.text(0.5, 0.86,
             "Three rightward net-force pulses, each with area 6 N s; all axes use the same scale",
             ha="center", fontsize=12)
    pulses = [([0.0, 0.5], [12.0, 12.0], "#276b9b", "Short rectangle", "12 N × 0.5 s = 6 N s"),
              (triangle_t, triangle_f, "#cf7323", "Short triangle", "½ × 24 N × 0.5 s = 6 N s"),
              ([0.0, 2.0], [3.0, 3.0], "#387f54", "Long rectangle", "3 N × 2 s = 6 N s")]
    for ax, (t, force, color, title, area_label) in zip(axes, pulses, strict=True):
        ax.fill_between(t, 0, force, color=color, alpha=0.24)
        ax.plot(t, force, color=color, lw=2.8)
        ax.plot([t[0], t[0]], [0, force[0]], color=color, lw=2.8)
        ax.plot([t[-1], t[-1]], [force[-1], 0], color=color, lw=2.8)
        ax.plot([t[-1], 2.15], [0, 0], color=color, lw=2.8)
        peak_idx = int(np.argmax(force))
        peak_x, peak = t[peak_idx], force[peak_idx]
        ax.annotate(f"Peak {peak:g} N", xy=(peak_x, peak),
                    xytext=(0.95, max(peak + 3.0, 9.0)), fontsize=11,
                    ha="center", color=color,
                    arrowprops={"arrowstyle": "->", "color": color, "lw": 1.3})
        ax.set(title=title, xlabel="Time t (s)", xlim=(-0.04, 2.15), ylim=(0, 29),
               xticks=[0, 0.5, 1, 1.5, 2], yticks=[0, 6, 12, 18, 24])
        ax.text(0.5, -0.25, area_label, transform=ax.transAxes,
                ha="center", fontsize=11, color=color)
        ax.set_facecolor("white")
        ax.set_axisbelow(True)
        ax.grid(alpha=0.17)
        ax.spines[["top", "right"]].set_visible(False)
        ax.title.set_fontsize(13)
    axes[0].set_ylabel("Net force F (N)")
    fig.text(0.5, 0.105,
             r"Same fixed mass $m=2$ kg: $\Delta v=J/m=6/2=3$ m/s for every pulse",
             ha="center", fontsize=13)
    fig.text(0.5, 0.04,
             "These are stipulated examples. Pulse shapes affect the motion during the interval, even when the final change is equal.",
             ha="center", fontsize=10)
    fig.savefig("impulse-force-pulses.png", dpi=120, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()