codex/spirometry-volume-flow-model.py

spirometry-volume-flow-model.py
#!/usr/bin/env python3
"""Constructed expiration models: identical final volume, different emptying rates.

Run in the desired output directory; uses the repository NumPy/Matplotlib environment.
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt


def main():
    t = np.linspace(0, 8, 801)
    fig, axes = plt.subplots(1, 2, figsize=(8, 3.5), dpi=120, facecolor="white")
    for tau, color in [(1.0, "#2367a1"), (2.0, "#b85224")]:
        volume = 4 * (1 - np.exp(-t / tau))
        flow = 4 / tau * np.exp(-t / tau)
        axes[0].plot(t, volume, color=color, label=rf"$\tau={tau:g}$ s")
        axes[1].plot(t, flow, color=color)
        v1 = 4 * (1 - np.exp(-1 / tau))
        axes[0].scatter([1], [v1], color=color, s=22, zorder=5)
        axes[0].annotate(f"{v1:.2f} L", (1, v1), xytext=(1.8, v1 - 0.05),
                         fontsize=9, color=color, arrowprops={"arrowstyle": "-", "color": color})
    axes[0].axhline(4, color="0.5", ls=":", lw=1)
    axes[0].axvline(1, color="0.55", ls="--", lw=1)
    axes[0].set(xlabel="Time (s)", ylabel="Expelled volume (L)", ylim=(0, 4.3))
    axes[0].legend(loc="lower right", fontsize=9)
    axes[1].set(xlabel="Time (s)", ylabel="Outward flow (L/s)", ylim=(0, 4.3))
    for ax in axes:
        ax.set_xlim(0, 8)
        ax.grid(alpha=0.18)
        ax.set_facecolor("white")
    fig.suptitle("Same final volume, different emptying rates", fontsize=13)
    fig.text(0.5, 0.015, "Constructed exponential curves; not patient traces", ha="center", fontsize=9)
    fig.tight_layout(rect=(0, 0.06, 1, 0.96))
    fig.savefig("spirometry-volume-flow-model.png", dpi=120, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()