Coverage for /builds/debichem-team/python-ase/ase/calculators/gulp.py: 25.23%

218 statements  

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

1"""This module defines an ASE interface to GULP. 

2 

3Written by: 

4 

5Andy Cuko <andi.cuko@upmc.fr> 

6Antoni Macia <tonimacia@gmail.com> 

7 

8EXPORT ASE_GULP_COMMAND="/path/to/gulp < PREFIX.gin > PREFIX.got" 

9 

10Keywords 

11Options 

12 

13""" 

14import os 

15import re 

16 

17import numpy as np 

18 

19from ase.calculators.calculator import FileIOCalculator, ReadError 

20from ase.units import Ang, eV 

21 

22 

23class GULPOptimizer: 

24 def __init__(self, atoms, calc): 

25 self.atoms = atoms 

26 self.calc = calc 

27 

28 def todict(self): 

29 return {'type': 'optimization', 

30 'optimizer': 'GULPOptimizer'} 

31 

32 def run(self, fmax=None, steps=None, **gulp_kwargs): 

33 if fmax is not None: 

34 gulp_kwargs['gmax'] = fmax 

35 if steps is not None: 

36 gulp_kwargs['maxcyc'] = steps 

37 

38 self.calc.set(**gulp_kwargs) 

39 self.atoms.calc = self.calc 

40 self.atoms.get_potential_energy() 

41 self.atoms.cell = self.calc.get_atoms().cell 

42 self.atoms.positions[:] = self.calc.get_atoms().positions 

43 

44 

45class GULP(FileIOCalculator): 

46 implemented_properties = ['energy', 'free_energy', 'forces', 'stress'] 

47 _legacy_default_command = 'gulp < PREFIX.gin > PREFIX.got' 

48 discard_results_on_any_change = True 

49 default_parameters = dict( 

50 keywords='conp gradients', 

51 options=[], 

52 shel=[], 

53 library="ffsioh.lib", 

54 conditions=None) 

55 

56 def get_optimizer(self, atoms): 

57 gulp_keywords = self.parameters.keywords.split() 

58 if 'opti' not in gulp_keywords: 

59 raise ValueError('Can only create optimizer from GULP calculator ' 

60 'with "opti" keyword. Current keywords: {}' 

61 .format(gulp_keywords)) 

62 

63 opt = GULPOptimizer(atoms, self) 

64 return opt 

65 

66 def __init__(self, restart=None, 

67 ignore_bad_restart_file=FileIOCalculator._deprecated, 

68 label='gulp', atoms=None, optimized=None, 

69 Gnorm=1000.0, steps=1000, conditions=None, **kwargs): 

70 """Construct GULP-calculator object.""" 

71 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file, 

72 label, atoms, **kwargs) 

73 self.optimized = optimized 

74 self.Gnorm = Gnorm 

75 self.steps = steps 

76 self.conditions = conditions 

77 self.library_check() 

78 self.atom_types = [] 

79 

80 # GULP prints the fractional coordinates before the Final 

81 # lattice vectors so they need to be stored and then atoms 

82 # positions need to be set after we get the Final lattice 

83 # vectors 

84 self.fractional_coordinates = None 

85 

86 def write_input(self, atoms, properties=None, system_changes=None): 

87 FileIOCalculator.write_input(self, atoms, properties, system_changes) 

88 p = self.parameters 

89 

90 # Build string to hold .gin input file: 

91 s = p.keywords 

92 s += '\ntitle\nASE calculation\nend\n\n' 

93 

94 if all(self.atoms.pbc): 

95 cell_params = self.atoms.cell.cellpar() 

96 # Formating is necessary since Gulp max-line-length restriction 

97 s += 'cell\n{:9.6f} {:9.6f} {:9.6f} ' \ 

98 '{:8.5f} {:8.5f} {:8.5f}\n'.format(*cell_params) 

99 s += 'frac\n' 

100 coords = self.atoms.get_scaled_positions() 

101 else: 

102 s += 'cart\n' 

103 coords = self.atoms.get_positions() 

104 

105 if self.conditions is not None: 

106 c = self.conditions 

107 labels = c.get_atoms_labels() 

108 self.atom_types = c.get_atom_types() 

109 else: 

110 labels = self.atoms.get_chemical_symbols() 

111 

112 for xyz, symbol in zip(coords, labels): 

113 s += ' {:2} core' \ 

114 ' {:10.7f} {:10.7f} {:10.7f}\n' .format(symbol, *xyz) 

115 if symbol in p.shel: 

116 s += ' {:2} shel' \ 

117 ' {:10.7f} {:10.7f} {:10.7f}\n' .format(symbol, *xyz) 

118 

119 if p.library: 

120 s += f'\nlibrary {p.library}\n' 

121 

122 if p.options: 

123 for t in p.options: 

124 s += f'{t}\n' 

125 

126 gin_path = os.path.join(self.directory, self.prefix + '.gin') 

