Coverage for /builds/ericyuan00000/ase/ase/optimize/climbfixinternals.py: 98.51%

67 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-06-18 01:20 +0000

1# fmt: off 

2 

3from typing import IO, Any, Dict, List, Optional, Type, Union 

4 

5from numpy.linalg import norm 

6 

7from ase import Atoms 

8from ase.constraints import FixInternals 

9from ase.optimize.bfgs import BFGS 

10from ase.optimize.optimize import Optimizer 

11 

12 

13class BFGSClimbFixInternals(BFGS): 

14 """Class for transition state search and optimization 

15 

16 Climbs the 1D reaction coordinate defined as constrained internal coordinate 

17 via the :class:`~ase.constraints.FixInternals` class while minimizing all 

18 remaining degrees of freedom. 

19 

20 Details: Two optimizers, 'A' and 'B', are applied orthogonal to each other. 

21 Optimizer 'A' climbs the constrained coordinate while optimizer 'B' 

22 optimizes the remaining degrees of freedom after each climbing step. 

23 Optimizer 'A' uses the BFGS algorithm to climb along the projected force of 

24 the selected constraint. Optimizer 'B' can be any ASE optimizer 

25 (default: BFGS). 

26 

27 In combination with other constraints, the order of constraints matters. 

28 Generally, the FixInternals constraint should come first in the list of 

29 constraints. 

30 This has been tested with the :class:`~ase.constraints.FixAtoms` constraint. 

31 

32 Inspired by concepts described by P. N. Plessow. [1]_ 

33 

34 .. [1] Plessow, P. N., Efficient Transition State Optimization of Periodic 

35 Structures through Automated Relaxed Potential Energy Surface Scans. 

36 J. Chem. Theory Comput. 2018, 14 (2), 981–990. 

37 https://doi.org/10.1021/acs.jctc.7b01070. 

38 

39 .. note:: 

40 Convergence is based on 'fmax' of the total forces which is the sum of 

41 the projected forces and the forces of the remaining degrees of freedom. 

42 This value is logged in the 'logfile'. Optimizer 'B' logs 'fmax' of the 

43 remaining degrees of freedom without the projected forces. The projected 

44 forces can be inspected using the :meth:`get_projected_forces` method: 

45 

46 >>> for _ in dyn.irun(): 

47 ... projected_forces = dyn.get_projected_forces() 

48 

49 Example 

50 ------- 

51 .. literalinclude:: ../../ase/test/optimize/test_climb_fix_internals.py 

52 :end-before: # end example for documentation 

53 """ 

54 

55 def __init__( 

56 self, 

57 atoms: Atoms, 

58 restart: Optional[str] = None, 

59 logfile: Union[IO, str] = '-', 

60 trajectory: Optional[str] = None, 

61 maxstep: Optional[float] = None, 

62 alpha: Optional[float] = None, 

63 climb_coordinate: Optional[List[FixInternals]] = None, 

64 optB: Type[Optimizer] = BFGS, 

65 optB_kwargs: Optional[Dict[str, Any]] = None, 

66 optB_fmax: float = 0.05, 

67 optB_fmax_scaling: float = 0.0, 

68 **kwargs, 

69 ): 

70 """Allowed parameters are similar to the parent class 

71 :class:`~ase.optimize.bfgs.BFGS` with the following additions: 

72 

73 Parameters 

74 ---------- 

75 climb_coordinate: list 

76 Specifies which subconstraint of the 

77 :class:`~ase.constraints.FixInternals` constraint is to be climbed. 

78 Provide the corresponding nested list of indices 

79 (including coefficients in the case of Combo constraints). 

80 See the example above. 

81 

82 optB: any ASE optimizer, optional 

83 Optimizer 'B' for optimization of the remaining degrees of freedom 

84 after each climbing step. 

85 

86 optB_kwargs: dict, optional 

87 Specifies keyword arguments to be passed to optimizer 'B' at its 

88 initialization. By default, optimizer 'B' writes a logfile and 

89 trajectory (optB_{...}.log, optB_{...}.traj) where {...} is the 

90 current value of the ``climb_coordinate``. Set ``logfile`` to '-' 

91 for console output. Set ``trajectory`` to 'None' to suppress 

92 writing of the trajectory file. 

93 

94 optB_fmax: float, optional 

95 Specifies the convergence criterion 'fmax' of optimizer 'B'. 

96 

97 optB_fmax_scaling: float, optional 

98 Scaling factor to dynamically tighten 'fmax' of optimizer 'B' to 

99 the value of ``optB_fmax`` when close to convergence. 

100 Can speed up the climbing process. The scaling formula is 

101 

102 'fmax' = ``optB_fmax`` + ``optB_fmax_scaling`` 

103 :math:`\\cdot` norm_of_projected_forces 

104 

105 The final optimization with optimizer 'B' is 

106 performed with ``optB_fmax`` independent of ``optB_fmax_scaling``. 

107 """ 

