Coverage for /builds/kinetik161/ase/ase/md/md.py: 82.69%

52 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1"""Molecular Dynamics.""" 

2import warnings 

3from typing import IO, Optional, Union 

4 

5import numpy as np 

6 

7from ase import Atoms, units 

8from ase.io.trajectory import Trajectory 

9from ase.md.logger import MDLogger 

10from ase.optimize.optimize import Dynamics 

11 

12 

13def process_temperature( 

14 temperature: Optional[float], 

15 temperature_K: Optional[float], 

16 orig_unit: str, 

17) -> float: 

18 """Handle that temperature can be specified in multiple units. 

19 

20 For at least a transition period, molecular dynamics in ASE can 

21 have the temperature specified in either Kelvin or Electron 

22 Volt. The different MD algorithms had different defaults, by 

23 forcing the user to explicitly choose a unit we can resolve 

24 this. Using the original method then will issue a 

25 FutureWarning. 

26 

27 Four parameters: 

28 

29 temperature: None or float 

30 The original temperature specification in whatever unit was 

31 historically used. A warning is issued if this is not None and 

32 the historical unit was eV. 

33 

34 temperature_K: None or float 

35 Temperature in Kelvin. 

36 

37 orig_unit: str 

38 Unit used for the `temperature`` parameter. Must be 'K' or 'eV'. 

39 

40 Exactly one of the two temperature parameters must be different from 

41 None, otherwise an error is issued. 

42 

43 Return value: Temperature in Kelvin. 

44 """ 

45 if (temperature is not None) + (temperature_K is not None) != 1: 

46 raise TypeError("Exactly one of the parameters 'temperature'," 

47 + " and 'temperature_K', must be given") 

48 if temperature is not None: 

49 w = "Specify the temperature in K using the 'temperature_K' argument" 

50 if orig_unit == 'K': 

51 return temperature 

52 elif orig_unit == 'eV': 

53 warnings.warn(FutureWarning(w)) 

54 return temperature / units.kB 

55 else: 

56 raise ValueError("Unknown temperature unit " + orig_unit) 

57 

58 assert temperature_K is not None 

59 return temperature_K 

60 

61 

62class MolecularDynamics(Dynamics): 

63 """Base-class for all MD classes.""" 

64 

65 def __init__( 

66 self, 

67 atoms: Atoms, 

68 timestep: float, 

69 trajectory: Optional[str] = None, 

70 logfile: Optional[Union[IO, str]] = None, 

71 loginterval: int = 1, 

72 append_trajectory: bool = False, 

73 ): 

74 """Molecular Dynamics object. 

75 

76 Parameters: 

77 

78 atoms: Atoms object 

79 The Atoms object to operate on. 

80 

81 timestep: float 

82 The time step in ASE time units. 

83 

84 trajectory: Trajectory object or str 

85 Attach trajectory object. If *trajectory* is a string a 

86 Trajectory will be constructed. Use *None* for no 

87 trajectory. 

88 

89 logfile: file object or str (optional) 

90 If *logfile* is a string, a file with that name will be opened. 

91 Use '-' for stdout. 

92 

93 loginterval: int (optional) 

94 Only write a log line for every *loginterval* time steps. 

95 Default: 1 

96 

97 append_trajectory: boolean (optional) 

98 Defaults to False, which causes the trajectory file to be 

99 overwriten each time the dynamics is restarted from scratch. 

100 If True, the new structures are appended to the trajectory 

101 file instead. 

102 """ 

103 # dt as to be attached _before_ parent class is initialized 

104 self.dt = timestep 

105 

106 super().__init__(atoms, logfile=None, trajectory=None) 

107 

108 # Some codes (e.g. Asap) may be using filters to 

109 # constrain atoms or do other things. Current state of the art 

110 # is that "atoms" must be either Atoms or Filter in order to 

111 # work with dynamics. 

112 # 

113 # In the future, we should either use a special role interface 

114 # for MD, or we should ensure that the input is *always* a Filter. 

115 # That way we won't need to test multiple cases. Currently, 

116 # we do not test /any/ kind of MD with any kind of Filter in ASE. 

117 self.atoms = atoms 

118 self.masses = self.atoms.get_masses() 

119 

120 if 0 in self.masses: 

121 warnings.warn('Zero mass encountered in atoms; this will ' 

122 'likely lead to errors if the massless atoms ' 

123 'are unconstrained.') 

124 

125 self.masses.shape = (-1, 1) 

126 

127 if not self.atoms.has('momenta'): 

128 self.atoms.set_momenta(np.zeros([len(self.atoms), 3])) 

129 

130 # Trajectory is attached here instead of in Dynamics.__init__ 

131 # to respect the loginterval argument. 

132 if trajectory is not None: 

133 if isinstance(trajectory, str): 

134 mode = "a" if append_trajectory else "w" 

135 trajectory = self.closelater( 

136 Trajectory(trajectory, mode=mode, atoms=atoms) 

137 ) 

138 self.attach(trajectory, interval=loginterval) 

139 

140 if logfile: 

141 logger = self.closelater( 

142 MDLogger(dyn=self, atoms=atoms, logfile=logfile)) 

143 self.attach(logger, loginterval) 

144 

145 def todict(self): 

146 return {'type': 'molecular-dynamics', 

147 'md-type': self.__class__.__name__, 

148 'timestep': self.dt} 

149 

150 def irun(self, steps=50): 

151 """Run molecular dynamics algorithm as a generator. 

152 

153 Parameters 

154 ---------- 

155 steps : int, default=DEFAULT_MAX_STEPS 

156 Number of molecular dynamics steps to be run. 

157 

158 Yields 

159 ------ 

160 converged : bool 

161 True if the maximum number of steps are reached. 

162 """ 

163 return Dynamics.irun(self, steps=steps) 

164 

165 def run(self, steps=50): 

166 """Run molecular dynamics algorithm. 

167 

168 Parameters 

169 ---------- 

170 steps : int, default=DEFAULT_MAX_STEPS 

171 Number of molecular dynamics steps to be run. 

172 

173 Returns 

174 ------- 

175 converged : bool 

176 True if the maximum number of steps are reached. 

177 """ 

178 return Dynamics.run(self, steps=steps) 

179 

180 def get_time(self): 

181 return self.nsteps * self.dt 

182 

183 def converged(self): 

184 """ MD is 'converged' when number of maximum steps is reached. """ 

185 return self.nsteps >= self.max_steps 

186 

187 def _get_com_velocity(self, velocity): 

188 """Return the center of mass velocity. 

189 Internal use only. This function can be reimplemented by Asap. 

190 """ 

191 return np.dot(self.masses.ravel(), velocity) / self.masses.sum() 

192 

193 # Make the process_temperature function available to subclasses 

194 # as a static method. This makes it easy for MD objects to use 

195 # it, while functions in md.velocitydistribution have access to it 

196 # as a function. 

197 _process_temperature = staticmethod(process_temperature)