chemistrykit.md#
chemistrykit.md: molecular dynamics and force fields.
Lennard-Jones fluid simulation in reduced units (energy, pressure, radial
distribution function g(r)); pairwise potentials (Morse, Buckingham,
harmonic bond/angle terms); SHAKE bond constraints; velocity-rescaling,
Berendsen, Nose-Hoover, and stochastic velocity-rescaling thermostats;
Einstein and Green-Kubo self-diffusion coefficients; periodic boundary conditions with the minimum-image
convention and a Verlet neighbor list. Integrated via
chemistrykit.integrators’s velocity-Verlet
(chemistrykit.integrators.velocity_verlet_step()) – not a
reimplementation, see
chemistrykit.md.core.base_system.MolecularDynamicsSystem.step().
MD’s trajectories are a natural cross-check for
chemistrykit.statmech’s distribution functions: the speed
distribution of an equilibrated LJFluid
should follow chemistrykit.statmech.MaxwellBoltzmannSpeedDistribution
at the simulation’s instantaneous temperature (see
examples/md/lj_fluid/plot_02_maxwell_boltzmann_check.py).
- class chemistrykit.md.AnglePotential[source]#
Bases:
ABCCommon interface for a three-body bond-angle potential \(U(\theta)\).
Used for angle-bending terms in
HarmonicMolecule, where \(\theta\) is the angle at a vertex atom j between the two bonds i-j and k-j.
- class chemistrykit.md.BerendsenThermostat(target_temperature, tau, k_b=1.0)[source]#
Bases:
objectThe Berendsen weak-coupling thermostat.
Rescales every velocity by a common factor after each step so that the instantaneous kinetic temperature relaxes exponentially toward target_temperature with time constant \(\tau\) – H. J. C. Berendsen, J. P. M. Postma, W. F. van Gunsteren, A. DiNola & J. R. Haak, J. Chem. Phys. 81, 3684 (1984), eq. 11:
\[\lambda^2 = 1 + \frac{\Delta t}{\tau}\left(\frac{T_0}{T} - 1\right), \qquad \frac{dT}{dt} = \frac{T_0 - T}{\tau}.\]For \(\tau = \Delta t\) this reduces to the immediate, complete rescaling of
VelocityRescalingThermostat. Like that scheme it suppresses canonical kinetic-energy fluctuations, so it does not sample the NVT ensemble exactly (seeStochasticVelocityRescalingThermostat).- Parameters:
Examples
With no forces acting, each call moves the temperature a fraction \(\Delta t/\tau\) of the way to the target:
>>> from chemistrykit.md.systems.lj_fluid import LJFluid >>> fluid = LJFluid.from_lattice(n_per_side=3, cutoff=1.5, density=0.6, temperature=2.0, rng=0) >>> thermostat = BerendsenThermostat(target_temperature=1.0, tau=0.1) >>> thermostat.apply(fluid, dt=0.01) >>> round(fluid.temperature(), 10) # 2.0 + 0.1 * (1.0 - 2.0) 1.9
- class chemistrykit.md.Buckingham(A, B, C)[source]#
Bases:
PairPotentialThe Buckingham (exp-6) pair potential.
\[U(r) = A e^{-Br} - \frac{C}{r^6}\]An alternative to Lennard-Jones for the short-range repulsion, replacing the \(r^{-12}\) term with an exponential (closer to the true quantum-mechanical electron-overlap repulsion) at the cost of an unphysical turnover to \(U\to-\infty\) as \(r\to0\) that pure LJ does not have – R. A. Buckingham, Proc. R. Soc. A 168, 264 (1938); used extensively for rare-gas potentials and ionic-crystal lattice energies (see the future
chemistrykit.crystal).- Parameters:
Examples
At large separation the potential is dominated by the (attractive, slower-decaying) dispersion term, so both the energy and the force are negative:
>>> pot = Buckingham(A=1.0e4, B=3.0, C=1.0) >>> bool(pot.energy(10.0) < 0.0) True >>> bool(pot.force_scalar(10.0) < 0.0) True
- force_scalar(r)[source]#
Return the radial force magnitude \(f(r) = -dU/dr\).
By convention, positive is repulsive (pushes the two particles apart along their separation vector \(\vec r_i - \vec r_j\)) and negative is attractive – so the force on particle i due to particle j is
f(r) * (r_i - r_j) / r.
- class chemistrykit.md.DiatomicOscillator(potential, m1, m2, r0, v_rel0=0.0, axis=(1.0, 0.0, 0.0))[source]#
Bases:
MolecularDynamicsSystemTwo atoms bound by a radial potential: classical bond-vibration MD.
A textbook two-body problem: with no external forces, the center-of-mass moves uniformly and the relative coordinate oscillates as if a single particle of the reduced mass \(\mu=m_1m_2/(m_1+m_2)\) were moving in the bond potential itself. For a
HarmonicBond, this gives the exact classical vibration period \(T=2\pi\sqrt{\mu/k}\) (seeharmonic_period()), which the simulated trajectory is checked against in the test suite.- Parameters:
- degrees_of_freedom()[source]#
A single relative-coordinate (bond-stretch) vibrational degree of freedom.
- Return type:
- forces_and_potential(positions)[source]#
Return the force on every particle and the total potential energy.
- Parameters:
positions (ndarray, shape (n_particles, n_dim))
- Returns:
forces (ndarray, shape (n_particles, n_dim))
potential_energy (float)
- class chemistrykit.md.HarmonicAngle(k_theta, theta0)[source]#
Bases:
AnglePotentialA harmonic bond-angle-bending potential.
\[U(\theta) = \frac{1}{2}k_\theta(\theta-\theta_0)^2\]- Parameters:
Examples
>>> angle = HarmonicAngle(k_theta=100.0, theta0=1.9106) # ~109.47 deg, tetrahedral >>> float(angle.energy(angle.theta0)) 0.0 >>> float(angle.torque(angle.theta0)) -0.0
- class chemistrykit.md.HarmonicBond(k, r0)[source]#
Bases:
PairPotentialA harmonic (Hookean) bond-stretch potential.
\[U(r) = \frac{1}{2}k(r-r_0)^2\]The simplest bonded term in a classical force field – the small -oscillation limit of any real bond potential, including
Morse(Atkins & de Paula, Physical Chemistry, 11th ed., Ch. 8, the classical harmonic oscillator).Examples
>>> bond = HarmonicBond(k=50.0, r0=1.0) >>> float(bond.energy(1.0)) 0.0 >>> round(float(bond.force_scalar(1.1)), 10) -5.0
- force_scalar(r)[source]#
Return the radial force magnitude \(f(r) = -dU/dr\).
By convention, positive is repulsive (pushes the two particles apart along their separation vector \(\vec r_i - \vec r_j\)) and negative is attractive – so the force on particle i due to particle j is
f(r) * (r_i - r_j) / r.
- class chemistrykit.md.HarmonicMolecule(positions, velocities, masses, bonds=(), angles=())[source]#
Bases:
MolecularDynamicsSystemA small, non-periodic cluster of atoms held together by harmonic bonds and angles.
A minimal molecular “force field”: mirrors
chemistrykit.kinetics.systems.networks.StoichiometricNetwork’s structural-array-and-factory-closure pattern, but for bonded-force evaluation instead of mass-action reaction rates. The angle-bending force is derived from the chain rule \(\vec F_i=-dU/d\theta\cdot d\theta/d\vec r_i\) applied to \(\theta=\arccos(\hat v_1\cdot\hat v_2)\), a standard force-field result (e.g. Leach, Molecular Modelling: Principles and Applications, 2nd ed., Ch. 4).- Parameters:
positions (array-like, shape (n_atoms, 3))
velocities (array-like, shape (n_atoms, 3))
masses (array-like, shape (n_atoms,))
bonds (
Sequence) – Harmonic bonds between atom indices i, j with force constant k and equilibrium length r0.angles (
Sequence) – Harmonic angle terms at vertex atom j (between bonds i-j and k-j) with force constant k_theta and equilibrium angle theta0 (radians).
- class chemistrykit.md.LJFluid(positions, velocities, box_length, epsilon=1.0, sigma=1.0, mass=1.0, cutoff=2.5, skin=0.3, rebuild_every=20)[source]#
Bases:
MolecularDynamicsSystemA monatomic Lennard-Jones fluid in a cubic periodic box, in reduced units.
Positions evolve under
chemistrykit.integrators.velocity_verlet_step()driven by a@njitpairwise-force loop (_make_lj_accel_njit()) that reads its interacting pairs from a periodically-rebuiltVerletNeighborList, under the minimum-image convention (chemistrykit.md.utils.pbc.minimum_image_displacement()). See the module docstring for the reduced-units convention.- Parameters:
positions (array-like, shape (N, 3))
velocities (array-like, shape (N, 3))
box_length (
float) – Cubic periodic box side length; must be at least2 * cutofffor the minimum-image convention to be unambiguous.epsilon (
float) – Lennard-Jones/particle parameters (all particles identical).sigma (
float) – Lennard-Jones/particle parameters (all particles identical).mass (
float) – Lennard-Jones/particle parameters (all particles identical).cutoff (
float) – Interaction cutoff, in units of sigma (2.5 is the conventional standard truncation for a liquid-density LJ fluid; Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., Ch. 1.4).skin (
float) – Neighbor-list skin distance, seeVerletNeighborList.rebuild_every (
int) – Rebuild the neighbor list after this manystep()calls.
- forces_and_potential(positions)[source]#
Exact (full, non-skin-list) forces, energy, and (cached) virial.
Unlike
step()’s force evaluation (which reuses a skin-enlarged neighbor list for performance), this always rebuilds an exact-cutoff pair list from the given positions, so energy, pressure, and g(r) queries are never affected by neighbor-list staleness.- Parameters:
positions (ndarray, shape (N, 3))
- Returns:
forces (ndarray, shape (N, 3))
potential_energy (float)
- classmethod from_lattice(n_per_side, density, temperature, epsilon=1.0, sigma=1.0, mass=1.0, cutoff=2.5, rng=None, **kwargs)[source]#
Build an
LJFluidon a simple-cubic lattice with Maxwell-Boltzmann velocities.A standard way to initialize an MD run: a regular lattice avoids any risk of initial particle overlap (which would otherwise blow up the steeply repulsive \(r^{-12}\) term), and velocity components drawn independently from a Gaussian with variance \(k_BT/m\) reproduce the Maxwell-Boltzmann distribution exactly (see
chemistrykit.statmech.MaxwellBoltzmannSpeedDistribution); the total momentum is then zeroed and the velocities rescaled so the realized instantaneous temperature matches temperature exactly, rather than only on average.- Parameters:
n_per_side (
int) – Particles per lattice edge; total particle count isn_per_side**3.density (
float) – Number density \(\rho=N/V\), in units of \(\sigma^{-3}\).temperature (
float) – Target temperature, in units of \(\epsilon/k_B\).epsilon (
float)sigma (
float)mass (
float)cutoff (
float)rng (int, numpy.random.Generator, or None) – Seed or generator for the initial velocities.
**kwargs – Forwarded to the
LJFluidconstructor (e.g.skin,rebuild_every).
- Return type:
- Returns:
LJFluid
Examples
>>> fluid = LJFluid.from_lattice(n_per_side=4, density=0.5, temperature=1.0, rng=0) >>> fluid.positions.shape (64, 3) >>> round(fluid.temperature(), 6) 1.0
- pressure(k_b=1.0)[source]#
Instantaneous pressure via the virial theorem.
\[P = \frac{N k_B T}{V} + \frac{W}{dV}\](Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., eq. 2.60, generalized from \(d=3\) to general dimension.)
Examples
A very dilute LJ gas (large box, few particles) has pressure close to the ideal-gas value \(P=\rho k_B T\), since the virial correction from interactions vanishes as the density does:
>>> import numpy as np >>> rng = np.random.default_rng(0) >>> n, L = 20, 40.0 >>> positions = rng.uniform(0.0, L, size=(n, 3)) >>> velocities = rng.normal(0.0, 1.0, size=(n, 3)) >>> velocities -= velocities.mean(axis=0) >>> fluid = LJFluid(positions, velocities, box_length=L, cutoff=2.5) >>> fluid.virial() # no pairs within cutoff at this density 0.0 >>> rho = n / L**3 >>> ideal_pressure = rho * fluid.temperature() >>> bool(abs(fluid.pressure() - ideal_pressure) < 1e-12) True
- radial_distribution_function(r_max=None, n_bins=100, positions=None)[source]#
Compute the radial distribution function g(r) for a configuration.
\[g(r) = \frac{2\,\langle n_{\text{pairs}}(r, r+dr)\rangle}{N\,\rho\,4\pi r^2 dr}\]the (3D) local density of particles at distance r from a reference particle, relative to the bulk density \(\rho\) (Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., Ch. 2.6; Frenkel & Smit, Understanding Molecular Simulation, 2nd ed., Ch. 3.6). \(g(r) \to 1\) for an ideal (non-interacting, spatially uniform) gas, and develops structure (a first peak near the LJ minimum, decaying oscillations beyond it) in a liquid or solid.
- Parameters:
- Returns:
r (ndarray, shape (n_bins,)) – Bin-center separations.
g (ndarray, shape (n_bins,))
- step(dt)[source]#
Advance the stored state by one velocity-Verlet step, in place.
Delegates the actual kick-drift-kick arithmetic to the shared
chemistrykit.integrators.velocity_verlet_step()– this method exists only to supply that function with this system’s_accel_njit/params, exactly aschemistrykit.kinetics.core.base_system.ReactionNetwork.integrate()wrapschemistrykit.integrators.rk4_integrate().
- class chemistrykit.md.LennardJones(epsilon=1.0, sigma=1.0)[source]#
Bases:
PairPotentialThe 12-6 Lennard-Jones pair potential.
\[U(r) = 4\epsilon\left[\left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^{6}\right]\]The \(r^{-12}\) term models short-range Pauli repulsion (steeply, for computational convenience – it has no deeper physical justification), and the \(r^{-6}\) term is the genuine London-dispersion attraction (J. E. Lennard-Jones, Proc. R. Soc. A 106, 463 (1924); Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., Ch. 1.4).
Examples
The potential has its minimum exactly at \(r=2^{1/6}\sigma\) (
r_min), where \(U=-\epsilon\) and the force vanishes – the mechanical-equilibrium separation of an isolated pair:>>> lj = LennardJones(epsilon=1.0, sigma=1.0) >>> round(float(lj.energy(lj.r_min)), 10) -1.0 >>> round(float(lj.force_scalar(lj.r_min)), 10) -0.0
At
r=sigmathe potential is exactly zero by construction:>>> round(float(lj.energy(1.0)), 10) 0.0
- force_scalar(r)[source]#
Return the radial force magnitude \(f(r) = -dU/dr\).
By convention, positive is repulsive (pushes the two particles apart along their separation vector \(\vec r_i - \vec r_j\)) and negative is attractive – so the force on particle i due to particle j is
f(r) * (r_i - r_j) / r.
- class chemistrykit.md.MDResult(t, positions, velocities, kinetic_energy, potential_energy, temperature, box_length=None, extra=<factory>)[source]#
Bases:
objectContainer for the output of a
MolecularDynamicsSystem.run()call.Mirrors
chemistrykit.kinetics.core.base_system.KineticsResult(a stable, dataclass return type consumed by visualizers), specialized to a particle trajectory instead of a concentration one.- Parameters:
- box_length: float | None = None#
Cubic periodic box side length, or
Nonefor a non-periodic system.- Type:
float or None
- extra: dict#
Free-form slot for any additional diagnostics a system chooses to attach (e.g. pressure).
- Type:
- positions: ndarray#
Particle positions at each sampled frame.
- Type:
ndarray, shape (n_frames, n_particles, n_dim)
- speeds()[source]#
Return per-particle speeds at every sampled frame.
- Return type:
- Returns:
ndarray, shape (n_frames, n_particles)
Examples
>>> import numpy as np >>> result = MDResult( ... t=np.array([0.0]), ... positions=np.zeros((1, 2, 3)), ... velocities=np.array([[[3.0, 4.0, 0.0], [0.0, 0.0, 0.0]]]), ... kinetic_energy=np.array([0.0]), ... potential_energy=np.array([0.0]), ... temperature=np.array([0.0]), ... ) >>> result.speeds() array([[5., 0.]])
- temperature: ndarray#
Instantaneous kinetic temperature (see
MolecularDynamicsSystem.temperature()) at each frame.- Type:
ndarray, shape (n_frames,)
- class chemistrykit.md.MolecularDynamicsSystem(positions, velocities, masses)[source]#
Bases:
ABCCommon engine for a system of particles integrated by velocity-Verlet.
Concrete subclasses must set, in
__init__(before callingsuper().__init__(...)):self._accel_njit: an@njitdispatcher with signature(positions, t, params) -> acceleration(thechemistrykit.integrators.RHSFuncconvention, specialized so the “state” is a position array and the return value is acceleration, i.e. force already divided by mass – exactly the conventionchemistrykit.integrators.velocity_verlet_step()expects).self.params: ndarray – the parameter vector read byself._accel_njit(np.empty(0)if every numeric parameter is instead baked into the njit closure itself).self.box_length: float orNone– cubic periodic box side length, for systems that use periodic boundary conditions (Nonefor a finite, non-periodic cluster).
and call
super().__init__(positions, velocities, masses).- Parameters:
positions (array-like, shape (n_particles, n_dim))
velocities (array-like, shape (n_particles, n_dim))
masses (array-like, shape (n_particles,))
- degrees_of_freedom()[source]#
Number of kinetic degrees of freedom, assuming zero net momentum.
n_dim * n_particles - n_dim: total Cartesian degrees of freedom minus the n_dim removed by fixing the center-of-mass velocity – standard practice whenever the total momentum is conserved/zeroed at initialization (Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., Ch. 2.6). Subclasses with different constraints (e.g. a fixed number of internal vibrational modes) should override this.- Return type:
- Returns:
int
- abstractmethod forces_and_potential(positions)[source]#
Return the force on every particle and the total potential energy.
- Parameters:
positions (ndarray, shape (n_particles, n_dim))
- Returns:
forces (ndarray, shape (n_particles, n_dim))
potential_energy (float)
- kinetic_energy()[source]#
Total kinetic energy, \(K = \sum_i \frac{1}{2} m_i v_i^2\).
- Return type:
- Returns:
float
- potential_energy()[source]#
Total potential energy at the system’s current configuration.
- Return type:
- Returns:
float
- run(dt, n_steps, thermostat=None, sample_every=1)[source]#
Integrate forward n_steps velocity-Verlet steps, sampling as it goes.
- Parameters:
dt (
float) – Time step.n_steps (
int) – Number of integration steps (must be a multiple of sample_every).thermostat (object, optional) – An object with an
apply(system, dt)method, called once after every step (e.g.VelocityRescalingThermostat,NoseHooverThermostat). Omit for a microcanonical (NVE) run.sample_every (
int) – Record a frame every this many steps (plus the initial state).
- Return type:
- Returns:
MDResult
- step(dt)[source]#
Advance the stored state by one velocity-Verlet step, in place.
Delegates the actual kick-drift-kick arithmetic to the shared
chemistrykit.integrators.velocity_verlet_step()– this method exists only to supply that function with this system’s_accel_njit/params, exactly aschemistrykit.kinetics.core.base_system.ReactionNetwork.integrate()wrapschemistrykit.integrators.rk4_integrate().
- class chemistrykit.md.Morse(De, a, re)[source]#
Bases:
PairPotentialThe Morse interatomic potential.
\[U(r) = D_e\left[1 - e^{-a(r-r_e)}\right]^2 - D_e\]A three-parameter anharmonic bond potential: dissociation energy \(D_e\), equilibrium separation \(r_e\), and a width parameter a controlling the curvature at the minimum (related to the harmonic force constant there by \(k = 2D_ea^2\), see
force_constant) – P. M. Morse, Phys. Rev. 34, 57 (1929); Atkins & de Paula, Physical Chemistry, 11th ed., Ch. 12 (used there for vibrational anharmonicity; see alsoDiatomicOscillator, whose classical vibration this potential drives).- Parameters:
Examples
Exactly reproduces the well depth and zero force at the minimum:
>>> morse = Morse(De=4.0, a=1.5, re=1.0) >>> round(float(morse.energy(morse.re)), 10) -4.0 >>> round(float(morse.force_scalar(morse.re)), 10) 0.0
And the dissociation limit \(U(r\to\infty) \to 0\):
>>> round(float(morse.energy(20.0)), 6) -0.0
- force_scalar(r)[source]#
Return the radial force magnitude \(f(r) = -dU/dr\).
By convention, positive is repulsive (pushes the two particles apart along their separation vector \(\vec r_i - \vec r_j\)) and negative is attractive – so the force on particle i due to particle j is
f(r) * (r_i - r_j) / r.
- classmethod from_force_constant(De, k, re)[source]#
Build a
Morsepotential from \(D_e\), a harmonic force constant k, and re.Inverts \(k=2D_ea^2\) for \(a=\sqrt{k/(2D_e)}\), so the curvature at the minimum matches a given harmonic bond exactly while still capturing anharmonicity/dissociation away from it.
Examples
>>> morse = Morse.from_force_constant(De=4.0, k=10.0, re=1.0) >>> round(morse.force_constant, 6) 10.0
- class chemistrykit.md.NoseHooverThermostat(target_temperature, Q, dof=None, k_b=1.0)[source]#
Bases:
objectThe Nose-Hoover extended-system thermostat.
Couples the physical system to a fictitious “friction” degree of freedom \(\xi\) whose equation of motion drives the time-averaged kinetic temperature to target_temperature while (in the exact, continuous-time extended Lagrangian) still generating genuine canonical-ensemble fluctuations – S. Nose, J. Chem. Phys. 81, 511 (1984); W. G. Hoover, Phys. Rev. A 31, 1695 (1985).
\[\dot{\vec v}_i = \frac{\vec F_i}{m_i} - \xi \vec v_i, \qquad Q\dot\xi = \left(\sum_i m_i v_i^2\right) - \text{dof}\cdot k_BT\]This implementation integrates the friction/velocity-scaling part with the explicit, first-order update
\[\xi \mathrel{+}= \frac{2K - \text{dof}\cdot k_BT}{Q}\,dt, \qquad \vec v_i \mathrel{*}= e^{-\xi\,dt}\]applied once after each ordinary velocity-Verlet step – an approximation to the reference operator-split (“Trotter factorization”) integrator of Martyna, Tuckerman, Tobias & Klein, Mol. Phys. 87, 1117 (1996) (see also Frenkel & Smit, Understanding Molecular Simulation, 2nd ed., Ch. 6.1.2, Algorithm 15), which instead splits the thermostat update into two half-steps symmetrically wrapped around the position/velocity update for improved (time-reversible) long-time behavior. The simplified, single-step coupling used here still relaxes the time-averaged kinetic temperature to target_temperature, but without that scheme’s more favorable long-time energy-conservation properties – flagged explicitly here as the approximation it is.
- Parameters:
target_temperature (
float) – Must be positive.Q (
float) – Thermostat “mass” (coupling strength); a larger Q couples more weakly, giving a slower thermostat response. Must be positive.dof (
int) – Degrees of freedom used for the target kinetic energy \(\text{dof}\cdot k_BT/2\); defaults to the system’s owndegrees_of_freedom()at the first call toapply().k_b (
float)
Examples
>>> import numpy as np >>> from chemistrykit.md.systems.lj_fluid import LJFluid >>> fluid = LJFluid.from_lattice(n_per_side=3, cutoff=1.5, density=0.6, temperature=2.0, rng=0) >>> fluid.velocities *= 2.0 # perturb away from the target temperature >>> thermostat = NoseHooverThermostat(target_temperature=2.0, Q=10.0) >>> result = fluid.run(dt=0.001, n_steps=200, thermostat=thermostat, sample_every=20) >>> bool(abs(result.temperature[-1] - 2.0) < abs(result.temperature[0] - 2.0)) True
- class chemistrykit.md.PairPotential[source]#
Bases:
ABCCommon interface for a radially symmetric two-body potential U(r).
Concrete subclasses (
LennardJones,Morse,Buckingham,HarmonicBond) implementenergy()andforce_scalar()so they can be swapped in and compared directly, mirroring the rolechemistrykit.thermo.core.base_system.EquationOfStateplays for equations of state.- abstractmethod force_scalar(r)[source]#
Return the radial force magnitude \(f(r) = -dU/dr\).
By convention, positive is repulsive (pushes the two particles apart along their separation vector \(\vec r_i - \vec r_j\)) and negative is attractive – so the force on particle i due to particle j is
f(r) * (r_i - r_j) / r.
- class chemistrykit.md.ShakeMolecule(positions, velocities, masses, constraints, bonds=(), angles=(), tol=1e-10)[source]#
Bases:
HarmonicMoleculeA
HarmonicMoleculewith rigid (SHAKE) bonds.Each velocity-Verlet step makes an unconstrained half-kick and drift, corrects the positions with
shake(), recovers the half-step velocities from the constrained displacement, completes the second half-kick, and finally removes each constrained bond’s relative velocity component (RATTLE’s velocity stage).- Parameters:
positions (array-like, shape (n_atoms, 3))
velocities (array-like, shape (n_atoms, 3))
masses (array-like, shape (n_atoms,))
constraints (
Sequence) – Rigid bonds; positions should already satisfy them.bonds (
Sequence) – Flexible harmonic terms, as forHarmonicMolecule.angles (
Sequence) – Flexible harmonic terms, as forHarmonicMolecule.tol (
float)
Examples
A rigid rotating diatomic keeps its bond length exactly:
>>> import numpy as np >>> pos = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) >>> vel = np.array([[0.0, -1.0, 0.0], [0.0, 1.0, 0.0]]) >>> rotor = ShakeMolecule(pos, vel, [1.0, 1.0], constraints=[(0, 1, 1.0)]) >>> for _ in range(500): ... rotor.step(0.01) >>> round(float(np.linalg.norm(rotor.positions[0] - rotor.positions[1])), 8) 1.0
- bond_lengths()[source]#
Current length of every constrained bond.
- Return type:
- Returns:
ndarray, shape (n_constraints,)
- class chemistrykit.md.StochasticVelocityRescalingThermostat(target_temperature, tau, rng=None, dof=None, k_b=1.0)[source]#
Bases:
objectThe Bussi-Donadio-Parrinello stochastic velocity-rescaling thermostat.
Berendsen’s exponential relaxation of the kinetic energy \(K\) plus a correctly scaled stochastic term, chosen so that the kinetic energy samples its exact canonical distribution – G. Bussi, D. Donadio & M. Parrinello, J. Chem. Phys. 126, 014101 (2007), eq. 7 and Appendix (eq. A7):
\[\alpha^2 = c + (1-c)\frac{\bar K}{N_f K}\sum_{i=1}^{N_f} R_i^2 + 2 R_1 \sqrt{c(1-c)\frac{\bar K}{N_f K}}, \qquad c = e^{-\Delta t/\tau},\]where \(\bar K = N_f k_B T_0/2\), the \(R_i\) are independent standard normal numbers (the sum over \(i \ge 2\) is drawn as a single \(\chi^2_{N_f-1}\) variate), and every velocity is scaled by \(\alpha\). The stationary distribution of \(K\) is the canonical gamma distribution with mean \(\bar K\) and variance \(2\bar K^2/N_f\).
- Parameters:
Examples
>>> from chemistrykit.md.systems.lj_fluid import LJFluid >>> fluid = LJFluid.from_lattice(n_per_side=3, cutoff=1.5, density=0.6, temperature=3.0, rng=0) >>> thermostat = StochasticVelocityRescalingThermostat(target_temperature=1.0, tau=0.01, rng=0) >>> for _ in range(200): ... thermostat.apply(fluid, dt=0.01) >>> bool(0.5 < fluid.temperature() < 1.5) True
- class chemistrykit.md.VelocityRescalingThermostat(target_temperature, interval=1, k_b=1.0)[source]#
Bases:
objectSimple (deterministic) velocity-rescaling thermostat.
Rescales every particle’s velocity by a common factor \(\lambda=\sqrt{T_{\text{target}}/T_{\text{current}}}\) every interval MD steps, so the instantaneous kinetic temperature matches target_temperature exactly at each rescaling (Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., Ch. 2.3; Frenkel & Smit, Understanding Molecular Simulation, 2nd ed., Ch. 6.1.1).
This is the crude, deterministic member of the “velocity rescaling” thermostat family: it does not itself sample the canonical (NVT) ensemble correctly (the kinetic-energy fluctuations of a true canonical ensemble are suppressed to zero at every rescaling step), unlike the stochastic Bussi-Donadio-Parrinello (2007) variant, or (to leading order, with a finite relaxation time) the Berendsen (1984) weak-coupling thermostat. It is nonetheless the standard first introduction to temperature control in MD and is exactly what is meant by “velocity rescaling” here.
- Parameters:
Examples
>>> import numpy as np >>> from chemistrykit.md.systems.lj_fluid import LJFluid >>> fluid = LJFluid.from_lattice(n_per_side=3, cutoff=1.5, density=0.6, temperature=2.0, rng=0) >>> fluid.velocities *= 2.0 # perturb away from the target temperature >>> thermostat = VelocityRescalingThermostat(target_temperature=2.0) >>> result = fluid.run(dt=0.001, n_steps=50, thermostat=thermostat, sample_every=10) >>> bool(abs(result.temperature[-1] - 2.0) < 0.5) True
- apply(system, dt)[source]#
Rescale
system.velocitiesin place if a rescaling is due.- Parameters:
system (chemistrykit.md.core.base_system.MolecularDynamicsSystem)
dt (
float) – Unused (present for interface symmetry withNoseHooverThermostat).
- Return type:
- class chemistrykit.md.VerletNeighborList(cutoff, skin=0.3, rebuild_every=20)[source]#
Bases:
objectA Verlet (“skin”) neighbor list, rebuilt only every few calls.
Building the pair list with a cutoff enlarged by a skin distance means the list stays a superset of the true cutoff-pairs for several integration steps, since a pair can only enter the true cutoff sphere after its separation has changed by up to skin – the standard MD performance trick (Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., Ch. 5.3). This implementation rebuilds unconditionally every rebuild_every calls rather than tracking accumulated displacement, which is simpler but requires the caller to choose rebuild_every/skin conservatively for their timestep and temperature – a stricter implementation would instead track the two largest particle displacements since the last rebuild and force an early rebuild once their sum exceeds skin.
- Parameters:
- chemistrykit.md.build_neighbor_list(positions, box_length, cutoff)[source]#
Build the list of all particle pairs within cutoff of each other.
A direct, vectorized \(O(N^2)\) all-pairs distance computation under the minimum-image convention – exact, and fast enough for the particle counts this package targets (up to a few thousand). A production MD code instead uses a cell (linked-list) decomposition to reduce this to \(O(N)\); that further optimization is out of scope here. See
VerletNeighborListfor the one optimization this module does provide: reusing a list built with a distance “skin” across several integration steps instead of rebuilding every step.- Parameters:
- Returns:
pairs_i, pairs_j (ndarray of int64, shape (n_pairs,)) – Indices such that
pairs_i < pairs_jfor every returned pair.
Examples
>>> import numpy as np >>> positions = np.array([[0.0, 0.0, 0.0], [0.5, 0.0, 0.0], [5.0, 0.0, 0.0]]) >>> pairs_i, pairs_j = build_neighbor_list(positions, box_length=None, cutoff=1.0) >>> list(zip(pairs_i.tolist(), pairs_j.tolist())) [(0, 1)]
- chemistrykit.md.einstein_diffusion_coefficient(t, msd, n_dim=3, fit_from=0.0)[source]#
Self-diffusion coefficient from the Einstein relation \(\mathrm{MSD}=2dDt\).
Fits a straight line to
msdagainsttfort >= fit_from(skipping the short-time ballistic regime) and returns slope/(2d).- Parameters:
t (array-like) – Lag times.
msd (array-like) – Mean-squared displacement at each lag (see
mean_squared_displacement()).n_dim (
int)fit_from (
float)
- Return type:
- Returns:
float
Examples
>>> import numpy as np >>> t = np.linspace(0.0, 10.0, 11) >>> round(einstein_diffusion_coefficient(t, 6.0 * 0.25 * t + 0.1), 12) 0.25
- chemistrykit.md.green_kubo_diffusion_coefficient(t, vacf, n_dim=3)[source]#
Self-diffusion coefficient from the Green-Kubo integral of the VACF.
\[D = \frac{1}{d}\int_0^{t_{\max}} \langle\vec v(0)\cdot\vec v(t)\rangle\,dt\]evaluated by the trapezoidal rule.
- Parameters:
t (array-like) – Lag times, starting at 0.
vacf (array-like) – Unnormalized velocity autocorrelation (see
velocity_autocorrelation()).n_dim (
int)
- Return type:
- Returns:
float
Examples
An exponentially decaying VACF \(C(t)=3(k_BT/m)e^{-\gamma t}\) (Langevin dynamics) integrates to the Stokes-Einstein-like value \(k_BT/(m\gamma)\):
>>> import numpy as np >>> t = np.linspace(0.0, 40.0, 40001) >>> round(green_kubo_diffusion_coefficient(t, 3.0 * 1.5 * np.exp(-2.0 * t)), 6) 0.75
- chemistrykit.md.mean_squared_displacement(positions, box_length=None, max_lag=None)[source]#
Time-origin-averaged mean-squared displacement.
\[\mathrm{MSD}(\ell) = \left\langle |\vec r_i(t_0+\ell) - \vec r_i(t_0)|^2 \right\rangle_{i,\,t_0}\]- Parameters:
positions (array-like, shape (n_frames, n_particles, n_dim)) – Sampled positions (e.g.
chemistrykit.md.MDResult.positions). Wrapped periodic positions are unwrapped first viaunwrap_trajectory()when box_length is given.max_lag (
int|None) – Largest lag, in frames; defaults ton_frames // 2.
- Returns:
ndarray, shape (max_lag + 1,) – MSD at lags
0, 1, ..., max_lagframes.
Examples
Particles moving ballistically at unit speed have MSD equal to the lag squared:
>>> import numpy as np >>> t = np.arange(5.0) >>> positions = np.stack([t, np.zeros(5), np.zeros(5)], axis=-1)[:, None, :] >>> mean_squared_displacement(positions, max_lag=3) array([0., 1., 4., 9.])
- chemistrykit.md.minimum_image_displacement(r_i, r_j, box_length)[source]#
Displacement \(\vec r_i - \vec r_j\) under the minimum-image convention.
\[\Delta\vec r = (\vec r_i - \vec r_j) - L \, \mathrm{round}\!\left(\frac{\vec r_i - \vec r_j}{L}\right)\]for a cubic periodic box of side L – the displacement to the closest periodic image of particle j relative to particle i, valid whenever L is at least twice the interaction cutoff (Allen & Tildesley, Computer Simulation of Liquids, 2nd ed., Ch. 1.5.2). If box_length is
Noneor<= 0, plain (non-periodic) displacement is returned unchanged.- Parameters:
- Returns:
ndarray
Examples
Two particles near opposite edges of a box of length 10 are actually close together once the box wraps around:
>>> import numpy as np >>> r_i = np.array([0.5, 0.0, 0.0]) >>> r_j = np.array([9.5, 0.0, 0.0]) >>> minimum_image_displacement(r_i, r_j, box_length=10.0) array([1., 0., 0.])
With no box, this is just the ordinary displacement:
>>> minimum_image_displacement(r_i, r_j, box_length=None) array([-9., 0., 0.])
- chemistrykit.md.shake(positions_new, positions_old, constraints, masses, tol=1e-10, max_iter=1000)[source]#
Correct unconstrained positions so every bond-length constraint holds (SHAKE).
- Parameters:
positions_new (array-like, shape (n_atoms, n_dim)) – Positions after an unconstrained update.
positions_old (array-like, shape (n_atoms, n_dim)) – Positions at the previous step (which satisfy the constraints); their bond vectors set the correction directions.
constraints (sequence of (i, j, d)) – Atom pairs and their fixed separations.
masses (array-like, shape (n_atoms,))
tol (
float) – Relative tolerance on \(|s^2 - d^2|/d^2\).max_iter (
int)
- Returns:
positions (ndarray, shape (n_atoms, n_dim)) – Corrected positions.
n_iter (int) – Number of sweeps over the constraints needed to converge.
Examples
Two equal masses pulled apart to separation 1.2 are pulled back symmetrically to the constrained length 1.0:
>>> import numpy as np >>> old = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) >>> new = np.array([[-0.1, 0.0, 0.0], [1.1, 0.0, 0.0]]) >>> fixed, _ = shake(new, old, [(0, 1, 1.0)], masses=[1.0, 1.0]) >>> np.round(fixed[:, 0], 10) array([0., 1.])
- chemistrykit.md.unwrap_trajectory(positions, box_length)[source]#
Undo periodic wrapping of a sampled trajectory.
Accumulates the minimum-image displacement between consecutive frames, so it is exact provided no particle moves more than half a box length between two samples.
- Parameters:
- Returns:
ndarray, shape (n_frames, n_particles, n_dim)
Examples
>>> import numpy as np >>> wrapped = np.array([[[9.5]], [[0.3]], [[1.1]]]) # crosses the box edge at 10 >>> unwrap_trajectory(wrapped, box_length=10.0).ravel() array([ 9.5, 10.3, 11.1])
- chemistrykit.md.velocity_autocorrelation(velocities, max_lag=None, normalize=False)[source]#
Time-origin-averaged velocity autocorrelation function.
\[C(\ell) = \left\langle \vec v_i(t_0)\cdot\vec v_i(t_0+\ell) \right\rangle_{i,\,t_0}\]- Parameters:
- Returns:
ndarray, shape (max_lag + 1,)
Examples
>>> import numpy as np >>> v = np.array([[[1.0, 0.0]], [[0.0, 1.0]], [[-1.0, 0.0]]]) # a velocity rotating by 90 degrees per frame >>> velocity_autocorrelation(v, max_lag=2) array([ 1., 0., -1.])