import numpy as np
from ase.constraints.constraint import FixConstraint
from ase.geometry import find_mic, wrap_positions
[docs]
class FixLinearTriatomic(FixConstraint):
"""Holonomic constraints for rigid linear triatomic molecules."""
def __init__(self, triples):
"""Apply RATTLE-type bond constraints between outer atoms n and m
and linear vectorial constraints to the position of central
atoms o to fix the geometry of linear triatomic molecules of the
type:
n--o--m
Parameters
----------
triples: list
Indices of the atoms forming the linear molecules to constrain
as triples. Sequence should be (n, o, m) or (m, o, n).
When using these constraints in molecular dynamics or structure
optimizations, atomic forces need to be redistributed within a
triple. The function redistribute_forces_optimization implements
the redistribution of forces for structure optimization, while
the function redistribute_forces_md implements the redistribution
for molecular dynamics.
References
----------
Ciccotti et al. Molecular Physics 47 (1982)
:doi:`10.1080/00268978200100942`
"""
self.triples = np.asarray(triples)
if self.triples.shape[1] != 3:
raise ValueError('"triples" has wrong size')
self.bondlengths = None
def get_removed_dof(self, atoms):
return 4 * len(self.triples)
@property
def n_ind(self):
return self.triples[:, 0]
@property
def m_ind(self):
return self.triples[:, 2]
@property
def o_ind(self):
return self.triples[:, 1]
def initialize(self, atoms):
masses = atoms.get_masses()
self.mass_n, self.mass_m, self.mass_o = self.get_slices(masses)
self.bondlengths = self.initialize_bond_lengths(atoms)
self.bondlengths_nm = self.bondlengths.sum(axis=1)
C1 = self.bondlengths[:, ::-1] / self.bondlengths_nm[:, None]
C2 = (
C1[:, 0] ** 2 * self.mass_o * self.mass_m
+ C1[:, 1] ** 2 * self.mass_n * self.mass_o
+ self.mass_n * self.mass_m
)
C2 = C1 / C2[:, None]
C3 = self.mass_n * C1[:, 1] - self.mass_m * C1[:, 0]
C3 = C2 * self.mass_o[:, None] * C3[:, None]
C3[:, 1] *= -1
C3 = (C3 + 1) / np.vstack((self.mass_n, self.mass_m)).T
C4 = C1[:, 0] ** 2 + C1[:, 1] ** 2 + 1
C4 = C1 / C4[:, None]
self.C1 = C1
self.C2 = C2
self.C3 = C3
self.C4 = C4
def adjust_positions(self, atoms, new):
old = atoms.positions
new_n, new_m, new_o = self.get_slices(new)
if self.bondlengths is None:
self.initialize(atoms)
r0 = old[self.n_ind] - old[self.m_ind]
d0, _ = find_mic(r0, atoms.cell, atoms.pbc)
d1 = new_n - new_m - r0 + d0
a = np.einsum('ij,ij->i', d0, d0)
b = np.einsum('ij,ij->i', d1, d0)
c = np.einsum('ij,ij->i', d1, d1) - self.bondlengths_nm**2
g = (b - (b**2 - a * c) ** 0.5) / (a * self.C3.sum(axis=1))
g = g[:, None] * self.C3
new_n -= g[:, 0, None] * d0
new_m += g[:, 1, None] * d0
if np.allclose(d0, r0):
new_o = self.C1[:, 0, None] * new_n + self.C1[:, 1, None] * new_m
else:
v1, _ = find_mic(new_n, atoms.cell, atoms.pbc)
v2, _ = find_mic(new_m, atoms.cell, atoms.pbc)
rb = self.C1[:, 0, None] * v1 + self.C1[:, 1, None] * v2
new_o = wrap_positions(rb, atoms.cell, atoms.pbc)
self.set_slices(new_n, new_m, new_o, new)
def adjust_momenta(self, atoms, p):
old = atoms.positions
p_n, p_m, p_o = self.get_slices(p)
if self.bondlengths is None:
self.initialize(atoms)
mass_nn = self.mass_n[:, None]
mass_mm = self.mass_m[:, None]
mass_oo = self.mass_o[:, None]
d = old[self.n_ind] - old[self.m_ind]
d, _ = find_mic(d, atoms.cell, atoms.pbc)
dv = p_n / mass_nn - p_m / mass_mm
k = np.einsum('ij,ij->i', dv, d) / self.bondlengths_nm**2
k = self.C3 / (self.C3.sum(axis=1)[:, None]) * k[:, None]
p_n -= k[:, 0, None] * mass_nn * d
p_m += k[:, 1, None] * mass_mm * d
p_o = mass_oo * (
self.C1[:, 0, None] * p_n / mass_nn
+ self.C1[:, 1, None] * p_m / mass_mm
)
self.set_slices(p_n, p_m, p_o, p)
def adjust_forces(self, atoms, forces):
if self.bondlengths is None:
self.initialize(atoms)
A = self.C4 * np.diff(self.C1)
A[:, 0] *= -1
A -= 1
B = np.diff(self.C4) / (A.sum(axis=1))[:, None]
A /= (A.sum(axis=1))[:, None]
self.constraint_forces = -forces
old = atoms.positions
fr_n, fr_m, fr_o = self.redistribute_forces_optimization(forces)
d = old[self.n_ind] - old[self.m_ind]
d, _ = find_mic(d, atoms.cell, atoms.pbc)
df = fr_n - fr_m
k = -np.einsum('ij,ij->i', df, d) / self.bondlengths_nm**2
forces[self.n_ind] = fr_n + k[:, None] * d * A[:, 0, None]
forces[self.m_ind] = fr_m - k[:, None] * d * A[:, 1, None]
forces[self.o_ind] = fr_o + k[:, None] * d * B
self.constraint_forces += forces
def redistribute_forces_optimization(self, forces):
"""Redistribute forces within a triple when performing structure
optimizations.
The redistributed forces needs to be further adjusted using the
appropriate Lagrange multipliers as implemented in adjust_forces."""
forces_n, forces_m, forces_o = self.get_slices(forces)
C1_1 = self.C1[:, 0, None]
C1_2 = self.C1[:, 1, None]
C4_1 = self.C4[:, 0, None]
C4_2 = self.C4[:, 1, None]
fr_n = (1 - C4_1 * C1_1) * forces_n - C4_1 * (
C1_2 * forces_m - forces_o
)
fr_m = (1 - C4_2 * C1_2) * forces_m - C4_2 * (
C1_1 * forces_n - forces_o
)
fr_o = (
(1 - 1 / (C1_1**2 + C1_2**2 + 1)) * forces_o
+ C4_1 * forces_n
+ C4_2 * forces_m
)
return fr_n, fr_m, fr_o
def redistribute_forces_md(self, atoms, forces, rand=False):
"""Redistribute forces within a triple when performing molecular
dynamics.
When rand=True, use the equations for random force terms, as
used e.g. by Langevin dynamics, otherwise apply the standard
equations for deterministic forces (see Ciccotti et al. Molecular
Physics 47 (1982))."""
if self.bondlengths is None:
self.initialize(atoms)
forces_n, forces_m, forces_o = self.get_slices(forces)
C1_1 = self.C1[:, 0, None]
C1_2 = self.C1[:, 1, None]
C2_1 = self.C2[:, 0, None]
C2_2 = self.C2[:, 1, None]
mass_nn = self.mass_n[:, None]
mass_mm = self.mass_m[:, None]
mass_oo = self.mass_o[:, None]
if rand:
mr1 = (mass_mm / mass_nn) ** 0.5
mr2 = (mass_oo / mass_nn) ** 0.5
mr3 = (mass_nn / mass_mm) ** 0.5
mr4 = (mass_oo / mass_mm) ** 0.5
else:
mr1 = 1.0
mr2 = 1.0
mr3 = 1.0
mr4 = 1.0
fr_n = (1 - C1_1 * C2_1 * mass_oo * mass_mm) * forces_n - C2_1 * (
C1_2 * mr1 * mass_oo * mass_nn * forces_m
- mr2 * mass_mm * mass_nn * forces_o
)
fr_m = (1 - C1_2 * C2_2 * mass_oo * mass_nn) * forces_m - C2_2 * (
C1_1 * mr3 * mass_oo * mass_mm * forces_n
- mr4 * mass_mm * mass_nn * forces_o
)
self.set_slices(fr_n, fr_m, 0.0, forces)
def get_slices(self, a):
a_n = a[self.n_ind]
a_m = a[self.m_ind]
a_o = a[self.o_ind]
return a_n, a_m, a_o
def set_slices(self, a_n, a_m, a_o, a):
a[self.n_ind] = a_n
a[self.m_ind] = a_m
a[self.o_ind] = a_o
def initialize_bond_lengths(self, atoms):
bondlengths = np.zeros((len(self.triples), 2))
for i in range(len(self.triples)):
bondlengths[i, 0] = atoms.get_distance(
self.n_ind[i], self.o_ind[i], mic=True
)
bondlengths[i, 1] = atoms.get_distance(
self.o_ind[i], self.m_ind[i], mic=True
)
return bondlengths
def get_indices(self):
return np.unique(self.triples.ravel())
def todict(self):
return {
'name': 'FixLinearTriatomic',
'kwargs': {'triples': self.triples.tolist()},
}
def index_shuffle(self, atoms, ind):
"""Shuffle the indices of the three atoms in this constraint"""
map = np.zeros(len(atoms), int)
map[ind] = 1
n = map.sum()
map[:] = -1
map[ind] = range(n)
triples = map[self.triples]
self.triples = triples[(triples != -1).all(1)]
if len(self.triples) == 0:
raise IndexError('Constraint not part of slice')