Note
Go to the end to download the full example code.
Atomistic Water Model for Molecular Dynamics¶
- Authors:
Philip Loche @PicoCentauri, Marcel Langer @sirmarcel and Michele Ceriotti @ceriottm
In this example, we demonstrate how to construct a metatensor atomistic model for flexible three and four-point water
model, with parameters optimized for use together with quantum-nuclear-effects-aware
path integral simulations (cf. Habershon et al., JCP (2009)). The model also demonstrates the use of
torch-pme
, a Torch library for long-range interactions, and uses the resulting model
to perform demonstrative molecular dynamics simulations.
# sphinx_gallery_thumbnail_number = 3
import subprocess
from typing import Dict, List, Optional, Tuple
import ase.io
# Simulation and visualization tools
import chemiscope
import matplotlib.pyplot as plt
# Model wrapping and execution tools
import numpy as np
import torch
# Core atomistic libraries
import torchpme
from ase.optimize import LBFGS
from ipi.utils.parsing import read_output, read_trajectory
from ipi.utils.scripting import (
InteractiveSimulation,
forcefield_xml,
motion_nvt_xml,
simulation_xml,
)
from metatensor.torch import Labels, TensorBlock, TensorMap
from metatensor.torch.atomistic import (
MetatensorAtomisticModel,
ModelCapabilities,
ModelEvaluationOptions,
ModelMetadata,
ModelOutput,
NeighborListOptions,
System,
load_atomistic_model,
)
# Integration with ASE calculator for metatensor atomistic models
from metatensor.torch.atomistic.ase_calculator import MetatensorCalculator
from vesin.torch.metatensor import NeighborList
The q-TIP4P/F Model¶
The q-TIP4P/F model uses simple (quasi)-harmonic terms to describe intra-molecular
flexibility - with the use of a quartic term being a specific feature used to describe
the covalent bond softening for a H-bonded molecule - a Lennard-Jones term describing
dispersion and repulsion between O atoms, and an electrostatic potential between
partial charges on the H atoms and the oxygen electron density. For a four-point
model, the oxygen charge is slightly displaced from the oxygen’s position, improving
properties like the dielectric constant. The
fourth point, referred to as M
, is implicitly derived from the other atoms of each
water molecule.
Intra-molecular interactions¶
The molecular bond potential is usually defined as a harmonic potential of the form
Here, \(k_\mathrm{bond}\) is the force constant and \(r_0\) is the equilibrium distance. Bonded terms like this require defining a topology, i.e. a list of pairs of atoms that are actually bonded to each other.
q-TIP4P/F doesn’t use a harmonic potential but a quartic approximation of a Morse potential, that allows describing the anharmonicity of the O-H covalent bond, and how the mean distance changes due to zero-point fluctuations and/or hydrogen bonding.
Note
The harmonic coefficient is related to the coefficients in the original paper by \(k_r=2 D_r \alpha_r^2\).
def bond_energy(
distances: torch.Tensor,
coefficient_k: torch.Tensor,
coefficient_alpha: torch.Tensor,
coefficient_r0: torch.Tensor,
) -> torch.Tensor:
"""Harmonic potential for bond terms."""
dr = distances - coefficient_r0
alpha_dr = dr * coefficient_alpha
return 0.5 * coefficient_k * dr**2 * (1 - alpha_dr + alpha_dr**2 * 7 / 12)
The parameters reproduce the form of a Morse potential close to the minimum, but avoids the dissociation of the bond, due to the truncation to the quartic term. These are the parameters used for q-TIP4P/F
OH_kr = 116.09 * 2 * 2.287**2 # kcal/mol/Å**2
OH_r0 = 0.9419 # Å
OH_alpha = 2.287 # 1/Å
bond_distances = np.linspace(0.5, 1.65, 200)
bond_potential = bond_energy(
distances=bond_distances,
coefficient_k=OH_kr,
coefficient_alpha=OH_alpha,
coefficient_r0=OH_r0,
)
fig, ax = plt.subplots(1, 1, figsize=(4, 3), constrained_layout=True)
ax.set_title("Bond Potential Between Oxygen and Hydrogen")
ax.plot(bond_distances, bond_potential)
ax.axvline(OH_r0, label="equilibrium distance", color="black", linestyle="--")
ax.set_xlabel("Distance / Å ")
ax.set_ylabel("Bond Potential / (kcal/mol)")
ax.legend()
fig.show()

The harmonic angle potential describe the bending of the HOH angle, and is usually modeled as a (curvilinear) harmonic term, defined based on the angle
where \(k_\mathrm{angle}\) is the force constant and \(\theta_0\) is the equilibrium angle between the three atoms.
def bend_energy(
angles: torch.Tensor, coefficient: torch.Tensor, equilibrium_angle: torch.Tensor
):
"""Harmonic potential for angular terms."""
return 0.5 * coefficient * (angles - equilibrium_angle) ** 2
We use the following parameters:
HOH_angle_coefficient = 87.85 # kcal/mol/rad^2
HOH_equilibrium_angle = 107.4 * torch.pi / 180 # radians
We can plot the bend energy as a function of the angle that is, unsurprisingly, parabolic around the equilibrium angle
angle_distances = np.linspace(100, 115, 200)
angle_potential = bend_energy(
angles=angle_distances * torch.pi / 180,
coefficient=HOH_angle_coefficient,
equilibrium_angle=HOH_equilibrium_angle,
)
fig, ax = plt.subplots(1, 1, figsize=(4, 3), constrained_layout=True)
ax.set_title("Harmonic Angular Potential for a Water Molecule")
ax.plot(angle_distances, angle_potential)
ax.axvline(
HOH_equilibrium_angle * 180 / torch.pi,
label="equilibrium angle",
color="black",
linestyle=":",
)
ax.set_xlabel("Angle / degrees")
ax.set_ylabel("Harmonic Angular Potential / (kcal/mol)")
ax.legend()
fig.show()

Lennard-Jones Potential¶
The Lennard-Jones (LJ) potential describes the interaction between a pair of neutral atoms or molecules, balancing dispersion forces at longer ranges and repulsive forces at shorter ranges. The LJ potential is defined as:
where \(\epsilon\) is the depth of the potential well and \(\sigma\) the distance at which the potential is zero. For water there is usually only an oxygen-oxygen Lennard-Jones potential.
We implement the Lennard-Jones potential as a function that takes distances, along
with the parameters sigma
, epsilon
, and cutoff
that indicates the distance
at which the interaction is assumed to be zero. To ensure that there is no
discontinuity an offset is included to shift the curve so it is zero at the cutoff
distance.
def lennard_jones_pair(
distances: torch.Tensor,
sigma: torch.Tensor,
epsilon: torch.Tensor,
cutoff: torch.Tensor,
):
"""Shifted Lennard-Jones potential for pair terms."""
c6 = (sigma / distances) ** 6
c12 = c6**2
lj = 4 * epsilon * (c12 - c6)
sigma_cutoff_6 = (sigma / cutoff) ** 6
offset = 4 * epsilon * sigma_cutoff_6 * (sigma_cutoff_6 - 1)
return lj - offset
We plot this potential to visualize its behavior, using q-TIP4P/F parameters. To highlight the offset, we use a cutoff of 5 Å instead of the usual 7 Å.
O_sigma = 3.1589 # Å
O_epsilon = 0.1852 # kcal/mol
cutoff = 5.0 # Å <- cut short, to highlight offset
lj_distances = np.linspace(3, cutoff, 200)
lj_potential = lennard_jones_pair(
distances=lj_distances, sigma=O_sigma, epsilon=O_epsilon, cutoff=cutoff
)
fig, ax = plt.subplots(1, 1, figsize=(4, 3), constrained_layout=True)
ax.set_title("Lennard-Jones Potential Between Two Oxygen Atoms")
ax.axhline(0, color="black", linestyle="--")
ax.axhline(-O_epsilon, color="red", linestyle=":", label="Oxygen ε")
ax.axvline(O_sigma, color="black", linestyle=":", label="Oxygen σ")
ax.plot(lj_distances, lj_potential)
ax.set_xlabel("Distance / Å")
ax.set_ylabel("Lennard-Jones Potential / (kcal/mol)")
ax.legend()
fig.show()

