Coverage for ase / constraints / fix_linear_triatomic.py: 97.47%
158 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1import numpy as np
3from ase.constraints.constraint import FixConstraint
4from ase.geometry import find_mic, wrap_positions
7class FixLinearTriatomic(FixConstraint):
8 """Holonomic constraints for rigid linear triatomic molecules."""
10 def __init__(self, triples):
11 """Apply RATTLE-type bond constraints between outer atoms n and m
12 and linear vectorial constraints to the position of central
13 atoms o to fix the geometry of linear triatomic molecules of the
14 type:
16 n--o--m
18 Parameters
19 ----------
21 triples: list
22 Indices of the atoms forming the linear molecules to constrain
23 as triples. Sequence should be (n, o, m) or (m, o, n).
25 When using these constraints in molecular dynamics or structure
26 optimizations, atomic forces need to be redistributed within a
27 triple. The function redistribute_forces_optimization implements
28 the redistribution of forces for structure optimization, while
29 the function redistribute_forces_md implements the redistribution
30 for molecular dynamics.
32 References
33 ----------
35 Ciccotti et al. Molecular Physics 47 (1982)
36 :doi:`10.1080/00268978200100942`
37 """
38 self.triples = np.asarray(triples)
39 if self.triples.shape[1] != 3:
40 raise ValueError('"triples" has wrong size')
41 self.bondlengths = None
43 def get_removed_dof(self, atoms):
44 return 4 * len(self.triples)
46 @property
47 def n_ind(self):
48 return self.triples[:, 0]
50 @property
51 def m_ind(self):
52 return self.triples[:, 2]
54 @property
55 def o_ind(self):
56 return self.triples[:, 1]
58 def initialize(self, atoms):
59 masses = atoms.get_masses()
60 self.mass_n, self.mass_m, self.mass_o = self.get_slices(masses)
62 self.bondlengths = self.initialize_bond_lengths(atoms)
63 self.bondlengths_nm = self.bondlengths.sum(axis=1)
65 C1 = self.bondlengths[:, ::-1] / self.bondlengths_nm[:, None]
66 C2 = (
67 C1[:, 0] ** 2 * self.mass_o * self.mass_m
68 + C1[:, 1] ** 2 * self.mass_n * self.mass_o
69 + self.mass_n * self.mass_m
70 )
71 C2 = C1 / C2[:, None]
72 C3 = self.mass_n * C1[:, 1] - self.mass_m * C1[:, 0]
73 C3 = C2 * self.mass_o[:, None] * C3[:, None]
74 C3[:, 1] *= -1
75 C3 = (C3 + 1) / np.vstack((self.mass_n, self.mass_m)).T
76 C4 = C1[:, 0] ** 2 + C1[:, 1] ** 2 + 1
77 C4 = C1 / C4[:, None]
79 self.C1 = C1
80 self.C2 = C2
81 self.C3 = C3
82 self.C4 = C4
84 def adjust_positions(self, atoms, new):
85 old = atoms.positions
86 new_n, new_m, new_o = self.get_slices(new)
88 if self.bondlengths is None:
89 self.initialize(atoms)
91 r0 = old[self.n_ind] - old[self.m_ind]
92 d0, _ = find_mic(r0, atoms.cell, atoms.pbc)
93 d1 = new_n - new_m - r0 + d0
94 a = np.einsum('ij,ij->i', d0, d0)
95 b = np.einsum('ij,ij->i', d1, d0)
96 c = np.einsum('ij,ij->i', d1, d1) - self.bondlengths_nm**2
97 g = (b - (b**2 - a * c) ** 0.5) / (a * self.C3.sum(axis=1))
98 g = g[:, None] * self.C3
99 new_n -= g[:, 0, None] * d0
100 new_m += g[:, 1, None] * d0
101 if np.allclose(d0, r0):
102 new_o = self.C1[:, 0, None] * new_n + self.C1[:, 1, None] * new_m
103 else:
104 v1, _ = find_mic(new_n, atoms.cell, atoms.pbc)
105 v2, _ = find_mic(new_m, atoms.cell, atoms.pbc)
106 rb = self.C1[:, 0, None] * v1 + self.C1[:, 1, None] * v2
107 new_o = wrap_positions(rb, atoms.cell, atoms.pbc)
109 self.set_slices(new_n, new_m, new_o, new)
111 def adjust_momenta(self, atoms, p):
112 old = atoms.positions
113 p_n, p_m, p_o = self.get_slices(p)
115 if self.bondlengths is None:
116 self.initialize(atoms)
118 mass_nn = self.mass_n[:, None]
119 mass_mm = self.mass_m[:, None]
120 mass_oo = self.mass_o[:, None]
122 d = old[self.n_ind] - old[self.m_ind]
123 d, _ = find_mic(d, atoms.cell, atoms.pbc)
124 dv = p_n / mass_nn - p_m / mass_mm
125 k = np.einsum('ij,ij->i', dv, d) / self.bondlengths_nm**2
126 k = self.C3 / (self.C3.sum(axis=1)[:, None]) * k[:, None]
127 p_n -= k[:, 0, None] * mass_nn * d
128 p_m += k[:, 1, None] * mass_mm * d
129 p_o = mass_oo * (
130 self.C1[:, 0, None] * p_n / mass_nn
131 + self.C1[:, 1, None] * p_m / mass_mm
132 )
134 self.set_slices(p_n, p_m, p_o, p)
136 def adjust_forces(self, atoms, forces):
137 if self.bondlengths is None:
138 self.initialize(atoms)
140 A = self.C4 * np.diff(self.C1)
141 A[:, 0] *= -1
142 A -= 1
143 B = np.diff(self.C4) / (A.sum(axis=1))[:, None]
144 A /= (A.sum(axis=1))[:, None]
146 self.constraint_forces = -forces
147 old = atoms.positions
149 fr_n, fr_m, fr_o = self.redistribute_forces_optimization(forces)
151 d = old[self.n_ind] - old[self.m_ind]
152 d, _ = find_mic(d, atoms.cell, atoms.pbc)
153 df = fr_n - fr_m
154 k = -np.einsum('ij,ij->i', df, d) / self.bondlengths_nm**2
155 forces[self.n_ind] = fr_n + k[:, None] * d * A[:, 0, None]
156 forces[self.m_ind] = fr_m - k[:, None] * d * A[:, 1, None]
157 forces[self.o_ind] = fr_o + k[:, None] * d * B
159 self.constraint_forces += forces
161 def redistribute_forces_optimization(self, forces):
162 """Redistribute forces within a triple when performing structure
163 optimizations.
165 The redistributed forces needs to be further adjusted using the
166 appropriate Lagrange multipliers as implemented in adjust_forces."""
167 forces_n, forces_m, forces_o = self.get_slices(forces)
168 C1_1 = self.C1[:, 0, None]
169 C1_2 = self.C1[:, 1, None]
170 C4_1 = self.C4[:, 0, None]
171 C4_2 = self.C4[:, 1, None]
173 fr_n = (1 - C4_1 * C1_1) * forces_n - C4_1 * (
174 C1_2 * forces_m - forces_o
175 )
176 fr_m = (1 - C4_2 * C1_2) * forces_m - C4_2 * (
177 C1_1 * forces_n - forces_o
178 )
179 fr_o = (
180 (1 - 1 / (C1_1**2 + C1_2**2 + 1)) * forces_o
181 + C4_1 * forces_n
182 + C4_2 * forces_m
183 )
185 return fr_n, fr_m, fr_o
187 def redistribute_forces_md(self, atoms, forces, rand=False):
188 """Redistribute forces within a triple when performing molecular
189 dynamics.
191 When rand=True, use the equations for random force terms, as
192 used e.g. by Langevin dynamics, otherwise apply the standard
193 equations for deterministic forces (see Ciccotti et al. Molecular
194 Physics 47 (1982))."""
195 if self.bondlengths is None:
196 self.initialize(atoms)
197 forces_n, forces_m, forces_o = self.get_slices(forces)
198 C1_1 = self.C1[:, 0, None]
199 C1_2 = self.C1[:, 1, None]
200 C2_1 = self.C2[:, 0, None]
201 C2_2 = self.C2[:, 1, None]
202 mass_nn = self.mass_n[:, None]
203 mass_mm = self.mass_m[:, None]
204 mass_oo = self.mass_o[:, None]
205 if rand:
206 mr1 = (mass_mm / mass_nn) ** 0.5
207 mr2 = (mass_oo / mass_nn) ** 0.5
208 mr3 = (mass_nn / mass_mm) ** 0.5
209 mr4 = (mass_oo / mass_mm) ** 0.5
210 else:
211 mr1 = 1.0
212 mr2 = 1.0
213 mr3 = 1.0
214 mr4 = 1.0
216 fr_n = (1 - C1_1 * C2_1 * mass_oo * mass_mm) * forces_n - C2_1 * (
217 C1_2 * mr1 * mass_oo * mass_nn * forces_m
218 - mr2 * mass_mm * mass_nn * forces_o
219 )
221 fr_m = (1 - C1_2 * C2_2 * mass_oo * mass_nn) * forces_m - C2_2 * (
222 C1_1 * mr3 * mass_oo * mass_mm * forces_n
223 - mr4 * mass_mm * mass_nn * forces_o
224 )
226 self.set_slices(fr_n, fr_m, 0.0, forces)
228 def get_slices(self, a):
229 a_n = a[self.n_ind]
230 a_m = a[self.m_ind]
231 a_o = a[self.o_ind]
233 return a_n, a_m, a_o
235 def set_slices(self, a_n, a_m, a_o, a):
236 a[self.n_ind] = a_n
237 a[self.m_ind] = a_m
238 a[self.o_ind] = a_o
240 def initialize_bond_lengths(self, atoms):
241 bondlengths = np.zeros((len(self.triples), 2))
243 for i in range(len(self.triples)):
244 bondlengths[i, 0] = atoms.get_distance(
245 self.n_ind[i], self.o_ind[i], mic=True
246 )
247 bondlengths[i, 1] = atoms.get_distance(
248 self.o_ind[i], self.m_ind[i], mic=True
249 )
251 return bondlengths
253 def get_indices(self):
254 return np.unique(self.triples.ravel())
256 def todict(self):
257 return {
258 'name': 'FixLinearTriatomic',
259 'kwargs': {'triples': self.triples.tolist()},
260 }
262 def index_shuffle(self, atoms, ind):
263 """Shuffle the indices of the three atoms in this constraint"""
264 map = np.zeros(len(atoms), int)
265 map[ind] = 1
266 n = map.sum()
267 map[:] = -1
268 map[ind] = range(n)
269 triples = map[self.triples]
270 self.triples = triples[(triples != -1).all(1)]
271 if len(self.triples) == 0:
272 raise IndexError('Constraint not part of slice')