127 with open(gin_path, 'w') as fd: 

128 fd.write(s) 

129 

130 def read_results(self): 

131 FileIOCalculator.read(self, self.label) 

132 got_path = os.path.join(self.directory, self.label + '.got') 

133 if not os.path.isfile(got_path): 

134 raise ReadError 

135 

136 with open(got_path) as fd: 

137 lines = fd.readlines() 

138 

139 cycles = -1 

140 self.optimized = None 

141 for i, line in enumerate(lines): 

142 m = re.match(r'\s*Total lattice energy\s*=\s*(\S+)\s*eV', line) 

143 if m: 

144 energy = float(m.group(1)) 

145 self.results['energy'] = energy 

146 self.results['free_energy'] = energy 

147 

148 elif line.find('Optimisation achieved') != -1: 

149 self.optimized = True 

150 

151 elif line.find('Final Gnorm') != -1: 

152 self.Gnorm = float(line.split()[-1]) 

153 

154 elif line.find('Cycle:') != -1: 

155 cycles += 1 

156 

157 elif line.find('Final Cartesian derivatives') != -1: 

158 s = i + 5 

159 forces = [] 

160 while True: 

161 s = s + 1 

162 if lines[s].find("------------") != -1: 

163 break 

164 if lines[s].find(" s ") != -1: 

165 continue 

166 g = lines[s].split()[3:6] 

167 G = [-float(x) * eV / Ang for x in g] 

168 forces.append(G) 

169 forces = np.array(forces) 

170 self.results['forces'] = forces 

171 

172 elif line.find('Final internal derivatives') != -1: 

173 s = i + 5 

174 forces = [] 

175 while True: 

176 s = s + 1 

177 if lines[s].find("------------") != -1: 

178 break 

179 g = lines[s].split()[3:6] 

180 

181 # Uncomment the section below to separate the numbers when 

182 # there is no space between them, in the case of long 

183 # numbers. This prevents the code to break if numbers are 

184 # too big. 

185 

186 '''for t in range(3-len(g)): 

187 g.append(' ') 

188 for j in range(2): 

189 min_index=[i+1 for i,e in enumerate(g[j][1:]) 

190 if e == '-'] 

191 if j==0 and len(min_index) != 0: 

192 if len(min_index)==1: 

193 g[2]=g[1] 

194 g[1]=g[0][min_index[0]:] 

195 g[0]=g[0][:min_index[0]] 

196 else: 

197 g[2]=g[0][min_index[1]:] 

198 g[1]=g[0][min_index[0]:min_index[1]] 

199 g[0]=g[0][:min_index[0]] 

200 break 

201 if j==1 and len(min_index) != 0: 

202 g[2]=g[1][min_index[0]:] 

203 g[1]=g[1][:min_index[0]]''' 

204 

205 G = [-float(x) * eV / Ang for x in g] 

206 forces.append(G) 

207 forces = np.array(forces) 

208 self.results['forces'] = forces 

209 

210 elif line.find('Final cartesian coordinates of atoms') != -1: 

211 s = i + 5 

212 positions = [] 

213 while True: 

214 s = s + 1 

215 if lines[s].find("------------") != -1: 

216 break 

217 if lines[s].find(" s ") != -1: 

218 continue 

219 xyz = lines[s].split()[3:6] 

220 XYZ = [float(x) * Ang for x in xyz] 

221 positions.append(XYZ) 

222 positions = np.array(positions) 

223 self.atoms.set_positions(positions) 

224 

225 elif line.find('Final stress tensor components') != -1: 

226 res = [0., 0., 0., 0., 0., 0.] 

227 for j in range(3): 

228 var = lines[i + j + 3].split()[1] 

229 res[j] = float(var) 

230 var = lines[i + j + 3].split()[3] 

231 res[j + 3] = float(var) 

232 stress = np.array(res) 

233 self.results['stress'] = stress 

234 

235 elif line.find('Final Cartesian lattice vectors') != -1: 

236 lattice_vectors = np.zeros((3, 3)) 

237 s = i + 2 

238 for j in range(s, s + 3): 

239 temp = lines[j].split() 

240 for k in range(3): 

241 lattice_vectors[j - s][k] = float(temp[k]) 

242 self.atoms.set_cell(lattice_vectors) 

243 if self.fractional_coordinates is not None: 

244 self.fractional_coordinates = np.array( 

245 self.fractional_coordinates) 

246 self.atoms.set_scaled_positions( 

247 self.fractional_coordinates) 

248 

249 elif line.find('Final fractional coordinates of atoms') != -1: 

250 s = i + 5 

251 scaled_positions = [] 

252 while True: 

253 s = s + 1 

254 if lines[s].find("------------") != -1: 

255 break 

256 if lines[s].find(" s ") != -1: 

257 continue 

258 xyz = lines[s].split()[3:6] 

259 XYZ = [float(x) for x in xyz] 