Due to our reduced cutoff the minimum of the blue line does not touch the red horizontal line.
Electrostatic Potential¶
The long-ranged nature of electrostatic interactions makes computing them in simulations non-trivial. For periodic systems the Coulomb energy is given by:
The sum over \(\boldsymbol n\) takes into account the periodic images of the charges and the prime indicates that in the case \(i=j\) the term \(n=0\) must be omitted. Further \(\boldsymbol r_{ij} = \boldsymbol r_i - \boldsymbol r_j\) and \(\boldsymbol L\) is the length of the (cubic)simulation box.
Since this sum is conditionally convergent it isn’t computable using a direct sum. Instead the Ewald summation, published in 1921, remains a foundational method that effectively defines how to compute the energy and forces of such systems. To further speed the methods, mesh based algorithm suing fast Fourier transformation have been developed, such as the Particle-Particle Particle-Mesh (P3M) algorithm. For further details we refer to a paper by Deserno and Holm.
We use a Torch implementation of the P3M method within the torch-pme
package.
The core class we use is the torchpme.P3MCalculator
class - you can read more
and see specific examples in the torchpme documentation As a demonstration we use two
charges in a cubic cell, computing the electrostatic energy as a function of distance
O_charge = -0.84
coulomb_distances = torch.linspace(0.5, 9.0, 50)
cell = torch.eye(3) * 10.0
We also use the parameter-tuning functionality of torchpme
, to provide efficient
evaluation at the desired level of accuracy. This is achieved calling
torchpme.tuning.tune_p3m()
on a template of the target structure
charges = torch.tensor([O_charge, -O_charge]).unsqueeze(-1)
positions_coul = torch.tensor([[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]])
neighbor_indices = torch.tensor([[0, 1]])
neighbor_distances = torch.tensor([4.0])
p3m_smearing, p3m_parameters, _ = torchpme.tuning.tune_p3m(
charges,
cell,
positions_coul,
cutoff,
neighbor_indices,
neighbor_distances,
accuracy=1e-4,
)
p3m_prefactor = torchpme.prefactors.kcalmol_A
The hydrogen charge is derived from the oxygen charge as \(q_H = -q_O/2\). The
smearing
and mesh_spacing
parameters are the central parameters for P3M and
are crucial to ensure the correct energy calculation. We now compute the electrostatic
energy between two point charges using the P3M algorithm.
p3m_calculator = torchpme.P3MCalculator(
potential=torchpme.CoulombPotential(p3m_smearing),
**p3m_parameters,
prefactor=p3m_prefactor,
)
For the inference, we need a neighbor list and distances which we compute “manually”. Typically, the neighbors are provided by the simulation engine.
neighbor_indices = torch.tensor([[0, 1]])
potential = torch.zeros_like(coulomb_distances)
for i_dist, dist in enumerate(coulomb_distances):
positions_coul = torch.tensor([[0.0, 0.0, 0.0], [dist, 0.0, 0.0]])
charges = torch.tensor([O_charge, -O_charge]).unsqueeze(-1)
neighbor_distances = torch.tensor([dist])
potential[i_dist] = p3m_calculator.forward(
positions=positions_coul,
cell=cell,
charges=charges,
neighbor_indices=neighbor_indices,
neighbor_distances=neighbor_distances,
)[0]
We plot the electrostatic potential between two point charges.
fig, ax = plt.subplots(1, 1, figsize=(4, 3), constrained_layout=True)
ax.set_title("Electrostatic Potential Between Two Point Charges")
ax.plot(coulomb_distances, potential)
ax.set_xlabel("Distance / Å")
ax.set_ylabel("Electrostatic Potential / (kcal/mol)")
fig.show()

