A bent triatomic molecule: coupled bond-stretch and angle-bend vibrations#

HarmonicMolecule builds a small molecular-mechanics “force field” out of nothing but harmonic bond (HarmonicBond) and harmonic angle-bend (HarmonicAngle) terms – the classical, spring-and-hinge picture of molecular structure that founded molecular mechanics. A bent, water-like triatomic (one heavy vertex atom, two light outer atoms, equilibrium angle 104.5 degrees) is displaced away from its equilibrium geometry and left to vibrate under velocity-Verlet integration alone (no thermostat): both bond lengths and the bond angle oscillate indefinitely about their equilibrium values, and the total energy is conserved to within integration error.

import matplotlib.pyplot as plt
import numpy as np

from chemistrykit.md.systems.pair_potentials import HarmonicMolecule

theta0 = np.deg2rad(104.5)  # a water-like equilibrium bond angle
r0 = 1.0
m_heavy, m_light = 16.0, 1.0

# Equilibrium geometry: the vertex (heavy) atom at the origin, the two
# light atoms placed symmetrically about it.
positions = np.array(
    [
        [r0 * np.sin(theta0 / 2), r0 * np.cos(theta0 / 2), 0.0],  # light atom 0
        [0.0, 0.0, 0.0],  # heavy vertex atom 1
        [-r0 * np.sin(theta0 / 2), r0 * np.cos(theta0 / 2), 0.0],  # light atom 2
    ]
)
velocities = np.zeros_like(positions)
positions[0] += np.array([0.15, 0.05, 0.0])  # displace: stretch + open the angle

masses = [m_light, m_heavy, m_light]
bonds = [(0, 1, 300.0, r0), (1, 2, 300.0, r0)]
angles = [(0, 1, 2, 50.0, theta0)]
molecule = HarmonicMolecule(positions, velocities, masses, bonds=bonds, angles=angles)

dt, n_steps = 0.001, 4000
t = np.arange(n_steps + 1) * dt
bond01, bond12, angle_deg, total_energy = [], [], [], []


def _record():
    p = molecule.positions
    r01 = float(np.linalg.norm(p[0] - p[1]))
    r12 = float(np.linalg.norm(p[2] - p[1]))
    v1, v2 = p[0] - p[1], p[2] - p[1]
    theta = float(np.arccos(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))))
    bond01.append(r01)
    bond12.append(r12)
    angle_deg.append(np.degrees(theta))
    total_energy.append(molecule.kinetic_energy() + molecule.potential_energy())


_record()
for _ in range(n_steps):
    molecule.step(dt)
    _record()

fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
axes[0].plot(t, bond01, label="bond 0-1 (stretched)")
axes[0].plot(t, bond12, label="bond 1-2")
axes[0].axhline(r0, color="gray", linestyle=":", linewidth=0.8, label="equilibrium r0")
axes[0].set_xlabel("t")
axes[0].set_ylabel("bond length")
axes[0].set_title("Bond-stretch vibration")
axes[0].legend(fontsize=8)

axes[1].plot(t, angle_deg, color="darkorange")
axes[1].axhline(np.degrees(theta0), color="gray", linestyle=":", linewidth=0.8, label="equilibrium theta0")
axes[1].set_xlabel("t")
axes[1].set_ylabel("bond angle (degrees)")
axes[1].set_title("Angle-bend vibration")
axes[1].legend(fontsize=8)
fig.tight_layout()
Bond-stretch vibration, Angle-bend vibration

Both bond lengths and the bond angle oscillate indefinitely about their equilibrium values – coupled through the shared vertex atom – and the total energy stays constant to within the integrator’s usual small, bounded error, exactly the symplectic-integration behavior expected of velocity-Verlet applied to a conservative force field:

total_energy = np.asarray(total_energy)
drift = abs(total_energy.max() - total_energy.min()) / abs(total_energy[0])
print(f"Relative energy drift over the run: {drift:.2e}")

plt.show()
Relative energy drift over the run: 7.81e-05

Total running time of the script: (0 minutes 0.566 seconds)

Gallery generated by Sphinx-Gallery