codex/bernoulli-reciprocal-trajectories.py

bernoulli-reciprocal-trajectories.py
"""Exact Bernoulli trajectories; Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7.

Run from _media; writes bernoulli-reciprocal-trajectories.png to the CWD.
The curves are analytic model solutions, not measurements.
"""
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, (ay, az) = plt.subplots(1, 2, figsize=(10.4, 4.5), dpi=120,
                                layout="constrained", facecolor="white")
    orange, blue = "#b85116", "#196f9b"
    xp = np.linspace(0, 1.999, 1200)
    xn = np.linspace(0, 3, 700)
    ay.plot(xp, np.exp(-xp)/(2-xp), color=orange, lw=2.6,
            label=r"$a=1/2:\ y=e^{-x}/(2-x)$")
    ay.plot(xn, -np.exp(-xn)/(1+xn), color=blue, lw=2.6,
            label=r"$a=-1:\ y=-e^{-x}/(1+x)$")
    ay.axhline(0, color="#555555", lw=1)
    ay.axvline(2, color="#777777", ls=":", lw=1.5)
    ay.scatter([0, 0, 1], [0.5, -1, np.exp(-1)],
               color=[orange, blue, orange], zorder=4)
    ay.annotate(r"minimum $(1,e^{-1})$", (1, np.exp(-1)),
                xytext=(0.2, 1.25), arrowprops={"arrowstyle": "->"})
    ay.annotate("blow-up at x = 2", (1.96, 3.4), xytext=(0.2, 3.2),
                arrowprops={"arrowstyle": "->"}, color=orange)
    ay.set(title=r"Nonlinear: $y'+y=e^x y^2$", xlabel="x", ylabel="y",
           xlim=(0, 3), ylim=(-1.15, 4))
    ay.legend(loc="upper right", bbox_to_anchor=(1, 0.55), fontsize=9,
              framealpha=1)

    xs = np.linspace(0, 2, 600)
    xe = np.linspace(2, 2.25, 120)
    xx = np.linspace(0, 2.25, 600)
    az.plot(xs, np.exp(xs)*(2-xs), color=orange, lw=2.6,
            label=r"$z=e^x(2-x)$")
    az.plot(xe, np.exp(xe)*(2-xe), color=orange, lw=2.6, ls="--",
            label="linear continuation only")
    az.plot(xx, -np.exp(xx)*(1+xx), color=blue, lw=2.6,
            label=r"$z=-e^x(1+x)$")
    az.axhline(0, color="#555555", lw=1)
    az.axvline(2, color="#777777", ls=":", lw=1.5)
    az.scatter([0, 0, 2], [2, -1, 0], color=[orange, blue, orange], zorder=4)
    az.annotate("z = 0: inverse undefined", (2, 0), xytext=(0.3, -10),
                arrowprops={"arrowstyle": "->"})
    az.set(title=r"Linearized exactly: $z'-z=-e^x$", xlabel="x",
           ylabel="z = 1/y", xlim=(0, 2.25), ylim=(-33, 5))
    az.legend(loc="lower left", fontsize=9, framealpha=1)
    for ax in (ay, az):
        ax.set_facecolor("white")
        ax.grid(alpha=0.15)
    fig.savefig(Path.cwd()/"bernoulli-reciprocal-trajectories.png",
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()