Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from ase.calculators.calculator import Calculator, all_changes 

2from ase.io.trajectory import Trajectory 

3from ase.parallel import broadcast 

4from ase.parallel import world 

5import numpy as np 

6from os.path import exists 

7from ase.units import fs, mol, kJ, nm 

8 

9 

10def restart_from_trajectory(prev_traj, *args, prev_steps=None, atoms=None, 

11 **kwargs): 

12 """This function helps the user to restart a plumed simulation 

13 from a trajectory file. 

14 

15 Parameters 

16 ---------- 

17 prev_traj : Trajectory object 

18 previous simulated trajectory 

19 

20 prev_steps : int. Default steps in prev_traj. 

21 number of previous steps 

22 

23 others : 

24 Same parameters of :mod:`~ase.calculators.plumed` calculator 

25 

26 Returns 

27 ------- 

28 Plumed calculator 

29 

30 

31 .. note:: prev_steps is crucial when trajectory does not contain 

32 all the previous steps. 

33 """ 

34 atoms.calc = Plumed(*args, atoms=atoms, restart=True, **kwargs) 

35 

36 with Trajectory(prev_traj) as traj: 

37 if prev_steps is None: 

38 atoms.calc.istep = len(traj) - 1 

39 else: 

40 atoms.calc.istep = prev_steps 

41 atoms.set_positions(traj[-1].get_positions()) 

42 atoms.set_momenta(traj[-1].get_momenta()) 

43 return atoms.calc 

44 

45 

46class Plumed(Calculator): 

47 implemented_properties = ['energy', 'forces'] 

48 

49 def __init__(self, calc, input, timestep, atoms=None, kT=1., log='', 

50 restart=False, use_charge=False, update_charge=False): 

51 """ 

52 Plumed calculator is used for simulations of enhanced sampling methods 

53 with the open-source code PLUMED (plumed.org). 

54 

55 [1] The PLUMED consortium, Nat. Methods 16, 670 (2019) 

56 [2] Tribello, Bonomi, Branduardi, Camilloni, and Bussi, 

57 Comput. Phys. Commun. 185, 604 (2014) 

58 

59 Parameters 

60 ---------- 

61 calc: Calculator object 

62 It computes the unbiased forces 

63 

64 input: List of strings 

65 It contains the setup of plumed actions 

66 

67 timestep: float 

68 Time step of the simulated dynamics 

69 

70 atoms: Atoms 

71 Atoms object to be attached 

72 

73 kT: float. Default 1. 

74 Value of the thermal energy in eV units. It is important for 

75 some methods of plumed like Well-Tempered Metadynamics. 

76 

77 log: string 

78 Log file of the plumed calculations 

79 

80 restart: boolean. Default False 

81 True if the simulation is restarted. 

82 

83 use_charge: boolean. Default False 

84 True if you use some collective variable which needs charges. If 

85 use_charges is True and update_charge is False, you have to define 

86 initial charges and then this charge will be used during all 

87 simulation. 

88 

89 update_charge: boolean. Default False 

90 True if you want the charges to be updated each time step. This 

91 will fail in case that calc does not have 'charges' in its 

92 properties. 

93 

94 

95 .. note:: For this case, the calculator is defined strictly with the 

96 object atoms inside. This is necessary for initializing the 

97 Plumed object. For conserving ASE convention, it can be initialized 

98 as atoms.calc = (..., atoms=atoms, ...) 

99 

100 

101 .. note:: In order to guarantee a proper restart, the user has to fix 

102 momenta, positions and Plumed.istep, where the positions and 

103 momenta corresponds to the last configuration in the previous 

104 simulation, while Plumed.istep is the number of timesteps 

105 performed previously. This can be done using 

106 ase.calculators.plumed.restart_from_trajectory. 

107 """ 

108 

109 from plumed import Plumed as pl 

110 

111 if atoms is None: 

