Coverage for ase / constraints / mirror_torque.py: 80.00%
55 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, slice2enlist
6class MirrorTorque(FixConstraint):
7 """Constraint object for mirroring the torque acting on a dihedral
8 angle defined by four atoms.
10 This class is designed to find a transition state with the help of a
11 single optimization. It can be used if the transition state belongs to a
12 cis-trans-isomerization with a change of dihedral angle. First the given
13 dihedral angle will be fixed until all other degrees of freedom are
14 optimized, then the torque acting on the dihedral angle will be mirrored
15 to find the transition state. Transition states in
16 dependence of the force can be obtained by stretching the molecule and
17 fixing its total length with *FixBondLength* or by using *ExternalForce*
18 during the optimization with *MirrorTorque*.
20 This constraint can be used to find
21 transition states of cis-trans-isomerization.
23 a1 a4
24 | |
25 a2 __ a3
27 Parameters
28 ----------
29 a1: int
30 First atom index.
31 a2: int
32 Second atom index.
33 a3: int
34 Third atom index.
35 a4: int
36 Fourth atom index.
37 max_angle: float
38 Upper limit of the dihedral angle interval where the transition state
39 can be found.
40 min_angle: float
41 Lower limit of the dihedral angle interval where the transition state
42 can be found.
43 fmax: float
44 Maximum force used for the optimization.
46 Notes
47 -----
48 You can combine this constraint for example with FixBondLength but make
49 sure that *MirrorTorque* comes first in the list if there are overlaps
50 between atom1-4 and atom5-6:
52 >>> from ase.build import bulk
54 >>> atoms = bulk('Cu', 'fcc', a=3.6)
55 >>> atom1, atom2, atom3, atom4, atom5, atom6 = atoms[:6]
56 >>> con1 = MirrorTorque(atom1, atom2, atom3, atom4)
57 >>> con2 = FixBondLength(atom5, atom6)
58 >>> atoms.set_constraint([con1, con2])
60 """
62 def __init__(
63 self, a1, a2, a3, a4, max_angle=2 * np.pi, min_angle=0.0, fmax=0.1
64 ):
65 self.indices = [a1, a2, a3, a4]
66 self.min_angle = min_angle
67 self.max_angle = max_angle
68 self.fmax = fmax
70 def adjust_positions(self, atoms, new):
71 pass
73 def adjust_forces(self, atoms, forces):
74 angle = atoms.get_dihedral(
75 self.indices[0], self.indices[1], self.indices[2], self.indices[3]
76 )
77 angle *= np.pi / 180.0
78 if (angle < self.min_angle) or (angle > self.max_angle):
79 # Stop structure optimization
80 forces[:] *= 0
81 return
82 p = atoms.positions[self.indices]
83 f = forces[self.indices]
85 f0 = (f[1] + f[2]) / 2.0
86 ff = f - f0
87 p0 = (p[2] + p[1]) / 2.0
88 m0 = np.cross(p[1] - p0, ff[1]) / (p[1] - p0).dot(p[1] - p0)
89 fff = ff - np.cross(m0, p - p0)
90 d1 = np.cross(np.cross(p[1] - p0, p[0] - p[1]), p[1] - p0) / (
91 p[1] - p0
92 ).dot(p[1] - p0)
93 d2 = np.cross(np.cross(p[2] - p0, p[3] - p[2]), p[2] - p0) / (
94 p[2] - p0
95 ).dot(p[2] - p0)
96 omegap1 = (np.cross(d1, fff[0]) / d1.dot(d1)).dot(
97 p[1] - p0
98 ) / np.linalg.norm(p[1] - p0)
99 omegap2 = (np.cross(d2, fff[3]) / d2.dot(d2)).dot(
100 p[2] - p0
101 ) / np.linalg.norm(p[2] - p0)
102 omegap = omegap1 + omegap2
103 con_saved = atoms.constraints
104 try:
105 con = [
106 con for con in con_saved if not isinstance(con, MirrorTorque)
107 ]
108 atoms.set_constraint(con)
109 forces_copy = atoms.get_forces()
110 finally:
111 atoms.set_constraint(con_saved)
112 df1 = (
113 -1
114 / 2.0
115 * omegap
116 * np.cross(p[1] - p0, d1)
117 / np.linalg.norm(p[1] - p0)
118 )
119 df2 = (
120 -1
121 / 2.0
122 * omegap
123 * np.cross(p[2] - p0, d2)
124 / np.linalg.norm(p[2] - p0)
125 )
126 forces_copy[self.indices] += (
127 df1,
128 [0.0, 0.0, 0.0],
129 [0.0, 0.0, 0.0],
130 df2,
131 )
132 # Check if forces would be converged if the dihedral angle with
133 # mirrored torque would also be fixed
134 if (forces_copy**2).sum(axis=1).max() < self.fmax**2:
135 factor = 1.0
136 else:
137 factor = 0.0
138 df1 = (
139 -(1 + factor)
140 / 2.0
141 * omegap
142 * np.cross(p[1] - p0, d1)
143 / np.linalg.norm(p[1] - p0)
144 )
145 df2 = (
146 -(1 + factor)
147 / 2.0
148 * omegap
149 * np.cross(p[2] - p0, d2)
150 / np.linalg.norm(p[2] - p0)
151 )
152 forces[self.indices] += (df1, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], df2)
154 def index_shuffle(self, atoms, ind):
155 # See docstring of superclass
156 indices = []
157 for new, old in slice2enlist(ind, len(atoms)):
158 if old in self.indices:
159 indices.append(new)
160 if len(indices) == 0:
161 raise IndexError('All indices in MirrorTorque not part of slice')
162 self.indices = np.asarray(indices, int)
164 def __repr__(self):
165 return 'MirrorTorque(%d, %d, %d, %d, %f, %f, %f)' % (
166 self.indices[0],
167 self.indices[1],
168 self.indices[2],
169 self.indices[3],
170 self.max_angle,
171 self.min_angle,
172 self.fmax,
173 )
175 def todict(self):
176 return {
177 'name': 'MirrorTorque',
178 'kwargs': {
179 'a1': self.indices[0],
180 'a2': self.indices[1],
181 'a3': self.indices[2],
182 'a4': self.indices[3],
183 'max_angle': self.max_angle,
184 'min_angle': self.min_angle,
185 'fmax': self.fmax,
186 },
187 }