codex/hyperbolic-cosine-catenary.py

hyperbolic-cosine-catenary.py
"""Catenary examples, vertical shift and scale; PNG goes to CWD.

Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
"""

from pathlib import Path

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


def main():
    x = np.linspace(-1.65, 1.65, 501)
    blue = "#176b9c"
    fig, axes = plt.subplots(1, 2, figsize=(10.8, 4.9), dpi=120)
    fig.patch.set_facecolor("white")
    ax = axes[0]
    ax.plot(x, np.cosh(x), color=blue, lw=2.6, label=r"$y=\cosh x$")
    nodes = np.array([-1., 0., 1.])
    ax.scatter(nodes, np.cosh(nodes), color=blue, s=45, zorder=4)
    ax.scatter([-1, 1], [np.cosh(1)]*2, marker="s", color="#444444",
               s=85, zorder=3, label="Supports at x = ±1")
    for value in nodes:
        text = "(0, 1)" if value == 0 else f"({int(value)}, 1.543)"
        offset = (0, -24) if value == 0 else (0, 13)
        ax.annotate(text, (value, np.cosh(value)), xytext=offset,
                    textcoords="offset points", ha="center", fontsize=11)
    ax.axhline(1, color="0.65", ls=":", lw=1)
    ax.text(-1.58, .18, r"$L_{[-1,1]}=2\sinh1\approx2.350$", fontsize=11)
    ax.set(xlim=(-1.7, 1.7), ylim=(0, 2.95), xlabel="Horizontal position x",
           ylabel="Height y", title="a = 1: lowest point at height 1")
    ax.legend(loc="upper center", fontsize=10, framealpha=1)

    ax = axes[1]
    for a, color in [(.5, "#bf5a15"), (1, blue), (2, "#33844a")]:
        ax.plot(x, a*(np.cosh(x/a)-1), color=color, lw=2.4,
                label=f"a = {a:g}")
    ax.plot(x, x*x/2, color="0.4", ls="--", lw=1.5,
            label=r"Parabola $x^2/2$ (a = 1 locally)")
    ax.scatter(nodes, np.cosh(nodes)-1, color=blue, s=35, zorder=4)
    ax.annotate("(0, 0)", (0, 0), xytext=(15, 12),
                textcoords="offset points", fontsize=11)
    ax.set(xlim=(-1.7, 1.7), ylim=(-.12, 3.0), xlabel="Horizontal position x",
           ylabel="Height above the lowest point",
           title=r"Shifted: $y=a[\cosh(x/a)-1]$")
    ax.legend(loc="upper center", fontsize=9.5, framealpha=1)
    for ax in axes:
        ax.set_facecolor("white")
        ax.axhline(0, color="0.5", lw=.8, zorder=0)
        ax.axvline(0, color="0.5", lw=.8, zorder=0)
        ax.grid(alpha=.17)
        ax.set_xticks([-1, 0, 1])
        ax.spines[["top", "right"]].set_visible(False)
    fig.suptitle("A catenary is a scaled hyperbolic cosine", fontsize=15)
    fig.text(.5, .035, "Uniform flexible, inextensible chain under its own weight.  "
             "Larger a = H/(μg) gives less sag over a fixed horizontal span.",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=.075, right=.98, bottom=.17, top=.83, wspace=.29)
    fig.savefig(Path.cwd() / "hyperbolic-cosine-catenary.png",
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()