112 raise TypeError('plumed calculator has to be defined with the \ 

113 object atoms inside.') 

114 

115 self.istep = 0 

116 Calculator.__init__(self, atoms=atoms) 

117 

118 self.input = input 

119 self.calc = calc 

120 self.use_charge = use_charge 

121 self.update_charge = update_charge 

122 

123 if world.rank == 0: 

124 natoms = len(atoms.get_positions()) 

125 self.plumed = pl() 

126 

127 ''' Units setup 

128 warning: inputs and outputs of plumed will still be in 

129 plumed units. 

130 

131 The change of Plumed units to ASE units is: 

132 kjoule/mol to eV 

133 nm to Angstrom 

134 ps to ASE time units 

135 ASE and plumed - charge unit is in e units 

136 ASE and plumed - mass unit is in a.m.u units ''' 

137 

138 ps = 1000 * fs 

139 self.plumed.cmd("setMDEnergyUnits", mol / kJ) 

140 self.plumed.cmd("setMDLengthUnits", 1 / nm) 

141 self.plumed.cmd("setMDTimeUnits", 1 / ps) 

142 self.plumed.cmd("setMDChargeUnits", 1.) 

143 self.plumed.cmd("setMDMassUnits", 1.) 

144 

145 self.plumed.cmd("setNatoms", natoms) 

146 self.plumed.cmd("setMDEngine", "ASE") 

147 self.plumed.cmd("setLogFile", log) 

148 self.plumed.cmd("setTimestep", float(timestep)) 

149 self.plumed.cmd("setRestart", restart) 

150 self.plumed.cmd("setKbT", float(kT)) 

151 self.plumed.cmd("init") 

152 for line in input: 

153 self.plumed.cmd("readInputLine", line) 

154 self.atoms = atoms 

155 

156 def _get_name(self): 

157 return f'{self.calc.name}+Plumed' 

158 

159 def calculate(self, atoms=None, properties=['energy', 'forces'], 

160 system_changes=all_changes): 

161 Calculator.calculate(self, atoms, properties, system_changes) 

162 

163 comp = self.compute_energy_and_forces(self.atoms.get_positions(), 

164 self.istep) 

165 energy, forces = comp 

166 self.istep += 1 

167 self.results['energy'], self. results['forces'] = energy, forces 

168 

169 def compute_energy_and_forces(self, pos, istep): 

170 unbiased_energy = self.calc.get_potential_energy(self.atoms) 

171 unbiased_forces = self.calc.get_forces(self.atoms) 

172 

173 if world.rank == 0: 

174 ener_forc = self.compute_bias(pos, istep, unbiased_energy) 

175 else: 

176 ener_forc = None 

177 energy_bias, forces_bias = broadcast(ener_forc) 

178 energy = unbiased_energy + energy_bias 

179 forces = unbiased_forces + forces_bias 

180 return energy, forces 

181 

182 def compute_bias(self, pos, istep, unbiased_energy): 

183 self.plumed.cmd("setStep", istep) 

184 

185 if self.use_charge: 

186 if 'charges' in self.calc.implemented_properties and \ 

187 self.update_charge: 

188 charges = self.calc.get_charges(atoms=self.atoms.copy()) 

189 

190 elif self.atoms.has('initial_charges') and not self.update_charge: 

191 charges = self.atoms.get_initial_charges() 

192 

193 else: 

194 assert not self.update_charge, "Charges cannot be updated" 

195 assert self.update_charge, "Not initial charges in Atoms" 

196 

197 self.plumed.cmd("setCharges", charges) 

198 

199 # Box for functions with PBC in plumed 

200 if self.atoms.cell: 

201 cell = np.asarray(self.atoms.get_cell()) 

202 self.plumed.cmd("setBox", cell) 

203 

204 self.plumed.cmd("setPositions", pos) 

205 self.plumed.cmd("setEnergy", unbiased_energy) 

206 self.plumed.cmd("setMasses", self.atoms.get_masses()) 

207 forces_bias = np.zeros((self.atoms.get_positions()).shape) 

208 self.plumed.cmd("setForces", forces_bias) 

209 virial = np.zeros((3, 3)) 

210 self.plumed.cmd("setVirial", virial) 

211 self.plumed.cmd("prepareCalc") 

212 self.plumed.cmd("performCalc") 

213 energy_bias = np.zeros((1,)) 

214 self.plumed.cmd("getBias", energy_bias) 

215 return [energy_bias, forces_bias] 

216 

217 def write_plumed_files(self, images): 

218 """ This function computes what is required in 

219 plumed input for some trajectory. 

220 

221 The outputs are saved in the typical files of 

222 plumed such as COLVAR, HILLS """ 

223 for i, image in enumerate(images): 

224 pos = image.get_positions() 

225 self.compute_energy_and_forces(pos, i) 

226 return self.read_plumed_files() 

227 

228 def read_plumed_files(self, file_name=None): 

229 read_files = {} 

230 if file_name is not None: 

231 read_files[file_name] = np.loadtxt(file_name, unpack=True) 

232 else: 

233 for line in self.input: 

234 if line.find('FILE') != -1: 

235 ini = line.find('FILE') 

236 end = line.find(' ', ini) 

237 if end == -1: 

238 file_name = line[ini + 5:] 

239 else: 

240 file_name = line[ini + 5:end] 

241 read_files[file_name] = np.loadtxt(file_name, unpack=True) 

242 

243 if len(read_files) == 0: 

244 if exists('COLVAR'): 

245 read_files['COLVAR'] = np.loadtxt('COLVAR', unpack=True) 

246 if exists('HILLS'): 

247 read_files['HILLS'] = np.loadtxt('HILLS', unpack=True) 

248 assert not len(read_files) == 0, "There are not files for reading" 

249 return read_files 

250 

251 def __enter__(self): 

252 return self 

253 

254 def __exit__(self, *args): 

255 self.plumed.finalize()