Coverage for /builds/ase/ase/ase/calculators/crystal.py : 8.09%

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
1"""This module defines an ASE interface to CRYSTAL14/CRYSTAL17
3http://www.crystal.unito.it/
5Written by:
7 Daniele Selli, daniele.selli@unimib.it
8 Gianluca Fazio, g.fazio3@campus.unimib.it
10The file 'fort.34' contains the input and output geometry
11and it will be updated during the crystal calculations.
12The wavefunction is stored in 'fort.20' as binary file.
14The keywords are given, for instance, as follows:
16 guess = True,
17 xc = 'PBE',
18 kpts = (2,2,2),
19 otherkeys = [ 'scfdir', 'anderson', ['maxcycles','500'],
20 ['fmixing','90']],
21 ...
24 When used for QM/MM, Crystal calculates coulomb terms
25 within all point charges. This is wrong and should be corrected by either:
27 1. Re-calculating the terms and subtracting them
28 2. Reading in the values from FORCES_CHG.DAT and subtracting
31 BOTH Options should be available, with 1 as standard, since 2 is
32 only available in a development version of CRYSTAL
34"""
36from ase.units import Hartree, Bohr
37from ase.io import write
38import numpy as np
39import os
40from ase.calculators.calculator import FileIOCalculator
43class CRYSTAL(FileIOCalculator):
44 """ A crystal calculator with ase-FileIOCalculator nomenclature
45 """
47 implemented_properties = ['energy', 'forces', 'stress', 'charges',
48 'dipole']
50 def __init__(self, restart=None,
51 ignore_bad_restart_file=FileIOCalculator._deprecated,
52 label='cry', atoms=None, crys_pcc=False, **kwargs):
53 """Construct a crystal calculator.
55 """
56 # default parameters
57 self.default_parameters = dict(
58 xc='HF',
59 spinpol=False,
60 oldgrid=False,
61 neigh=False,
62 coarsegrid=False,
63 guess=True,
64 kpts=None,
65 isp=1,
66 basis='custom',
67 smearing=None,
68 otherkeys=[])
70 self.pcpot = None
71 self.lines = None
72 self.atoms = None
73 self.crys_pcc = crys_pcc # True: Reads Coulomb Correction from file.
74 self.atoms_input = None
75 self.outfilename = 'cry.out'
77 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file,
78 label, atoms,
79 **kwargs)
81 def write_crystal_in(self, filename):
82 """ Write the input file for the crystal calculation.
83 Geometry is taken always from the file 'fort.34'
84 """
86 # write BLOCK 1 (only SP with gradients)
87 with open(filename, 'wt', encoding='latin-1') as outfile:
88 self._write_crystal_in(outfile)
90 def _write_crystal_in(self, outfile):
91 outfile.write('Single point + Gradient crystal calculation \n')
92 outfile.write('EXTERNAL \n')
93 outfile.write('NEIGHPRT \n')
94 outfile.write('0 \n')
96 if self.pcpot:
97 outfile.write('POINTCHG \n')
98 self.pcpot.write_mmcharges('POINTCHG.INP')
100 # write BLOCK 2 from file (basis sets)
101 p = self.parameters
102 if p.basis == 'custom':
103 outfile.write('END \n')
104 with open(os.path.join(self.directory, 'basis')) as basisfile:
105 basis_ = basisfile.readlines()
106 for line in basis_:
107 outfile.write(line)
108 outfile.write('99 0 \n')
109 outfile.write('END \n')
110 else:
111 outfile.write('BASISSET \n')
112 outfile.write(p.basis.upper() + '\n')
114 # write BLOCK 3 according to parameters set as input
115 # ----- write hamiltonian
117 if self.atoms.get_initial_magnetic_moments().any():
118 p.spinpol = True
120 if p.xc == 'HF':
121 if p.spinpol:
122 outfile.write('UHF \n')
123 else:
124 outfile.write('RHF \n')
125 elif p.xc == 'MP2':
126 outfile.write('MP2 \n')
127 outfile.write('ENDMP2 \n')
128 else:
129 outfile.write('DFT \n')
130 # Standalone keywords and LDA are given by a single string.
131 if isinstance(p.xc, str):
132 xc = {'LDA': 'EXCHANGE\nLDA\nCORRELAT\nVWN',
133 'PBE': 'PBEXC'}.get(p.xc, p.xc)
134 outfile.write(xc.upper() + '\n')
135 # Custom xc functional are given by a tuple of string
136 else:
137 x, c = p.xc
138 outfile.write('EXCHANGE \n')
139 outfile.write(x + ' \n')
140 outfile.write('CORRELAT \n')
141 outfile.write(c + ' \n')
142 if p.spinpol:
143 outfile.write('SPIN \n')
144 if p.oldgrid:
145 outfile.write('OLDGRID \n')
146 if p.coarsegrid:
147 outfile.write('RADIAL\n')
148 outfile.write('1\n')
149 outfile.write('4.0\n')
150 outfile.write('20\n')
151 outfile.write('ANGULAR\n')
152 outfile.write('5\n')
153 outfile.write('0.1667 0.5 0.9 3.05 9999.0\n')
154 outfile.write('2 6 8 13 8\n')
155 outfile.write('END \n')
156 # When guess=True, wf is read.
157 if p.guess:
158 # wf will be always there after 2nd step.
159 if os.path.isfile('fort.20'):
160 outfile.write('GUESSP \n')
161 elif os.path.isfile('fort.9'):
162 outfile.write('GUESSP \n')
163 os.system('cp fort.9 fort.20')
165 # smearing
166 if p.smearing is not None:
167 if p.smearing[0] != 'Fermi-Dirac':
168 raise ValueError('Only Fermi-Dirac smearing is allowed.')
169 else:
170 outfile.write('SMEAR \n')
171 outfile.write(str(p.smearing[1] / Hartree) + ' \n')
173 # ----- write other CRYSTAL keywords
174 # ----- in the list otherkey = ['ANDERSON', ...] .
176 for keyword in p.otherkeys:
177 if isinstance(keyword, str):
178 outfile.write(keyword.upper() + '\n')
179 else:
180 for key in keyword:
181 outfile.write(key.upper() + '\n')
183 ispbc = self.atoms.get_pbc()
184 self.kpts = p.kpts
186 # if it is periodic, gamma is the default.
187 if any(ispbc):
188 if self.kpts is None:
189 self.kpts = (1, 1, 1)
190 else:
191 self.kpts = None
193 # explicit lists of K-points, shifted Monkhorst-
194 # Pack net and k-point density definition are
195 # not allowed.
196 if self.kpts is not None:
197 if isinstance(self.kpts, float):
198 raise ValueError('K-point density definition not allowed.')
199 if isinstance(self.kpts, list):
200 raise ValueError('Explicit K-points definition not allowed.')
201 if isinstance(self.kpts[-1], str):
202 raise ValueError('Shifted Monkhorst-Pack not allowed.')
203 outfile.write('SHRINK \n')
204 # isp is by default 1, 2 is suggested for metals.
205 outfile.write('0 ' + str(p.isp * max(self.kpts)) + ' \n')
206 if ispbc[2]:
207 outfile.write(str(self.kpts[0])
208 + ' ' + str(self.kpts[1])
209 + ' ' + str(self.kpts[2]) + ' \n')
210 elif ispbc[1]:
211 outfile.write(str(self.kpts[0])
212 + ' ' + str(self.kpts[1])
213 + ' 1 \n')
214 elif ispbc[0]:
215 outfile.write(str(self.kpts[0])
216 + ' 1 1 \n')
218 # GRADCAL command performs a single
219 # point and prints out the forces
220 # also on the charges
221 outfile.write('GRADCAL \n')
222 outfile.write('END \n')
224 def write_input(self, atoms, properties=None, system_changes=None):
225 FileIOCalculator.write_input(
226 self, atoms, properties, system_changes)
227 self.write_crystal_in(os.path.join(self.directory, 'INPUT'))
228 write(os.path.join(self.directory, 'fort.34'), atoms)
229 # self.atoms is none until results are read out,
230 # then it is set to the ones at writing input
231 self.atoms_input = atoms
232 self.atoms = None
234 def read_results(self):
235 """ all results are read from OUTPUT file
236 It will be destroyed after it is read to avoid
237 reading it once again after some runtime error """
239 with open(os.path.join(self.directory, 'OUTPUT'),
240 'rt',
241 encoding='latin-1') as myfile:
242 self.lines = myfile.readlines()
244 self.atoms = self.atoms_input
245 # Energy line index
246 estring1 = 'SCF ENDED'
247 estring2 = 'TOTAL ENERGY + DISP'
248 for iline, line in enumerate(self.lines):
249 if line.find(estring1) >= 0:
250 index_energy = iline
251 pos_en = 8
252 break
253 else:
254 raise RuntimeError('Problem in reading energy')
255 # Check if there is dispersion corrected
256 # energy value.
257 for iline, line in enumerate(self.lines):
258 if line.find(estring2) >= 0:
259 index_energy = iline
260 pos_en = 5
262 # If there's a point charge potential (QM/MM), read corrections
263 e_coul = 0
264 if self.pcpot:
265 if self.crys_pcc:
266 self.pcpot.read_pc_corrections()
267 # also pass on to pcpot that it should read in from file
268 self.pcpot.crys_pcc = True
269 else:
270 self.pcpot.manual_pc_correct()
271 e_coul, f_coul = self.pcpot.coulomb_corrections
273 energy = float(self.lines[index_energy].split()[pos_en]) * Hartree
274 energy -= e_coul # e_coul already in eV.
276 self.results['energy'] = energy
277 # Force line indexes
278 fstring = 'CARTESIAN FORCES'
279 gradients = []
280 for iline, line in enumerate(self.lines):
281 if line.find(fstring) >= 0:
282 index_force_begin = iline + 2
283 break
284 else:
285 raise RuntimeError('Problem in reading forces')
286 for j in range(index_force_begin, index_force_begin + len(self.atoms)):
287 word = self.lines[j].split()
288 # If GHOST atoms give problems, have a close look at this
289 if len(word) == 5:
290 gradients.append([float(word[k + 2]) for k in range(0, 3)])
291 elif len(word) == 4:
292 gradients.append([float(word[k + 1]) for k in range(0, 3)])
293 else:
294 raise RuntimeError('Problem in reading forces')
296 forces = np.array(gradients) * Hartree / Bohr
298 self.results['forces'] = forces
300 # stress stuff begins
301 sstring = 'STRESS TENSOR, IN'
302 have_stress = False
303 stress = []
304 for iline, line in enumerate(self.lines):
305 if sstring in line:
306 have_stress = True
307 start = iline + 4
308 end = start + 3
309 for i in range(start, end):
310 cell = [float(x) for x in self.lines[i].split()]
311 stress.append(cell)
312 if have_stress:
313 stress = -np.array(stress) * Hartree / Bohr**3
314 self.results['stress'] = stress
316 # stress stuff ends
318 # Get partial charges on atoms.
319 # In case we cannot find charges
320 # they are set to None
321 qm_charges = []
323 # ----- this for cycle finds the last entry of the
324 # ----- string search, which corresponds
325 # ----- to the charges at the end of the SCF.
326 for n, line in enumerate(self.lines):
327 if 'TOTAL ATOMIC CHARGE' in line:
328 chargestart = n + 1
329 lines1 = self.lines[chargestart:(chargestart
330 + (len(self.atoms) - 1) // 6 + 1)]
331 atomnum = self.atoms.get_atomic_numbers()
332 words = []
333 for line in lines1:
334 for el in line.split():
335 words.append(float(el))
336 i = 0
337 for atn in atomnum:
338 qm_charges.append(-words[i] + atn)
339 i = i + 1
340 charges = np.array(qm_charges)
341 self.results['charges'] = charges
343 # Read dipole moment.
344 dipole = np.zeros([1, 3])
345 for n, line in enumerate(self.lines):
346 if 'DIPOLE MOMENT ALONG' in line:
347 dipolestart = n + 2
348 dipole = np.array([float(f) for f in
349 self.lines[dipolestart].split()[2:5]])
350 break
351 # debye to e*Ang
352 self.results['dipole'] = dipole * 0.2081943482534
354 def embed(self, mmcharges=None, directory='./'):
355 """Embed atoms in point-charges (mmcharges)
356 """
357 self.pcpot = PointChargePotential(mmcharges, self.directory)
358 return self.pcpot
361class PointChargePotential:
362 def __init__(self, mmcharges, directory='./'):
363 """Point-charge potential for CRYSTAL.
364 """
365 self.mmcharges = mmcharges
366 self.directory = directory
367 self.mmpositions = None
368 self.mmforces = None
369 self.coulomb_corrections = None
370 self.crys_pcc = False
372 def set_positions(self, mmpositions):
373 self.mmpositions = mmpositions
375 def set_charges(self, mmcharges):
376 self.mmcharges = mmcharges
378 def write_mmcharges(self, filename):
379 """ mok all
380 write external charges as monopoles for CRYSTAL.
382 """
383 if self.mmcharges is None:
384 print("CRYSTAL: Warning: not writing external charges ")
385 return
386 with open(os.path.join(self.directory, filename), 'w') as charge_file:
387 charge_file.write(str(len(self.mmcharges)) + ' \n')
388 for [pos, charge] in zip(self.mmpositions, self.mmcharges):
389 [x, y, z] = pos
390 charge_file.write('%12.6f %12.6f %12.6f %12.6f \n'
391 % (x, y, z, charge))
393 def get_forces(self, calc, get_forces=True):
394 """ returns forces on point charges if the flag get_forces=True """
395 if get_forces:
396 return self.read_forces_on_pointcharges()
397 else:
398 return np.zeros_like(self.mmpositions)
400 def read_forces_on_pointcharges(self):
401 """Read Forces from CRYSTAL output file (OUTPUT)."""
402 with open(os.path.join(self.directory, 'OUTPUT'), 'r') as infile:
403 lines = infile.readlines()
405 print('PCPOT crys_pcc: ' + str(self.crys_pcc))
406 # read in force and energy Coulomb corrections
407 if self.crys_pcc:
408 self.read_pc_corrections()
409 else:
410 self.manual_pc_correct()
411 e_coul, f_coul = self.coulomb_corrections
413 external_forces = []
414 for n, line in enumerate(lines):
415 if ('RESULTANT FORCE' in line):
416 chargeend = n - 1
417 break
418 else:
419 raise RuntimeError(
420 'Problem in reading forces on MM external-charges')
421 lines1 = lines[(chargeend - len(self.mmcharges)):chargeend]
422 for line in lines1:
423 external_forces.append(
424 [float(i) for i in line.split()[2:]])
426 f = np.array(external_forces) - f_coul
427 f *= (Hartree / Bohr)
429 return f
431 def read_pc_corrections(self):
432 ''' Crystal calculates Coulomb forces and energies between all
433 point charges, and adds that to the QM subsystem. That needs
434 to be subtracted again.
435 This will be standard in future CRYSTAL versions .'''
437 with open(os.path.join(self.directory,
438 'FORCES_CHG.DAT'), 'r') as infile:
439 lines = infile.readlines()
441 e = [float(x.split()[-1])
442 for x in lines if 'SELF-INTERACTION ENERGY(AU)' in x][0]
444 e *= Hartree
446 f_lines = [s for s in lines if '199' in s]
447 assert len(f_lines) == len(self.mmcharges), \
448 'Mismatch in number of point charges from FORCES_CHG.dat'
450 pc_forces = np.zeros((len(self.mmcharges), 3))
451 for i, l in enumerate(f_lines):
452 first = l.split(str(i + 1) + ' 199 ')
453 assert len(first) == 2, 'Problem reading FORCES_CHG.dat'
454 f = first[-1].split()
455 pc_forces[i] = [float(x) for x in f]
457 self.coulomb_corrections = (e, pc_forces)
459 def manual_pc_correct(self):
460 ''' For current versions of CRYSTAL14/17, manual Coulomb correction '''
462 R = self.mmpositions / Bohr
463 charges = self.mmcharges
465 forces = np.zeros_like(R)
466 energy = 0.0
468 for m in range(len(charges)):
469 D = R[m + 1:] - R[m]
470 d2 = (D**2).sum(1)
471 d = d2**0.5
473 e_c = charges[m + 1:] * charges[m] / d
475 energy += np.sum(e_c)
477 F = (e_c / d2)[:, None] * D
479 forces[m] -= F.sum(0)
480 forces[m + 1:] += F
482 energy *= Hartree
484 self.coulomb_corrections = (energy, forces)