Coverage for /builds/debichem-team/python-ase/ase/optimize/cellawarebfgs.py: 98.57%

70 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1import time 

2from typing import IO, Optional, Union 

3 

4import numpy as np 

5 

6from ase import Atoms 

7from ase.geometry import cell_to_cellpar 

8from ase.optimize import BFGS 

9from ase.optimize.optimize import Dynamics 

10from ase.units import GPa 

11 

12 

13def calculate_isotropic_elasticity_tensor(bulk_modulus, poisson_ratio, 

14 suppress_rotation=0): 

15 """ 

16 Parameters: 

17 bulk_modulus Bulk Modulus of the isotropic system used to set up the 

18 Hessian (in ASE units (eV/Å^3)). 

19 

20 poisson_ratio Poisson ratio of the isotropic system used to set up the 

21 initial Hessian (unitless, between -1 and 0.5). 

22 

23 suppress_rotation The rank-2 matrix C_ijkl.reshape((9,9)) has by 

24 default 6 non-zero eigenvalues, because energy is 

25 invariant to orthonormal rotations of the cell 

26 vector. This serves as a bad initial Hessian due to 3 

27 zero eigenvalues. Suppress rotation sets a value for 

28 those zero eigenvalues. 

29 

30 Returns C_ijkl 

31 """ 

32 

33 # https://scienceworld.wolfram.com/physics/LameConstants.html 

34 _lambda = 3 * bulk_modulus * poisson_ratio / (1 + 1 * poisson_ratio) 

35 _mu = _lambda * (1 - 2 * poisson_ratio) / (2 * poisson_ratio) 

36 

37 # https://en.wikipedia.org/wiki/Elasticity_tensor 

38 g_ij = np.eye(3) 

39 

40 # Construct 4th rank Elasticity tensor for isotropic systems 

41 C_ijkl = _lambda * np.einsum('ij,kl->ijkl', g_ij, g_ij) 

42 C_ijkl += _mu * (np.einsum('ik,jl->ijkl', g_ij, g_ij) + 

43 np.einsum('il,kj->ijkl', g_ij, g_ij)) 

44 

45 # Supplement the tensor with suppression of pure rotations that are right 

46 # now 0 eigenvalues. 

47 # Loop over all basis vectors of skew symmetric real matrix 

48 for i, j in ((0, 1), (0, 2), (1, 2)): 

49 Q = np.zeros((3, 3)) 

50 Q[i, j], Q[j, i] = 1, -1 

51 C_ijkl += (np.einsum('ij,kl->ijkl', Q, Q) 

52 * suppress_rotation / 2) 

53 

54 return C_ijkl 

55 

56 

57class CellAwareBFGS(BFGS): 

58 def __init__( 

59 self, 

60 atoms: Atoms, 

61 restart: Optional[str] = None, 

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

63 trajectory: Optional[str] = None, 

64 append_trajectory: bool = False, 

65 maxstep: Optional[float] = None, 

66 bulk_modulus: Optional[float] = 145 * GPa, 

67 poisson_ratio: Optional[float] = 0.3, 

68 alpha: Optional[float] = None, 

69 long_output: Optional[bool] = False, 

70 **kwargs, 

71 ): 

72 self.bulk_modulus = bulk_modulus 

73 self.poisson_ratio = poisson_ratio 

74 self.long_output = long_output 

75 BFGS.__init__(self, atoms=atoms, restart=restart, logfile=logfile, 

76 trajectory=trajectory, maxstep=maxstep, 

77 alpha=alpha, append_trajectory=append_trajectory, 

78 **kwargs) 

79 assert not isinstance(atoms, Atoms) 

80 if hasattr(atoms, 'exp_cell_factor'): 

81 assert atoms.exp_cell_factor == 1.0 

82 

83 def initialize(self): 

84 BFGS.initialize(self) 

85 C_ijkl = calculate_isotropic_elasticity_tensor( 

86 self.bulk_modulus, 

87 self.poisson_ratio, 

88 suppress_rotation=self.alpha) 

89 cell_H = self.H0[-9:, -9:] 

90 ind = np.where(self.atoms.mask.ravel() != 0)[0] 

91 cell_H[np.ix_(ind, ind)] = C_ijkl.reshape((9, 9))[ 

92 np.ix_(ind, ind)] * self.atoms.atoms.cell.volume 

93 

94 def converged(self, forces=None): 

95 if forces is None: 

96 forces = self.atoms.atoms.get_forces() 

97 stress = self.atoms.atoms.get_stress(voigt=False) * self.atoms.mask 

98 return np.max(np.sum(forces**2, axis=1))**0.5 < self.fmax and \ 

99 np.max(np.abs(stress)) < self.smax 

100 

101 def run(self, fmax=0.05, smax=0.005, steps=None): 

102 """ call Dynamics.run and keep track of fmax""" 

103 self.fmax = fmax 

104 self.smax = smax 

105 if steps is not None: 

106 self.max_steps = steps 

107 return Dynamics.run(self) 

108 

109 def log(self, forces=None): 

110 if forces is None: 

111 forces = self.atoms.atoms.get_forces() 

112 fmax = (forces ** 2).sum(axis=1).max() ** 0.5 

113 e = self.optimizable.get_potential_energy() 

114 T = time.localtime() 

115 smax = abs(self.atoms.atoms.get_stress(voigt=False) * 

116 self.atoms.mask).max() 

117 volume = self.atoms.atoms.cell.volume 

118 if self.logfile is not None: 

119 name = self.__class__.__name__ 

120 if self.nsteps == 0: 

121 args = (" " * len(name), 

122 "Step", "Time", "Energy", "fmax", "smax", "volume") 

123 msg = "\n%s %4s %8s %15s %15s %15s %15s" % args 

124 if self.long_output: 

125 msg += ("%8s %8s %8s %8s %8s %8s" % 

126 ('A', 'B', 'C', 'α', 'β', 'γ')) 

127 msg += '\n' 

128 self.logfile.write(msg) 

129 

130 ast = '' 

131 args = (name, self.nsteps, T[3], T[4], T[5], e, ast, fmax, smax, 

132 volume) 

133 msg = ("%s: %3d %02d:%02d:%02d %15.6f%1s %15.6f %15.6f %15.6f" % 

134 args) 

135 if self.long_output: 

136 msg += ("%8.3f %8.3f %8.3f %8.3f %8.3f %8.3f" % 

137 tuple(cell_to_cellpar(self.atoms.atoms.cell))) 

138 msg += '\n' 

139 self.logfile.write(msg) 

140 

141 self.logfile.flush()