The potential shape may appear unusual due to computations within a periodic box. For small distances, the potential behaves like \(1/r\), but it increases again as charges approach across periodic boundaries.
Note
In most water models, Coulomb interactions within each molecule are excluded, as intramolecular energies are already parametrized by the bond and angle interactions. Therefore, in our model defined below we first compute the electrostatic energy of all atoms and then subtract interactions between bonded atoms.
Implementation as a TIP4P/f torch
module¶
In order to implement a Q-TIP4P/f potential in practice, we first build a class that
follows the interface of a metatomic model. This requires defining the atomic
structure in terms of a metatensor.torch.atomistic.System
object - a simple
container for positions, cell, and atomic types, that can also be enriched with one or
more metatensor.torch.atomistic.NeighborList
objects holding neighbor
distance information. This is usually provided by the code used to perform a
simulation, but can be also computed explicitly using ase
or vesin, as we do here.
For running our simulation we use a small waterbox containing 32 water molecules.
atoms = ase.io.read("data/water_32.xyz")
chemiscope.show(
[atoms],
mode="structure",
settings=chemiscope.quick_settings(structure_settings={"unitCell": True}),
)
We transform the ase Atoms object into a metatensor atomistic system and define the options for the neighbor list.
system = System(
types=torch.from_numpy(atoms.get_atomic_numbers()),
positions=torch.from_numpy(atoms.positions),
cell=torch.from_numpy(atoms.cell.array),
pbc=torch.from_numpy(atoms.pbc),
)
nlo = NeighborListOptions(cutoff=7.0, full_list=False, strict=False)
calculator = NeighborList(nlo, length_unit="Angstrom")
neighbors = calculator.compute(system)
system.add_neighbor_list(nlo, neighbors)
Neighbor lists are stored within metatensor
as metatensor.TensorBlock
objects, if you’re curious
neighbors
TensorBlock
samples (6001): ['first_atom', 'second_atom', 'cell_shift_a', 'cell_shift_b', 'cell_shift_c']
components (3): ['xyz']
properties (1): ['distance']
gradients: None
Helper functions for molecular geometry¶
In order to compute the different terms in the Q-TIP4P/f potential, we need to extract some information on the geometry of the water molecule. To keep the model class clean, we define a helper functions that do two things.
First, it computes O-H covalent bond lengths and angle. We use heuristics to identify the covalent bonds as the shortest O-H distances in a simulation. This is necessary when using the model with an external code, that might re-order atoms internally (as is e.g. the case for LAMMPS). The heuristics here may fail in case molecules get too close together, or at very high temperature.
Second, it computes the position of the virtual “M sites”, the position of the O
charge in a TIP4P-like model. We also need distances to compute range-separated
electrostatics, which we obtain manipulating the neighbor list that is pre-attached to
the system. Thanks to the fact we rely on torch
autodifferentiation mechanism, the
forces acting on the virtual sites will be automatically split between O and H atoms,
in a way that is consistent with the definition.
# forces acting on the M sites will be automatically split between O and H atoms, in a
# way that is consistent with the definition.
def get_molecular_geometry(
system: System,
nlo: NeighborListOptions,
m_gamma: torch.Tensor,
m_charge: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor, System, torch.Tensor, torch.Tensor]:
"""Compute bond distances, angles and charge site positions for each molecules."""
neighbors = system.get_neighbor_list(nlo)
species = system.types
# get neighbor idx and vectors as torch tensors
neigh_ij = neighbors.samples.view(["first_atom", "second_atom"]).values
neigh_dij = neighbors.values.squeeze()
# get all OH distances and their sorting order
oh_mask = species[neigh_ij[:, 0]] != species[neigh_ij[:, 1]]
oh_ij = neigh_ij[oh_mask]
oh_dij = neigh_dij[oh_mask]
oh_dist = torch.linalg.vector_norm(oh_dij, dim=1).squeeze()
oh_sort = torch.argsort(oh_dist)
# gets the oxygen indices in the bonds, sorted by distance
oh_oidx = torch.where(species[oh_ij[:, 0]] == 8, oh_ij[:, 0], oh_ij[:, 1])[oh_sort]
# makes sure we always consider bonds in the O->H direction
oh_dij = oh_dij * torch.where(species[oh_ij[:, 0]] == 8, 1.0, -1.0).reshape(-1, 1)
# we assume that the first n_oxygen*2 bonds cover all water molecules.
# if not we throw an error
o_idx = torch.nonzero(species == 8).squeeze()
n_oh = len(o_idx) * 2
oh_oidx_sort = torch.argsort(oh_oidx[:n_oh])
oh_dij_oidx = oh_oidx[oh_oidx_sort] # indices of the O atoms for each dOH
# if each O index appears twice, this should be a vector of zeros
twoo_chk = oh_dij_oidx[::2] - oh_dij_oidx[1::2]
if (twoo_chk != 0).any():
raise RuntimeError("Cannot assign OH bonds to water molecules univocally.")
# finally, we compute O->H1 and O->H2 for each water molecule
oh_dij_sort = oh_dij[oh_sort[:n_oh]][oh_oidx_sort]
doh_1 = oh_dij_sort[::2]
doh_2 = oh_dij_sort[1::2]
oh_dist = torch.concatenate(
[
torch.linalg.vector_norm(doh_1, dim=1),
torch.linalg.vector_norm(doh_2, dim=1),
]
)
if oh_dist.max() > 2.0:
raise ValueError(
"Unphysical O-H bond length. Check that the molecules are entire, and "
"atoms are listed in the expected OHH order."
)
hoh_angles = torch.arccos(
torch.sum(doh_1 * doh_2, dim=1)
/ (oh_dist[: len(doh_1)] * oh_dist[len(doh_2) :])
)
# now we use this information to build the M sites
# we first put the O->H vectors in the same order as the
# positions. This allows us to manipulate all atoms at once later
oh1_vecs = torch.zeros_like(system.positions)
oh1_vecs[oh_dij_oidx[::2]] = doh_1
oh2_vecs = torch.zeros_like(system.positions)
oh2_vecs[oh_dij_oidx[1::2]] = doh_2
# we compute the vectors O->M displacing the O to the M sites
om_displacements = (1 - m_gamma) * 0.5 * (oh1_vecs + oh2_vecs)
# creates a new System with the m-sites
m_system = System(
types=system.types,
positions=system.positions + om_displacements,
cell=system.cell,
pbc=system.pbc,
)
# adjust neighbor lists to point at the m sites rather than O atoms. this assumes
# this won't have atoms cross the cutoff, which is of course only approximately
# true, so one should use a slighlty larger-than-usual cutoff nb - this is reshaped
# to match the values in a NL tensorblock
nl = system.get_neighbor_list(nlo)
i_idx = nl.samples.view(["first_atom"]).values.flatten()
j_idx = nl.samples.view(["second_atom"]).values.flatten()
m_nl = TensorBlock(
nl.values
+ (om_displacements[j_idx] - om_displacements[i_idx]).reshape(-1, 3, 1),
nl.samples,
nl.components,
nl.properties,
)
m_system.add_neighbor_list(nlo, m_nl)
# set charges of all atoms
charges = (
torch.where(species == 8, -m_charge, 0.5 * m_charge)
.reshape(-1, 1)
.to(dtype=system.positions.dtype, device=system.positions.device)
)
# Create metadata for the charges TensorBlock
samples = Labels(
"atom", torch.arange(len(system), device=charges.device).reshape(-1, 1)
)
properties = Labels(
"charge", torch.zeros(1, 1, device=charges.device, dtype=torch.int32)
)
data = TensorBlock(
values=charges, samples=samples, components=[], properties=properties
)
tensor = TensorMap(
keys=Labels("_", torch.zeros(1, 1, dtype=torch.int32)), blocks=[data]
)
m_system.add_data(name="charges", tensor=tensor)
# intra-molecular distances with M sites (for self-energy removal)
hh_dist = torch.sqrt(((doh_1 - doh_2) ** 2).sum(dim=1))
dmh_1 = doh_1 - om_displacements[oh_dij_oidx[::2]]
dmh_2 = doh_2 - om_displacements[oh_dij_oidx[1::2]]
mh_dist = torch.concatenate(
[
torch.linalg.vector_norm(dmh_1, dim=1),
torch.linalg.vector_norm(dmh_2, dim=1),
]
)
return oh_dist, hoh_angles, m_system, mh_dist, hh_dist
Defining the model¶
Armed with these functions, the definitions of bonded and LJ potentials, and the
torchpme.P3MCalculator
class, we can define with relatively small effort the
actual model. We do not hard-code the specific parameters of the Q-TIP4P/f potential
(so that in principle this class can be used for classical TIP4P, and - by setting
m_gamma
to one - a three-point water model).
A few points worth noting: (1) As discussed above we define a bare Coulomb potential
(that pretty much computes \(1/r\)) which we need to subtract the molecular “self
electrostatic interaction”; (2) units are expected to be Å for distances, kcal/mol for
energies, and radians for angles; (3) model parameters are registered as buffers
;
(4) P3M parameters can also be given, in the format returned by
torchpme.tuning.tune_p3m()
.
The forward
call is a relatively straightforward combination of all the helper
functions defined further up in this file, and should be relatively easy to follow.
class WaterModel(torch.nn.Module):
def __init__(
self,
cutoff: float,
lj_sigma: float,
lj_epsilon: float,
m_gamma: float,
m_charge: float,
oh_bond_eq: float,
oh_bond_k: float,
oh_bond_alpha: float,
hoh_angle_eq: float,
hoh_angle_k: float,
p3m_options: Optional[dict] = None,
dtype: Optional[torch.dtype] = None,
device: Optional[torch.device] = None,
):
super().__init__()
if p3m_options is None:
# should give a ~1e-4 relative accuracy on forces
p3m_options = (1.4, {"interpolation_nodes": 5, "mesh_spacing": 1.33}, 0)
p3m_smearing, p3m_parameters, _ = p3m_options
self.p3m_calculator = torchpme.metatensor.P3MCalculator(
potential=torchpme.CoulombPotential(p3m_smearing),
**p3m_parameters,
prefactor=torchpme.prefactors.kcalmol_A, # consistent units
)
self.p3m_calculator.to(device=device, dtype=dtype)
self.coulomb = torchpme.CoulombPotential()
self.coulomb.to(device=device, dtype=dtype)
# We use a half neighborlist and allow to have pairs farther than cutoff
# (`strict=False`) since this is not problematic for PME and may speed up the
# computation of the neigbors.
self.nlo = NeighborListOptions(cutoff=cutoff, full_list=False, strict=False)
# registers model parameters as buffers
self.dtype = dtype if dtype is not None else torch.get_default_dtype()
self.device = device if device is not None else torch.get_default_device()
self.register_buffer("cutoff", torch.tensor(cutoff, dtype=self.dtype))
self.register_buffer("lj_sigma", torch.tensor(lj_sigma, dtype=self.dtype))
self.register_buffer("lj_epsilon", torch.tensor(lj_epsilon, dtype=self.dtype))
self.register_buffer("m_gamma", torch.tensor(m_gamma, dtype=self.dtype))
self.register_buffer("m_charge", torch.tensor(m_charge, dtype=self.dtype))
self.register_buffer("oh_bond_eq", torch.tensor(oh_bond_eq, dtype=self.dtype))
self.register_buffer("oh_bond_k", torch.tensor(oh_bond_k, dtype=self.dtype))
self.register_buffer(
"oh_bond_alpha", torch.tensor(oh_bond_alpha, dtype=self.dtype)
)
self.register_buffer(
"hoh_angle_eq", torch.tensor(hoh_angle_eq, dtype=self.dtype)
)
self.register_buffer("hoh_angle_k", torch.tensor(hoh_angle_k, dtype=self.dtype))
def requested_neighbor_lists(self):
"""Returns the list of neighbor list options that are needed."""
return [self.nlo]
def _setup_systems(
self,
systems: list[System],
selected_atoms: Optional[Labels] = None,
) -> tuple[System, TensorBlock]:
if len(systems) > 1:
raise ValueError(f"only one system supported, got {len(systems)}")
system_i = 0
system = systems[system_i]
# select only real atoms and discard ghosts
# (this is to work in codes like LAMMPS that provide a neighbor
# list that also includes "domain decomposition" neigbhbors)
if selected_atoms is not None:
current_system_mask = selected_atoms.column("system") == system_i
current_atoms = selected_atoms.column("atom")
current_atoms = current_atoms[current_system_mask].to(torch.long)
types = system.types[current_atoms]
positions = system.positions[current_atoms]
system_clean = System(types, positions, system.cell, system.pbc)
for nlo in system.known_neighbor_lists():
system_clean.add_neighbor_list(nlo, system.get_neighbor_list(nlo))
else:
system_clean = system
return system_clean, system.get_neighbor_list(self.nlo)
def forward(
self,
systems: List[System], # noqa
outputs: Dict[str, ModelOutput], # noqa
selected_atoms: Optional[Labels] = None,
) -> Dict[str, TensorMap]: # noqa
if list(outputs.keys()) != ["energy"]:
raise ValueError(
f"`outputs` keys ({', '.join(outputs.keys())}) contain unsupported "
"keys. Only 'energy' is supported."
)
if outputs["energy"].per_atom:
raise NotImplementedError("per-atom energies are not supported.")
system, neighbors = self._setup_systems(systems, selected_atoms)
# compute non-bonded LJ energy
neighbor_indices = neighbors.samples.view(["first_atom", "second_atom"]).values
species = system.types
oo_mask = (species[neighbor_indices[:, 0]] == 8) & (
species[neighbor_indices[:, 1]] == 8
)
oo_distances = torch.linalg.vector_norm(neighbors.values[oo_mask], dim=1)
energy_lj = lennard_jones_pair(
oo_distances, self.lj_sigma, self.lj_epsilon, self.cutoff
).sum()
d_oh, a_hoh, m_system, mh_dist, hh_dist = get_molecular_geometry(
system, self.nlo, self.m_gamma, self.m_charge
)
# intra-molecular energetics
e_bond = bond_energy(
d_oh, self.oh_bond_k, self.oh_bond_alpha, self.oh_bond_eq
).sum()
e_bend = bend_energy(a_hoh, self.hoh_angle_k, self.hoh_angle_eq).sum()
# now this is the long-range part - computed over the M-site system
# m_system, mh_dist, hh_dist = get_msites(system, self.m_gamma, self.m_charge)
m_neighbors = m_system.get_neighbor_list(self.nlo)
potentials = self.p3m_calculator(m_system, m_neighbors).block(0).values
charges = m_system.get_data("charges").block().values
energy_coulomb = (potentials * charges).sum()
# this is the intra-molecular Coulomb interactions, that must be removed
# to avoid double-counting
energy_self = (
(
self.coulomb.from_dist(mh_dist).sum() * (-self.m_charge)
+ self.coulomb.from_dist(hh_dist).sum() * 0.5 * self.m_charge
)
* 0.5
* self.m_charge
* torchpme.prefactors.kcalmol_A
)
# combines all energy terms
energy_tot = e_bond + e_bend + energy_lj + energy_coulomb - energy_self
# Rename property label to follow metatensor's convention for an atomistic model
samples = Labels(
["system"], torch.arange(len(systems), device=self.device).reshape(-1, 1)
)
block = TensorBlock(
values=torch.sum(energy_tot).reshape(-1, 1),
samples=samples,
components=torch.jit.annotate(List[Labels], []),
properties=Labels(["energy"], torch.tensor([[0]], device=self.device)),
)
return {
"energy": TensorMap(
Labels("_", torch.tensor([[0]], device=self.device)), [block]
),
}
All this class does is take a System
and return its energy (as a
:clas:`metatensor.TensorMap``).
qtip4pf_parameters = dict(
cutoff=7.0,
lj_sigma=3.1589,
lj_epsilon=0.1852,
m_gamma=0.73612,
m_charge=1.1128,
oh_bond_eq=0.9419,
oh_bond_k=2 * 116.09 * 2.287**2,
oh_bond_alpha=2.287,
hoh_angle_eq=107.4 * torch.pi / 180,
hoh_angle_k=87.85,
)
qtip4pf_model = WaterModel(
**qtip4pf_parameters
# uncomment to override default options
# p3m_options = (1.4, {"interpolation_nodes": 5, "mesh_spacing": 1.33}, 0)
)
We re-initilize the system
to ask for gradients
system = System(
types=torch.from_numpy(atoms.get_atomic_numbers()),
positions=torch.from_numpy(atoms.positions).requires_grad_(),
cell=torch.from_numpy(atoms.cell.array).requires_grad_(),
pbc=torch.from_numpy(atoms.pbc),
)
system.add_neighbor_list(nlo, calculator.compute(system))
energy_unit = "kcal/mol"
length_unit = "angstrom"
outputs = {"energy": ModelOutput(quantity="energy", unit=energy_unit, per_atom=False)}
nrg = qtip4pf_model.forward([system], outputs)
nrg["energy"].block(0).values.backward()
print(
f"""
Energy is {nrg["energy"].block(0).values[0].item()} kcal/mol
The forces on the first molecule (in kcal/mol/Å) are
{system.positions.grad[:3]}
The stress is
{system.cell.grad}
"""
)
/home/runner/work/atomistic-cookbook/atomistic-cookbook/.nox/water-model/lib/python3.12/site-packages/torchpme/metatensor/calculator.py:88: UserWarning: custom data 'charges' is experimental, please contact metatensor's developers to add this data as a member of the `System` class (Triggered internally at /project/metatensor-torch/src/atomistic/system.cpp:866.)
charge_tensor = system.get_data("charges")
Energy is -274.07545584268337 kcal/mol
The forces on the first molecule (in kcal/mol/Å) are
tensor([[-22.5891, -4.0345, 16.9057],
[ 10.1369, 0.0815, -0.2837],
[ 1.1909, 9.2458, -7.3839]], dtype=torch.float64)
The stress is
tensor([[ -1.1619, 15.5917, -14.0268],
[ 18.7409, 5.3058, -16.6880],
[-31.6888, -8.1921, -42.9294]], dtype=torch.float64)
Build and save a MetatensorAtomisticModel
¶
This model can be wrapped into a
metatensor.torch.atomistic.MetatensorAtomisticModel
class, that provides
useful helpers to specify the capabilities of the model, and to save it as a
torchscript
module.
Model options include a definition of its units, and a description of the quantities it can compute.
Note
We neeed to specify that the model has infinite interaction range because of the presence of a long-range term that means one cannot assume that forces decay to zero beyond the cutoff.
options = ModelEvaluationOptions(
length_unit=length_unit, outputs=outputs, selected_atoms=None
)
model_capabilities = ModelCapabilities(
outputs=outputs,
atomic_types=[1, 8],
interaction_range=torch.inf,
length_unit=length_unit,
supported_devices=["cpu", "cuda"],
dtype="float32",
)
atomistic_model = MetatensorAtomisticModel(
qtip4pf_model.eval(), ModelMetadata(), model_capabilities
)
atomistic_model.save("qtip4pf-mta.pt")
Other water models¶
The WaterModel class is flexible enough that one can also implement (and export) other 4-point models, or even 3-point models if one sets the m_gamma parameter to one. For instance, we can implement the (classical) SPC/Fw model (Wu et al., JCP (2006))
spcf_parameters = dict(
cutoff=7.0,
lj_sigma=3.16549,
lj_epsilon=0.155425,
m_gamma=1.0,
m_charge=0.82,
oh_bond_eq=1.012,
oh_bond_k=1059.162,
oh_bond_alpha=0.0,
hoh_angle_eq=113.24 * torch.pi / 180,
hoh_angle_k=75.90,
)
spcf_model = WaterModel(**spcf_parameters)
atomistic_model = MetatensorAtomisticModel(
spcf_model.eval(), ModelMetadata(), model_capabilities
)
atomistic_model.save("spcfw-mta.pt")
Using the Q-TIP4P/f model¶
The torchscript
model can be reused with any simulation software compatible with
the metatomic
API. Here we give a couple of examples, designed to demonstrate the
usage more than to provide realistic use cases.
Geometry optimization with ase
¶
We begin with an example based on an ase
-compatible calculator. To this end,
metatensor
provides a compatible
metatensor.torch.atomistic.MetatensorCalculator
wrapper to a model. Note how
the metadata associated with the model are used to convert energy into the units
expected by ase
(eV and Å).
atomistic_model = load_atomistic_model("qtip4pf-mta.pt")
mta_calculator = MetatensorCalculator(atomistic_model)
atoms.calc = mta_calculator
nrg = atoms.get_potential_energy()
print(
f"""
Energy is {nrg} eV, corresponding to {nrg*23.060548} kcal/mol
"""
)
Energy is -11.8850679397583 eV, corresponding to -274.0761797080574 kcal/mol
We then use one of the built-in ase
functions to run the structural relaxation.
The relaxation is split into short segments just to be able to visualize the
trajectory.
fmax
is the threshold on the maximum force component and the optimization will
stop when the threshold is reached
opt_trj = []
opt_nrg = []
for _ in range(10):
opt_trj.append(atoms.copy())
opt_nrg.append(atoms.get_potential_energy())
opt = LBFGS(atoms, restart="lbfgs_restart.json")
opt.run(fmax=0.001, steps=5)
opt.run(fmax=0.001, steps=10)
nrg_final = atoms.get_potential_energy()
Step Time Energy fmax
LBFGS: 0 16:17:23 -11.885068 3.466991
LBFGS: 1 16:17:23 -12.927966 1.706440
LBFGS: 2 16:17:23 -13.351190 1.339507
LBFGS: 3 16:17:23 -14.189039 1.595507
LBFGS: 4 16:17:23 -14.462056 0.648454
LBFGS: 5 16:17:23 -14.683365 0.598450
Step Time Energy fmax
LBFGS: 0 16:17:23 -14.683365 0.598450
LBFGS: 1 16:17:23 -14.961548 0.829774
LBFGS: 2 16:17:23 -15.255526 0.781369
LBFGS: 3 16:17:23 -15.422079 0.612486
LBFGS: 4 16:17:23 -15.511729 0.412035
LBFGS: 5 16:17:23 -15.616710 0.654137
Step Time Energy fmax
LBFGS: 0 16:17:23 -15.616710 0.654137
LBFGS: 1 16:17:23 -15.758617 0.819072
LBFGS: 2 16:17:23 -15.908613 0.641355
LBFGS: 3 16:17:23 -16.021681 0.450603
LBFGS: 4 16:17:23 -16.081816 0.417817
LBFGS: 5 16:17:23 -16.140678 0.373983
Step Time Energy fmax
LBFGS: 0 16:17:23 -16.140678 0.373983
LBFGS: 1 16:17:23 -16.227745 0.516826
LBFGS: 2 16:17:23 -16.308884 0.410002
LBFGS: 3 16:17:23 -16.372532 0.393202
LBFGS: 4 16:17:23 -16.429617 0.366466
LBFGS: 5 16:17:23 -16.498814 0.375348
Step Time Energy fmax
LBFGS: 0 16:17:23 -16.498814 0.375348
LBFGS: 1 16:17:23 -16.559963 0.312198
LBFGS: 2 16:17:23 -16.603413 0.306909
LBFGS: 3 16:17:24 -16.635597 0.247765
LBFGS: 4 16:17:24 -16.677437 0.289578
LBFGS: 5 16:17:24 -16.740196 0.359780
Step Time Energy fmax
LBFGS: 0 16:17:24 -16.740196 0.359780
LBFGS: 1 16:17:24 -16.812780 0.307699
LBFGS: 2 16:17:24 -16.857498 0.333325
LBFGS: 3 16:17:24 -16.883713 0.346671
LBFGS: 4 16:17:24 -16.913483 0.282521
LBFGS: 5 16:17:24 -16.957270 0.312849
Step Time Energy fmax
LBFGS: 0 16:17:24 -16.957270 0.312849
LBFGS: 1 16:17:24 -17.000126 0.350248
LBFGS: 2 16:17:24 -17.030533 0.246559
LBFGS: 3 16:17:24 -17.052172 0.249770
LBFGS: 4 16:17:24 -17.078173 0.241961
LBFGS: 5 16:17:24 -17.114550 0.312436
Step Time Energy fmax
LBFGS: 0 16:17:24 -17.114550 0.312436
LBFGS: 1 16:17:24 -17.151857 0.254500
LBFGS: 2 16:17:24 -17.178072 0.226621
LBFGS: 3 16:17:24 -17.198822 0.197654
LBFGS: 4 16:17:24 -17.221266 0.175388
LBFGS: 5 16:17:24 -17.245617 0.225101
Step Time Energy fmax
LBFGS: 0 16:17:24 -17.245617 0.225101
LBFGS: 1 16:17:24 -17.264885 0.174632
LBFGS: 2 16:17:24 -17.281273 0.202240
LBFGS: 3 16:17:24 -17.300669 0.193787
LBFGS: 4 16:17:24 -17.328238 0.281959
LBFGS: 5 16:17:24 -17.355974 0.239306
Step Time Energy fmax
LBFGS: 0 16:17:24 -17.355974 0.239306
LBFGS: 1 16:17:24 -17.374947 0.192578
LBFGS: 2 16:17:24 -17.386719 0.163856
LBFGS: 3 16:17:24 -17.401201 0.196597
LBFGS: 4 16:17:24 -17.422884 0.323531
LBFGS: 5 16:17:24 -17.444946 0.221394
LBFGS: 6 16:17:24 -17.459980 0.259519
LBFGS: 7 16:17:24 -17.469679 0.160036
LBFGS: 8 16:17:25 -17.479334 0.189759
LBFGS: 9 16:17:25 -17.495975 0.274728
LBFGS: 10 16:17:25 -17.513550 0.243968
LBFGS: 11 16:17:25 -17.526806 0.143248
LBFGS: 12 16:17:25 -17.534597 0.128268
LBFGS: 13 16:17:25 -17.542601 0.159613
LBFGS: 14 16:17:25 -17.552849 0.173470
LBFGS: 15 16:17:25 -17.563478 0.143822
Use chemiscope to visualize the geometry optimization together with the convergence of the energy to a local minimum.
chemiscope.show(
frames=opt_trj,
properties={
"step": 1 + np.arange(0, len(opt_trj)),
"energy": opt_nrg - nrg_final,
},
mode="default",
settings=chemiscope.quick_settings(
map_settings={
"x": {"property": "step", "scale": "log"},
"y": {"property": "energy", "scale": "log"},
},
structure_settings={
"unitCell": True,
},
trajectory=True,
),
)
Path integral molecular dynamics with i-PI
¶
We use i-PI to perform path integral molecular-dynamics simulations of bulk water to include nuclear quntum effects. See this recipe for an introduction to constant-temperatur MD using i-PI.
First, the XML
input of the i-PI simulation is created using a few utility
functions. This input could also be written to file and used with the command-line
version of i-PI. We use the structure twice to generate a path integral with two
replicas: this is far from converged, see also this recipe if you
have never run path integral simulations before.
data = ase.io.read("data/water_32.xyz")
input_xml = simulation_xml(
structures=[data, data],
forcefield=forcefield_xml(
name="qtip4pf",
mode="direct",
pes="metatensor",
parameters={"model": "qtip4pf-mta.pt", "template": "data/water_32.xyz"},
),
motion=motion_nvt_xml(timestep=0.5 * ase.units.fs),
temperature=300,
prefix="qtip4pf-md",
)
print(input_xml)
<simulation verbosity='quiet' safe_stride='20'>
<ffdirect name='qtip4pf'>
<pes>metatensor</pes>
<parameters>{model: qtip4pf-mta.pt, template: data/water_32.xyz}</parameters>
</ffdirect>
<output prefix='qtip4pf-md'>
<properties stride='2' filename='out'>[ step, time{picosecond}, conserved{electronvolt}, temperature{kelvin}, potential{electronvolt} ]</properties>
<trajectory filename='pos' stride='20' cell_units='angstrom'>positions{angstrom}</trajectory>
<checkpoint stride='200'>
</checkpoint>
</output>
<system>
<beads natoms='96' nbeads='2'>
<q shape='(2, 288)'>
[ 7.09781133e+00, 8.90061005e+00, 1.79410598e+01, 8.70029908e+00, 8.07291001e+00,
1.82755414e+01, 7.55512505e+00, 1.00533430e+01, 1.66069132e+01, 1.87706496e+01,
1.67070687e+01, 6.91639762e-01, 1.91467051e+01, 1.54881953e+01, 2.11649326e+00,
1.77029543e+01, 1.59662960e+01, -5.97153456e-01, 6.06602086e-01, 2.81947138e+00,
1.09528526e+01, -5.42351398e-01, 3.76622417e+00, 9.90405463e+00, 1.49477337e+00,
3.89472555e+00, 1.20262171e+01, 1.51839494e+01, 1.83964838e+01, 8.13905042e+00,
1.36116973e+01, 1.84985290e+01, 9.15950253e+00, 1.63196748e+01, 1.75876811e+01,
9.29745254e+00, 1.07015191e+01, 1.71624927e+01, 1.24721924e+00, 1.20526732e+01,
1.83700277e+01, 1.53823707e+00, 9.85114229e+00, 1.69905276e+01, 2.93285495e+00,
1.53634734e+01, 4.06291117e-01, 2.26956108e+00, 1.54881953e+01, -1.22832198e-01,
4.00243993e+00, 1.68903721e+01, -3.04245906e-01, 1.54579597e+00, 1.54522905e+01,
7.87070931e+00, 1.35304391e+00, 1.49193878e+01, 9.57335255e+00, 1.58736995e+00,
1.45924651e+01, 6.73120446e+00, 2.53412273e+00, 1.19827534e+01, 6.15294827e+00,
1.82887694e+01, 1.34794165e+01, 6.46853253e+00, 1.92544195e+01, 1.22964479e+01,
4.53156325e+00, 1.76689393e+01, 2.57002753e-01, 1.47360843e+01, 5.17407013e+00,
-1.13383568e-02, 1.56847268e+01, 6.77277843e+00, 5.93374004e-01, 1.30485589e+01,
5.60492769e+00, 9.49965323e+00, 4.84336806e+00, 1.16577205e+01, 1.04653033e+01,
6.30412636e+00, 1.18334650e+01, 1.00401149e+01, 3.83803376e+00, 1.30806842e+01,
1.35379980e+01, 4.80368381e+00, 4.56935777e+00, 1.18070088e+01, 4.66195435e+00,
5.03989958e+00, 1.39537377e+01, 3.17662962e+00, 3.88716664e+00, 6.30412636e+00,
1.71039112e+01, 6.16995580e+00, 5.32524822e+00, 1.57622056e+01, 5.26666671e+00,
5.16462150e+00, 1.85079777e+01, 6.42128938e+00, 2.78923576e+00, 2.68341110e+00,
1.54768570e+00, 2.00310969e+00, 1.07336444e+00, 1.19241719e+00, 2.98954673e+00,
2.85159672e+00, 3.34481524e+00, 2.30924533e+00, 9.34658542e+00, 6.08113867e+00,
3.98921185e+00, 8.88738197e+00, 6.65561542e+00, 2.53601246e+00, 9.36548268e+00,
4.24243515e+00, 1.28312404e+01, 1.22662123e+01, 8.48109085e+00, 1.33849301e+01,
1.08753739e+01, 9.58847036e+00, 1.27499822e+01, 1.37817726e+01, 9.40138748e+00,
1.76311448e+01, 6.54790103e+00, 8.37148674e+00, 1.88481284e+01, 7.47575655e+00,
7.40394696e+00, 1.62195193e+01, 5.93940921e+00, 7.49465382e+00, 1.41427103e+01,
8.58502579e+00, 1.20545630e+01, 1.43694775e+01, 8.40928126e+00, 1.38479131e+01,
1.55732330e+01, 7.74409766e+00, 1.13137903e+01, 8.46597304e-01, 1.07733286e+01,
1.55316590e+01, 1.08281307e+00, 9.14627445e+00, 1.48740343e+01, 1.36060281e+00,
1.06788423e+01, 1.72399714e+01, 1.39083843e+00, 7.71386205e+00, 1.02990074e+00,
1.95019736e+00, 5.94696812e+00, 9.46752789e-01, -4.72431531e-01, 7.51733053e+00,
8.99509636e-01, 7.99165179e+00, 4.87927286e+00, 6.73120446e+00, 8.78722649e+00,
4.46731256e+00, 8.36014838e+00, 8.13716070e+00, 6.66317432e+00, 6.60648254e+00,
7.08458325e+00, 1.00495635e+01, 6.78600652e+00, 6.75955035e+00, 1.05824663e+01,
5.08336328e+00, 8.53967236e+00, 1.09830882e+01, 7.23954079e+00, 4.57313722e-01,
1.61382611e+01, 9.81712722e+00, 4.59203449e-01, 1.78616913e+01, 1.03065663e+01,
1.37005144e+00, 1.53105611e+01, 1.10813540e+01, 1.14611890e+01, 4.59203449e-01,
1.54409522e+01, 1.26328192e+01, -6.80301405e-01, 1.61741659e+01, 9.89271627e+00,
-3.77945225e-02, 1.62251885e+01, 1.39121637e+01, 1.51726111e+01, 1.71001317e+01,
1.26970698e+01, 1.47984453e+01, 1.57074036e+01, 1.28747041e+01, 1.48135631e+01,
1.84380578e+01, 3.72465019e+00, 2.21286929e+00, 6.86159556e+00, 2.87238371e+00,
2.64372685e+00, 8.45463469e+00, 5.33091740e+00, 2.98009810e+00, 6.67640240e+00,
1.45017583e+01, 1.24816411e+01, 4.03267555e+00, 1.34983137e+01, 1.22643226e+01,
5.51422084e+00, 1.61817248e+01, 1.26252602e+01, 4.55235024e+00, 5.23265164e+00,
1.39537377e+01, 1.37477576e+01, 4.44652557e+00, 1.52841049e+01, 1.46415980e+01,
4.24999406e+00, 1.23852650e+01, 1.39990911e+01, 4.25755296e+00, 5.27422562e+00,
1.38875973e+01, 5.13627561e+00, 6.10381539e+00, 1.52614282e+01, 5.58414070e+00,
5.04745848e+00, 1.26743931e+01, 6.90127981e+00, 1.88084441e+01, 1.75555557e+01,
7.77433328e+00, 1.79694057e+01, 1.88651359e+01, 6.04523388e+00, 2.01047963e+01,
1.84890804e+01, 9.94562860e+00, 1.26762829e+01, 1.43411316e+01, 8.15983741e+00,
1.33830404e+01, 1.45641193e+01, 1.03084560e+01, 1.33357973e+01, 1.27235260e+01,
6.62726952e+00, 1.27008493e+01, 1.92752065e+00, 7.74220794e+00, 1.38006699e+01,
1.14706376e+00, 6.77655789e+00, 1.13081211e+01, 7.91795247e-01, 1.03084560e+01,
1.73779215e+01, 1.11267074e+01, 1.03424711e+01, 1.81451503e+01, 1.27707692e+01,
8.59636415e+00, 1.74572900e+01, 1.03632581e+01, 7.09781133e+00, 8.90061005e+00,
1.79410598e+01, 8.70029908e+00, 8.07291001e+00, 1.82755414e+01, 7.55512505e+00,
1.00533430e+01, 1.66069132e+01, 1.87706496e+01, 1.67070687e+01, 6.91639762e-01,
1.91467051e+01, 1.54881953e+01, 2.11649326e+00, 1.77029543e+01, 1.59662960e+01,
-5.97153456e-01, 6.06602086e-01, 2.81947138e+00, 1.09528526e+01, -5.42351398e-01,
3.76622417e+00, 9.90405463e+00, 1.49477337e+00, 3.89472555e+00, 1.20262171e+01,
1.51839494e+01, 1.83964838e+01, 8.13905042e+00, 1.36116973e+01, 1.84985290e+01,
9.15950253e+00, 1.63196748e+01, 1.75876811e+01, 9.29745254e+00, 1.07015191e+01,
1.71624927e+01, 1.24721924e+00, 1.20526732e+01, 1.83700277e+01, 1.53823707e+00,
9.85114229e+00, 1.69905276e+01, 2.93285495e+00, 1.53634734e+01, 4.06291117e-01,
2.26956108e+00, 1.54881953e+01, -1.22832198e-01, 4.00243993e+00, 1.68903721e+01,
-3.04245906e-01, 1.54579597e+00, 1.54522905e+01, 7.87070931e+00, 1.35304391e+00,
1.49193878e+01, 9.57335255e+00, 1.58736995e+00, 1.45924651e+01, 6.73120446e+00,
2.53412273e+00, 1.19827534e+01, 6.15294827e+00, 1.82887694e+01, 1.34794165e+01,
6.46853253e+00, 1.92544195e+01, 1.22964479e+01, 4.53156325e+00, 1.76689393e+01,
2.57002753e-01, 1.47360843e+01, 5.17407013e+00, -1.13383568e-02, 1.56847268e+01,
6.77277843e+00, 5.93374004e-01, 1.30485589e+01, 5.60492769e+00, 9.49965323e+00,
4.84336806e+00, 1.16577205e+01, 1.04653033e+01, 6.30412636e+00, 1.18334650e+01,
1.00401149e+01, 3.83803376e+00, 1.30806842e+01, 1.35379980e+01, 4.80368381e+00,
4.56935777e+00, 1.18070088e+01, 4.66195435e+00, 5.03989958e+00, 1.39537377e+01,
3.17662962e+00, 3.88716664e+00, 6.30412636e+00, 1.71039112e+01, 6.16995580e+00,
5.32524822e+00, 1.57622056e+01, 5.26666671e+00, 5.16462150e+00, 1.85079777e+01,
6.42128938e+00, 2.78923576e+00, 2.68341110e+00, 1.54768570e+00, 2.00310969e+00,
1.07336444e+00, 1.19241719e+00, 2.98954673e+00, 2.85159672e+00, 3.34481524e+00,
2.30924533e+00, 9.34658542e+00, 6.08113867e+00, 3.98921185e+00, 8.88738197e+00,
6.65561542e+00, 2.53601246e+00, 9.36548268e+00, 4.24243515e+00, 1.28312404e+01,
1.22662123e+01, 8.48109085e+00, 1.33849301e+01, 1.08753739e+01, 9.58847036e+00,
1.27499822e+01, 1.37817726e+01, 9.40138748e+00, 1.76311448e+01, 6.54790103e+00,
8.37148674e+00, 1.88481284e+01, 7.47575655e+00, 7.40394696e+00, 1.62195193e+01,
5.93940921e+00, 7.49465382e+00, 1.41427103e+01, 8.58502579e+00, 1.20545630e+01,
1.43694775e+01, 8.40928126e+00, 1.38479131e+01, 1.55732330e+01, 7.74409766e+00,
1.13137903e+01, 8.46597304e-01, 1.07733286e+01, 1.55316590e+01, 1.08281307e+00,
9.14627445e+00, 1.48740343e+01, 1.36060281e+00, 1.06788423e+01, 1.72399714e+01,
1.39083843e+00, 7.71386205e+00, 1.02990074e+00, 1.95019736e+00, 5.94696812e+00,
9.46752789e-01, -4.72431531e-01, 7.51733053e+00, 8.99509636e-01, 7.99165179e+00,
4.87927286e+00, 6.73120446e+00, 8.78722649e+00, 4.46731256e+00, 8.36014838e+00,
8.13716070e+00, 6.66317432e+00, 6.60648254e+00, 7.08458325e+00, 1.00495635e+01,
6.78600652e+00, 6.75955035e+00, 1.05824663e+01, 5.08336328e+00, 8.53967236e+00,
1.09830882e+01, 7.23954079e+00, 4.57313722e-01, 1.61382611e+01, 9.81712722e+00,
4.59203449e-01, 1.78616913e+01, 1.03065663e+01, 1.37005144e+00, 1.53105611e+01,
1.10813540e+01, 1.14611890e+01, 4.59203449e-01, 1.54409522e+01, 1.26328192e+01,
-6.80301405e-01, 1.61741659e+01, 9.89271627e+00, -3.77945225e-02, 1.62251885e+01,
1.39121637e+01, 1.51726111e+01, 1.71001317e+01, 1.26970698e+01, 1.47984453e+01,
1.57074036e+01, 1.28747041e+01, 1.48135631e+01, 1.84380578e+01, 3.72465019e+00,
2.21286929e+00, 6.86159556e+00, 2.87238371e+00, 2.64372685e+00, 8.45463469e+00,
5.33091740e+00, 2.98009810e+00, 6.67640240e+00, 1.45017583e+01, 1.24816411e+01,
4.03267555e+00, 1.34983137e+01, 1.22643226e+01, 5.51422084e+00, 1.61817248e+01,
1.26252602e+01, 4.55235024e+00, 5.23265164e+00, 1.39537377e+01, 1.37477576e+01,
4.44652557e+00, 1.52841049e+01, 1.46415980e+01, 4.24999406e+00, 1.23852650e+01,
1.39990911e+01, 4.25755296e+00, 5.27422562e+00, 1.38875973e+01, 5.13627561e+00,
6.10381539e+00, 1.52614282e+01, 5.58414070e+00, 5.04745848e+00, 1.26743931e+01,
6.90127981e+00, 1.88084441e+01, 1.75555557e+01, 7.77433328e+00, 1.79694057e+01,
1.88651359e+01, 6.04523388e+00, 2.01047963e+01, 1.84890804e+01, 9.94562860e+00,
1.26762829e+01, 1.43411316e+01, 8.15983741e+00, 1.33830404e+01, 1.45641193e+01,
1.03084560e+01, 1.33357973e+01, 1.27235260e+01, 6.62726952e+00, 1.27008493e+01,
1.92752065e+00, 7.74220794e+00, 1.38006699e+01, 1.14706376e+00, 6.77655789e+00,
1.13081211e+01, 7.91795247e-01, 1.03084560e+01, 1.73779215e+01, 1.11267074e+01,
1.03424711e+01, 1.81451503e+01, 1.27707692e+01, 8.59636415e+00, 1.74572900e+01,
1.03632581e+01 ]
</q>
<p shape='(2, 288)'>
[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00 ]
</p>
<m shape='(96)'>
[ 2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03,
1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04,
1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03,
2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03,
1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04,
1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03,
2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03,
1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04,
1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03,
2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03,
1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04,
1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03,
2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03,
1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04,
1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03,
2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03,
1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04,
1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03, 1.83736223e+03,
2.91651223e+04, 1.83736223e+03, 1.83736223e+03, 2.91651223e+04, 1.83736223e+03,
1.83736223e+03 ]
</m>
<names shape='(96)'>
[ O, H, H, O, H,
H, O, H, H, O,
H, H, O, H, H,
O, H, H, O, H,
H, O, H, H, O,
H, H, O, H, H,
O, H, H, O, H,
H, O, H, H, O,
H, H, O, H, H,
O, H, H, O, H,
H, O, H, H, O,
H, H, O, H, H,
O, H, H, O, H,
H, O, H, H, O,
H, H, O, H, H,
O, H, H, O, H,
H, O, H, H, O,
H, H, O, H, H,
O, H, H, O, H,
H ]
</names>
</beads>
<cell shape='(3, 3)'>
[ 1.93885901e+01, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.93885901e+01,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.93885901e+01 ]
</cell>
<initialize nbeads='2'><velocities mode='thermal' units='ase'> 300 </velocities></initialize><ensemble><temperature units='ase'> 300 </temperature></ensemble>
<forces>
<force forcefield='qtip4pf'> </force>
</forces>
<motion mode="dynamics">
<dynamics mode="nvt">
<timestep units="ase"> 0.04911347394232032 </timestep>
<thermostat mode='svr'>
<tau units='ase'> 0.4911347394232032 </tau>
</thermostat>
</dynamics>
</motion>
</system>
</simulation>
Then, we create an InteractiveSimulation
object and run a short simulation (purely
for demonstrative purposes)
sim = InteractiveSimulation(input_xml)
sim.run(400)
ARGS {'verbose': False}
This is an unamed model
=======================
0
The simulation generates output files that can be parsed and visualized from Python
data, info = read_output("qtip4pf-md.out")
trj = read_trajectory("qtip4pf-md.pos_0.xyz")
fig, ax = plt.subplots(1, 1, figsize=(4, 3), constrained_layout=True)
ax.plot(data["time"], data["potential"], "b-", label="potential")
ax.plot(data["time"], data["conserved"] - 4, "k-", label="conserved")
ax.set_xlabel("t / ps")
ax.set_ylabel("energy / ev")
ax.legend()

<matplotlib.legend.Legend object at 0x7fcff6e97d40>
chemiscope.show(
frames=trj,
properties={
"time": data["time"][::10],
"potential": data["potential"][::10],
},
mode="default",
settings=chemiscope.quick_settings(
map_settings={
"x": {"property": "time", "scale": "linear"},
"y": {"property": "potential", "scale": "linear"},
},
structure_settings={
"unitCell": True,
},
trajectory=True,
),
)
Molecular dynamics with LAMMPS
¶
The metatomic
model can also be run with LAMMPS
and used to perform all kinds of atomistic simulations with it.
This only requires defining a pair_metatensor
potential, and specifying
the mapping between LAMMPS atom types and those used in the model.
Note also that the metatomic
interface takes care of converting the
model units to those used in the LAMMPS file, so it is possible to use
energies in eV even if the model outputs kcal/mol.
with open("data/spcfw.in", "r") as file:
lines = file.readlines()
for line in lines[:7] + lines[16:]:
print(line, end="")
units metal # Angstroms, eV, picoseconds
atom_style atomic
read_data water_32.data
# loads metatomic SPC/Fw model
pair_style metatensor spcfw-mta.pt
pair_coeff * * 1 8
fix 1 all nve
fix 2 all langevin 300 300 1.00 12345
run 200
This specific example runs a short MD trajectory, using a Langevin thermostat. Given that this is a classical MD trajectory, we use the SPC/Fw model that is fitted to reproduce (some) water properties even with a classical description of the nuclei.
We save to geometry to a LAMMPS data file and run the simulation
ase.io.write("water_32.data", atoms, format="lammps-data", masses=True)
subprocess.check_call(["lmp_serial", "-in", "data/spcfw.in"])
0
Total running time of the script: (1 minutes 10.007 seconds)