Coverage for ase / constraints / mirror_force.py: 73.33%
45 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 MirrorForce(FixConstraint):
7 """Constraint object for mirroring the force between two atoms.
9 This class is designed to find a transition state with the help of a
10 single optimization. It can be used if the transition state belongs to a
11 bond breaking reaction. First the given bond length will be fixed until
12 all other degrees of freedom are optimized, then the forces of the two
13 atoms will be mirrored to find the transition state. The mirror plane is
14 perpendicular to the connecting line of the atoms. Transition states in
15 dependence of the force can be obtained by stretching the molecule and
16 fixing its total length with *FixBondLength* or by using *ExternalForce*
17 during the optimization with *MirrorForce*.
19 Parameters
20 ----------
21 a1: int
22 First atom index.
23 a2: int
24 Second atom index.
25 max_dist: float
26 Upper limit of the bond length interval where the transition state
27 can be found.
28 min_dist: float
29 Lower limit of the bond length interval where the transition state
30 can be found.
31 fmax: float
32 Maximum force used for the optimization.
34 Notes
35 -----
36 You can combine this constraint for example with FixBondLength but make
37 sure that *MirrorForce* comes first in the list if there are overlaps
38 between atom1-2 and atom3-4:
40 >>> from ase.build import bulk
42 >>> atoms = bulk('Cu', 'fcc', a=3.6)
43 >>> atom1, atom2, atom3, atom4 = atoms[:4]
44 >>> con1 = MirrorForce(atom1, atom2)
45 >>> con2 = FixBondLength(atom3, atom4)
46 >>> atoms.set_constraint([con1, con2])
48 """
50 def __init__(self, a1, a2, max_dist=2.5, min_dist=1.0, fmax=0.1):
51 self.indices = [a1, a2]
52 self.min_dist = min_dist
53 self.max_dist = max_dist
54 self.fmax = fmax
56 def adjust_positions(self, atoms, new):
57 pass
59 def adjust_forces(self, atoms, forces):
60 dist = np.subtract.reduce(atoms.positions[self.indices])
61 d = np.linalg.norm(dist)
62 if (d < self.min_dist) or (d > self.max_dist):
63 # Stop structure optimization
64 forces[:] *= 0
65 return
66 dist /= d
67 df = np.subtract.reduce(forces[self.indices])
68 f = df.dot(dist)
69 con_saved = atoms.constraints
70 try:
71 con = [con for con in con_saved if not isinstance(con, MirrorForce)]
72 atoms.set_constraint(con)
73 forces_copy = atoms.get_forces()
74 finally:
75 atoms.set_constraint(con_saved)
76 df1 = -1 / 2.0 * f * dist
77 forces_copy[self.indices] += (df1, -df1)
78 # Check if forces would be converged if the bond with mirrored forces
79 # would also be fixed
80 if (forces_copy**2).sum(axis=1).max() < self.fmax**2:
81 factor = 1.0
82 else:
83 factor = 0.0
84 df1 = -(1 + factor) / 2.0 * f * dist
85 forces[self.indices] += (df1, -df1)
87 def index_shuffle(self, atoms, ind):
88 """Shuffle the indices of the two atoms in this constraint"""
89 newa = [-1, -1] # Signal error
90 for new, old in slice2enlist(ind, len(atoms)):
91 for i, a in enumerate(self.indices):
92 if old == a:
93 newa[i] = new
94 if newa[0] == -1 or newa[1] == -1:
95 raise IndexError('Constraint not part of slice')
96 self.indices = newa
98 def __repr__(self):
99 return 'MirrorForce(%d, %d, %f, %f, %f)' % (
100 self.indices[0],
101 self.indices[1],
102 self.max_dist,
103 self.min_dist,
104 self.fmax,
105 )
107 def todict(self):
108 return {
109 'name': 'MirrorForce',
110 'kwargs': {
111 'a1': self.indices[0],
112 'a2': self.indices[1],
113 'max_dist': self.max_dist,
114 'min_dist': self.min_dist,
115 'fmax': self.fmax,
116 },
117 }