codexcovalent-bond-energy-model.py
covalent-bond-energy-model.py
"""Plot an illustrative dimensionless Morse potential; write the PNG to cwd.
Build: make _media/covalent-bond-energy-model.png
Self-chosen a=2, r_e=D=1; not measured or fitted H2 data.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7 (system +dfsg1).
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def potential(x):
return (1.0 - np.exp(-2.0 * (np.asarray(x) - 1.0))) ** 2 - 1.0
def force(x):
q = np.exp(-2.0 * (np.asarray(x) - 1.0))
return 4.0 * (q * q - q)
def main():
x = np.linspace(0.5, 4.0, 1401)
xf = np.linspace(0.7, 4.0, 1321)
q = np.exp(-2.0 * (xf - 1.0))
repulsive = 4.0 * q * q
attractive = -4.0 * q
# Check the derivative numerically away from the equilibrium zero.
h = 1e-5
sample = np.array([0.5, 0.8, 1.0, 1.2, 1.5, 3.0])
assert np.allclose(force(sample), -(potential(sample + h) - potential(sample - h)) / (2 * h),
atol=1e-8)
curvature = (potential(1 + h) - 2 * potential(1) + potential(1 - h)) / h**2
assert np.isclose(curvature, 8, atol=2e-5)
assert potential(1) == -1 and force(1) == 0
assert np.isclose(x[np.argmin(potential(x))], 1)
assert np.all(force(sample[sample < 1]) > 0)
assert np.all(force(sample[sample > 1]) < 0)
assert np.allclose(repulsive + attractive, force(xf))
assert abs(potential(20)) < 1e-15
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white",
"axes.spines.top": False, "axes.spines.right": False})
fig, (energy_axis, force_axis) = plt.subplots(1, 2, figsize=(10, 4.7), dpi=120)
fig.subplots_adjust(left=0.08, right=0.98, bottom=0.24, top=0.78, wspace=0.28)
fig.suptitle("An illustrative bond: energy minimum and force balance", fontsize=15, y=0.96)
fig.text(0.5, 0.865, "Self-chosen a = 2, re = D = 1; not measured H2 data", ha="center", fontsize=11)
energy_axis.plot(x, potential(x), lw=2.5, color="#216b97")
energy_axis.axhline(0, color="#666666", lw=1, ls="--")
energy_axis.scatter([1], [-1], color="#28754f", zorder=3, s=45)
energy_axis.annotate("Stable minimum\nx = 1, V/D = −1", xy=(1, -1), xytext=(1.65, -0.92),
arrowprops={"arrowstyle": "->", "color": "#28754f"},
color="#28754f", fontsize=10)
energy_axis.text(2.4, 0.13, "Separated atoms: V → 0", fontsize=10)
energy_axis.annotate("Closer: repulsion", xy=(0.58, potential(0.58)), xytext=(1.2, 1.6),
arrowprops={"arrowstyle": "->"}, fontsize=10)
energy_axis.annotate("", xy=(3.7, 0), xytext=(3.7, -1),
arrowprops={"arrowstyle": "<->", "color": "#a44f25"})
energy_axis.text(3.79, -0.58, "D", color="#a44f25")
energy_axis.set(xlim=(0.5, 4), ylim=(-1.18, 2.35), xlabel=r"Separation $x=r/r_e$",
ylabel=r"Potential energy $u=V/D$")
force_axis.plot(xf, repulsive, color="#a44f25", lw=1.8, ls="--", label="Positive term")
force_axis.plot(xf, attractive, color="#75509b", lw=1.8, ls=":", label="Negative term")
force_axis.plot(xf, force(xf), color="#216b97", lw=2.5, label=r"Net: $f=-du/dx$")
force_axis.axhline(0, color="#666666", lw=1)
force_axis.axvline(1, color="#28754f", lw=1, ls=":")
force_axis.scatter([1, 1, 1], [4, -4, 0], color="#28754f", zorder=3, s=25)
force_axis.annotate("+4 − 4 = 0", xy=(1, 0), xytext=(1.55, 3.4),
arrowprops={"arrowstyle": "->", "color": "#28754f"},
color="#28754f", fontsize=11)
force_axis.legend(loc="upper right", frameon=False, fontsize=10)
force_axis.set(xlim=(0.7, 4), ylim=(-8.5, 14), xlabel=r"Separation $x=r/r_e$",
ylabel=r"Separating force $f=Fr_e/D$")
for axis in (energy_axis, force_axis):
axis.grid(alpha=0.15)
fig.text(0.5, 0.115, r"$u=(1-e^{-2(x-1)})^2-1$; $f=4e^{-4(x-1)}-4e^{-2(x-1)}$",
ha="center", fontsize=12)
fig.text(0.5, 0.035, "Force terms are algebraic model components, not measured microscopic forces.",
ha="center", fontsize=10)
fig.savefig(Path.cwd() / "covalent-bond-energy-model.png", transparent=False)
plt.close(fig)
print(f"u(0.5)={potential(0.5):.8f}; u(1)={potential(1):g}; "
f"u(1.5)={potential(1.5):.8f}; f(1)={force(1):g}; u''(1)≈{curvature:.6f}; "
"force derivative and sign checks passed.")
if __name__ == "__main__":
main()