260 scaled_positions.append(XYZ) 

261 self.fractional_coordinates = scaled_positions 

262 

263 self.steps = cycles 

264 

265 def get_opt_state(self): 

266 return self.optimized 

267 

268 def get_opt_steps(self): 

269 return self.steps 

270 

271 def get_Gnorm(self): 

272 return self.Gnorm 

273 

274 def library_check(self): 

275 if self.parameters['library'] is not None: 

276 if 'GULP_LIB' not in self.cfg: 

277 raise RuntimeError("Be sure to have set correctly $GULP_LIB " 

278 "or to have the force field library.") 

279 

280 

281class Conditions: 

282 """Atomic labels for the GULP calculator. 

283 

284 This class manages an array similar to 

285 atoms.get_chemical_symbols() via get_atoms_labels() method, but 

286 with atomic labels in stead of atomic symbols. This is useful 

287 when you need to use calculators like GULP or lammps that use 

288 force fields. Some force fields can have different atom type for 

289 the same element. In this class you can create a set_rule() 

290 function that assigns labels according to structural criteria.""" 

291 

292 def __init__(self, atoms): 

293 self.atoms = atoms 

294 self.atoms_symbols = atoms.get_chemical_symbols() 

295 self.atoms_labels = atoms.get_chemical_symbols() 

296 self.atom_types = [] 

297 

298 def min_distance_rule(self, sym1, sym2, 

299 ifcloselabel1=None, ifcloselabel2=None, 

300 elselabel1=None, max_distance=3.0): 

301 """Find pairs of atoms to label based on proximity. 

302 

303 This is for, e.g., the ffsioh or catlow force field, where we 

304 would like to identify those O atoms that are close to H 

305 atoms. For each H atoms, we must specially label one O atom. 

306 

307 This function is a rule that allows to define atom labels (like O1, 

308 O2, O_H etc..) starting from element symbols of an Atoms 

309 object that a force field can use and according to distance 

310 parameters. 

311 

312 Example: 

313 atoms = read('some_xyz_format.xyz') 

314 a = Conditions(atoms) 

315 a.set_min_distance_rule('O', 'H', ifcloselabel1='O2', 

316 ifcloselabel2='H', elselabel1='O1') 

317 new_atoms_labels = a.get_atom_labels() 

318 

319 In the example oxygens O are going to be labeled as O2 if they 

320 are close to a hydrogen atom othewise are labeled O1. 

321 

322 """ 

323 

324 if ifcloselabel1 is None: 

325 ifcloselabel1 = sym1 

326 if ifcloselabel2 is None: 

327 ifcloselabel2 = sym2 

328 if elselabel1 is None: 

329 elselabel1 = sym1 

330 

331 # self.atom_types is a list of element types used instead of element 

332 # symbols in orger to track the changes made. Take care of this because 

333 # is very important.. gulp_read function that parse the output 

334 # has to know which atom_type it has to associate with which 

335 # atom_symbol 

336 # 

337 # Example: [['O','O1','O2'],['H', 'H_C', 'H_O']] 

338 # this because Atoms oject accept only atoms symbols 

339 self.atom_types.append([sym1, ifcloselabel1, elselabel1]) 

340 self.atom_types.append([sym2, ifcloselabel2]) 

341 

342 dist_mat = self.atoms.get_all_distances() 

343 index_assigned_sym1 = [] 

344 index_assigned_sym2 = [] 

345 

346 for i in range(len(self.atoms_symbols)): 

347 if self.atoms_symbols[i] == sym2: 

348 dist_12 = 1000 

349 index_assigned_sym2.append(i) 

350 for t in range(len(self.atoms_symbols)): 

351 if (self.atoms_symbols[t] == sym1 

352 and dist_mat[i, t] < dist_12 

353 and t not in index_assigned_sym1): 

354 dist_12 = dist_mat[i, t] 

355 closest_sym1_index = t 

356 index_assigned_sym1.append(closest_sym1_index) 

357 

358 for i1, i2 in zip(index_assigned_sym1, index_assigned_sym2): 

359 if dist_mat[i1, i2] > max_distance: 

360 raise ValueError('Cannot unambiguously apply minimum-distance ' 

361 'rule because pairings are not obvious. ' 

362 'If you wish to ignore this, then increase ' 

363 'max_distance.') 

364 

365 for s in range(len(self.atoms_symbols)): 

366 if s in index_assigned_sym1: 

367 self.atoms_labels[s] = ifcloselabel1 

368 elif s not in index_assigned_sym1 and self.atoms_symbols[s] == sym1: 

369 self.atoms_labels[s] = elselabel1 

370 elif s in index_assigned_sym2: 

371 self.atoms_labels[s] = ifcloselabel2 

372 

373 def get_atom_types(self): 

374 return self.atom_types 

375 

376 def get_atoms_labels(self): 

377 labels = np.array(self.atoms_labels) 

378 return labels