codex/force-displacement-curve.py

force-displacement-curve.py
"""Illustrative force-displacement models; save PNG in the current directory.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
The two models are invented examples, not experimental observations.
"""

from pathlib import Path

import matplotlib

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


x = np.linspace(0.0, 0.05, 401)  # metres
linear = 200.0 * x
cubic = 80000.0 * x**3
linear_work = 200.0 * 0.05**2 / 2
cubic_work = 80000.0 * 0.05**4 / 4
assert np.isclose(linear[-1], 10.0)
assert np.isclose(cubic[-1], 10.0)
assert np.isclose(linear_work, 0.25)
assert np.isclose(cubic_work, 0.125)

plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
fig, axes = plt.subplots(1, 2, figsize=(9.6, 4.0), dpi=100, sharey=True)
fig.subplots_adjust(left=0.075, right=0.985, bottom=0.19, top=0.76, wspace=0.20)
fig.suptitle("Same final force and displacement, different work", y=0.96, fontsize=15)
for ax, values, title, label, color in [
    (axes[0], linear, "Linear model", "Work = 0.250 J", "#1766a0"),
    (axes[1], cubic, "Cubic model", "Work = 0.125 J", "#b04d13"),
]:
    ax.fill_between(x * 1000, values, color=color, alpha=0.17)
    ax.plot(x * 1000, values, color=color, linewidth=2.5)
    ax.scatter([50], [10], color=color, s=28, zorder=3)
    ax.set_title(title, fontsize=12)
    ax.text(0.05, 0.87, label, transform=ax.transAxes)
    ax.set(xlim=(0, 52), ylim=(0, 11.3), xlabel="Displacement x (mm)")
    ax.set_xticks([0, 10, 20, 30, 40, 50])
    ax.set_yticks([0, 2, 4, 6, 8, 10])
    ax.grid(alpha=0.18)
    ax.spines[["top", "right"]].set_visible(False)
axes[0].set_ylabel("Applied force F (N)")
fig.text(0.5, 0.045, "Illustrative models; shaded area is the work integral (x converted to metres).",
         ha="center", fontsize=10)
fig.savefig(Path("force-displacement-curve.png"), facecolor="white")
plt.close(fig)