108 self.targetvalue = None # may be assigned during restart in self.read() 

109 super().__init__(atoms, restart=restart, logfile=logfile, 

110 trajectory=trajectory, maxstep=maxstep, 

111 alpha=alpha, **kwargs) 

112 

113 self.constr2climb = get_constr2climb( 

114 self.optimizable.atoms, climb_coordinate) 

115 self.targetvalue = self.targetvalue or self.constr2climb.targetvalue 

116 

117 self.optB = optB 

118 self.optB_kwargs = optB_kwargs or {} 

119 self.optB_fmax = optB_fmax 

120 self.scaling = optB_fmax_scaling 

121 # log optimizer 'B' in logfiles named after current value of constraint 

122 self.autolog = 'logfile' not in self.optB_kwargs 

123 self.autotraj = 'trajectory' not in self.optB_kwargs 

124 

125 def read(self): 

126 (self.H, self.pos0, self.forces0, self.maxstep, 

127 self.targetvalue) = self.load() 

128 

129 def step(self): 

130 self.relax_remaining_dof() # optimization with optimizer 'B' 

131 

132 pos, dpos = self.pretend2climb() # with optimizer 'A' 

133 self.update_positions_and_targetvalue(pos, dpos) # obey other constr. 

134 

135 self.dump((self.H, self.pos0, self.forces0, self.maxstep, 

136 self.targetvalue)) 

137 

138 def pretend2climb(self): 

139 """Get directions for climbing and climb with optimizer 'A'.""" 

140 proj_forces = self.get_projected_forces() 

141 pos = self.optimizable.get_positions() 

142 dpos, steplengths = self.prepare_step(pos, proj_forces) 

143 dpos = self.determine_step(dpos, steplengths) 

144 return pos, dpos 

145 

146 def update_positions_and_targetvalue(self, pos, dpos): 

147 """Adjust constrained targetvalue of constraint and update positions.""" 

148 self.constr2climb.adjust_positions(pos, pos + dpos) # update sigma 

149 self.targetvalue += self.constr2climb.sigma # climb constraint 

150 self.constr2climb.targetvalue = self.targetvalue # adjust positions 

151 self.optimizable.set_positions( 

152 self.optimizable.get_positions()) # to targetvalue 

153 

154 def relax_remaining_dof(self): 

155 """Optimize remaining degrees of freedom with optimizer 'B'.""" 

156 if self.autolog: 

157 self.optB_kwargs['logfile'] = f'optB_{self.targetvalue}.log' 

158 if self.autotraj: 

159 self.optB_kwargs['trajectory'] = f'optB_{self.targetvalue}.traj' 

160 fmax = self.get_scaled_fmax() 

161 with self.optB(self.optimizable.atoms, **self.optB_kwargs) as opt: 

162 opt.run(fmax) # optimize with scaled fmax 

163 if self.converged() and fmax > self.optB_fmax: 

164 # (final) optimization with desired fmax 

165 opt.run(self.optB_fmax) 

166 

167 def get_scaled_fmax(self): 

168 """Return the adaptive 'fmax' based on the estimated distance to the 

169 transition state.""" 

170 return (self.optB_fmax + 

171 self.scaling * norm(self.constr2climb.projected_forces)) 

172 

173 def get_projected_forces(self): 

174 """Return the projected forces along the constrained coordinate in 

175 uphill direction (negative sign).""" 

176 forces = self.constr2climb.projected_forces 

177 forces = -forces.reshape(self.optimizable.get_positions().shape) 

178 return forces 

179 

180 def get_total_forces(self): 

181 """Return forces obeying all constraints plus projected forces.""" 

182 return self.optimizable.get_forces() + self.get_projected_forces() 

183 

184 def converged(self, forces=None): 

185 """Did the optimization converge based on the total forces?""" 

186 forces = forces or self.get_total_forces() 

187 return super().converged(forces=forces) 

188 

189 def log(self, forces=None): 

190 forces = forces or self.get_total_forces() 

191 super().log(forces=forces) 

192 

193 

194def get_fixinternals(atoms): 

195 """Get pointer to the FixInternals constraint on the atoms object.""" 

196 all_constr_types = list(map(type, atoms.constraints)) 

197 index = all_constr_types.index(FixInternals) # locate constraint 

198 return atoms.constraints[index] 

199 

200 

201def get_constr2climb(atoms, climb_coordinate): 

202 """Get pointer to the subconstraint that is to be climbed. 

203 Identification by its definition via indices (and coefficients).""" 

204 constr = get_fixinternals(atoms) 

205 return constr.get_subconstraint(atoms, climb_coordinate)