Coverage for /builds/kinetik161/ase/ase/calculators/demon/demon.py: 40.85%
355 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1"""This module defines an ASE interface to deMon.
3http://www.demon-software.com
5"""
6import os
7import os.path as op
8import shutil
9import subprocess
11import numpy as np
13import ase.data
14import ase.io
15from ase.calculators.calculator import (FileIOCalculator, Parameters,
16 ReadError, all_changes, equal,
17 CalculatorSetupError)
18from ase.units import Bohr, Hartree
20from .demon_io import parse_xray
22m_e_to_amu = 1822.88839
25class Parameters_deMon(Parameters):
26 """Parameters class for the calculator.
27 Documented in Base_deMon.__init__
29 The options here are the most important ones that the user needs to be
30 aware of. Further options accepted by deMon can be set in the dictionary
31 input_arguments.
33 """
35 def __init__(
36 self,
37 label='rundir',
38 atoms=None,
39 restart=None,
40 basis_path=None,
41 ignore_bad_restart_file=FileIOCalculator._deprecated,
42 deMon_restart_path='.',
43 title='deMon input file',
44 scftype='RKS',
45 forces=False,
46 dipole=False,
47 xc='VWN',
48 guess='TB',
49 print_out='MOE',
50 basis={},
51 ecps={},
52 mcps={},
53 auxis={},
54 augment={},
55 input_arguments=None):
56 kwargs = locals()
57 kwargs.pop('self')
58 Parameters.__init__(self, **kwargs)
61class Demon(FileIOCalculator):
62 """Calculator interface to the deMon code. """
64 implemented_properties = [
65 'energy',
66 'forces',
67 'dipole',
68 'eigenvalues']
70 def __init__(self, *, command=None, **kwargs):
71 """ASE interface to the deMon code.
73 The deMon2k code can be obtained from http://www.demon-software.com
75 The DEMON_COMMAND environment variable must be set to run the
76 executable, in bash it would be set along the lines of
77 export DEMON_COMMAND="deMon.4.3.6.std > deMon_ase.out 2>&1"
79 Parameters:
81 label : str
82 relative path to the run directory
83 atoms : Atoms object
84 the atoms object
85 command : str
86 Command to run deMon. If not present the environment
87 variable DEMON_COMMAND will be used
88 restart : str
89 Relative path to ASE restart directory for parameters and
90 atoms object and results
91 basis_path : str
92 Relative path to the directory containing
93 BASIS, AUXIS, ECPS, MCPS and AUGMENT
94 ignore_bad_restart_file : bool
95 Ignore broken or missing ASE restart files
96 By default, it is an error if the restart
97 file is missing or broken.
98 deMon_restart_path : str
99 Relative path to the deMon restart dir
100 title : str
101 Title in the deMon input file.
102 scftype : str
103 Type of scf
104 forces : bool
105 If True a force calculation will be enforced.
106 dipole : bool
107 If True a dipole calculation will be enforced
108 xc : str
109 xc-functional
110 guess : str
111 guess for initial density and wave functions
112 print_out : str | list
113 Options for the printing in deMon
114 basis : dict
115 Definition of basis sets.
116 ecps : dict
117 Definition of ECPs
118 mcps : dict
119 Definition of MCPs
120 auxis : dict
121 Definition of AUXIS
122 augment : dict
123 Definition of AUGMENT
124 input_arguments : dict
125 Explicitly given input arguments. The key is the input keyword
126 and the value is either a str, a list of str (will be written
127 on the same line as the keyword),
128 or a list of lists of str (first list is written on the first
129 line, the others on following lines.)
131 For example usage, see the tests h2o.py and h2o_xas_xes.py in
132 the directory ase/test/demon
134 """
136 parameters = Parameters_deMon(**kwargs)
138 # Setup the run command
139 if command is None:
140 command = self.cfg.get('DEMON_COMMAND')
142 FileIOCalculator.__init__(
143 self,
144 command=command,
145 **parameters)
147 def __getitem__(self, key):
148 """Convenience method to retrieve a parameter as
149 calculator[key] rather than calculator.parameters[key]
151 Parameters:
152 key : str, the name of the parameters to get.
153 """
154 return self.parameters[key]
156 def set(self, **kwargs):
157 """Set all parameters.
159 Parameters:
160 kwargs : Dictionary containing the keywords for deMon
161 """
162 # Put in the default arguments.
163 kwargs = self.default_parameters.__class__(**kwargs)
165 if 'parameters' in kwargs:
166 filename = kwargs.pop('parameters')
167 parameters = Parameters.read(filename)
168 parameters.update(kwargs)
169 kwargs = parameters
171 changed_parameters = {}
173 for key, value in kwargs.items():
174 oldvalue = self.parameters.get(key)
175 if key not in self.parameters or not equal(value, oldvalue):
176 changed_parameters[key] = value
177 self.parameters[key] = value
179 return changed_parameters
181 def link_file(self, fromdir, todir, filename):
182 if op.exists(todir + '/' + filename):
183 os.remove(todir + '/' + filename)
185 if op.exists(fromdir + '/' + filename):
186 os.symlink(fromdir + '/' + filename,
187 todir + '/' + filename)
188 else:
189 raise RuntimeError(
190 "{} doesn't exist".format(fromdir + '/' + filename))
192 def calculate(self,
193 atoms=None,
194 properties=['energy'],
195 system_changes=all_changes):
196 """Capture the RuntimeError from FileIOCalculator.calculate
197 and add a little debug information from the deMon output.
199 See base FileIocalculator for documentation.
200 """
202 if atoms is not None:
203 self.atoms = atoms.copy()
205 self.write_input(self.atoms, properties, system_changes)
206 command = self.command
208 # basis path
209 basis_path = self.parameters['basis_path']
210 if basis_path is None:
211 basis_path = self.cfg.get('DEMON_BASIS_PATH')
213 if basis_path is None:
214 raise RuntimeError('Please set basis_path keyword,' +
215 ' or the DEMON_BASIS_PATH' +
216 ' environment variable')
218 # link restart file
219 value = self.parameters['guess']
220 if value.upper() == 'RESTART':
221 value2 = self.parameters['deMon_restart_path']
223 if op.exists(self.directory + '/deMon.rst')\
224 or op.islink(self.directory + '/deMon.rst'):
225 os.remove(self.directory + '/deMon.rst')
226 abspath = op.abspath(value2)
228 if op.exists(abspath + '/deMon.mem') \
229 or op.islink(abspath + '/deMon.mem'):
231 shutil.copy(abspath + '/deMon.mem',
232 self.directory + '/deMon.rst')
233 else:
234 raise RuntimeError(
235 "{} doesn't exist".format(abspath + '/deMon.rst'))
237 abspath = op.abspath(basis_path)
239 for name in ['BASIS', 'AUXIS', 'ECPS', 'MCPS', 'FFDS']:
240 self.link_file(abspath, self.directory, name)
242 if command is None:
243 raise CalculatorSetupError
244 subprocess.check_call(command, shell=True, cwd=self.directory)
246 try:
247 self.read_results()
248 except Exception: # XXX Which kind of exception?
249 with open(self.directory + '/deMon.out') as fd:
250 lines = fd.readlines()
251 debug_lines = 10
252 print('##### %d last lines of the deMon.out' % debug_lines)
253 for line in lines[-20:]:
254 print(line.strip())
255 print('##### end of deMon.out')
256 raise RuntimeError
258 def set_label(self, label):
259 """Set label directory """
261 self.label = label
263 # in our case self.directory = self.label
264 self.directory = self.label
265 if self.directory == '':
266 self.directory = os.curdir
268 def write_input(self, atoms, properties=None, system_changes=None):
269 """Write input (in)-file.
270 See calculator.py for further details.
272 Parameters:
273 atoms : The Atoms object to write.
274 properties : The properties which should be calculated.
275 system_changes : List of properties changed since last run.
277 """
278 # Call base calculator.
279 FileIOCalculator.write_input(
280 self,
281 atoms=atoms,
282 properties=properties,
283 system_changes=system_changes)
285 if system_changes is None and properties is None:
286 return
288 filename = f'{self.directory}/deMon.inp'
290 add_print = ''
292 # Start writing the file.
293 with open(filename, 'w') as fd:
295 # write keyword argument keywords
296 value = self.parameters['title']
297 self._write_argument('TITLE', value, fd)
299 fd.write('#\n')
301 value = self.parameters['scftype']
302 self._write_argument('SCFTYPE', value, fd)
304 value = self.parameters['xc']
305 self._write_argument('VXCTYPE', value, fd)
307 value = self.parameters['guess']
308 self._write_argument('GUESS', value, fd)
310 # obtain forces through a single BOMD step
311 # only if forces is in properties, or if keyword forces is True
312 value = self.parameters['forces']
313 if 'forces' in properties or value:
315 self._write_argument('DYNAMICS',
316 ['INT=1', 'MAX=0', 'STEP=0'], fd)
317 self._write_argument('TRAJECTORY', 'FORCES', fd)
318 self._write_argument('VELOCITIES', 'ZERO', fd)
319 add_print = add_print + ' ' + 'MD OPT'
321 # if dipole is True, enforce dipole calculation.
322 # Otherwise only if asked for
323 value = self.parameters['dipole']
324 if 'dipole' in properties or value:
325 self._write_argument('DIPOLE', '', fd)
327 # print argument, here other options could change this
328 value = self.parameters['print_out']
329 assert isinstance(value, str)
330 value = value + add_print
332 if not len(value) == 0:
333 self._write_argument('PRINT', value, fd)
334 fd.write('#\n')
336 # write general input arguments
337 self._write_input_arguments(fd)
339 fd.write('#\n')
341 # write basis set, ecps, mcps, auxis, augment
342 basis = self.parameters['basis']
343 if 'all' not in basis:
344 basis['all'] = 'DZVP'
345 self._write_basis(fd, atoms, basis, string='BASIS')
347 ecps = self.parameters['ecps']
348 if not len(ecps) == 0:
349 self._write_basis(fd, atoms, ecps, string='ECPS')
351 mcps = self.parameters['mcps']
352 if not len(mcps) == 0:
353 self._write_basis(fd, atoms, mcps, string='MCPS')
355 auxis = self.parameters['auxis']
356 if not len(auxis) == 0:
357 self._write_basis(fd, atoms, auxis, string='AUXIS')
359 augment = self.parameters['augment']
360 if not len(augment) == 0:
361 self._write_basis(fd, atoms, augment, string='AUGMENT')
363 # write geometry
364 self._write_atomic_coordinates(fd, atoms)
366 # write xyz file for good measure.
367 ase.io.write(f'{self.directory}/deMon_atoms.xyz', self.atoms)
369 def read(self, restart_path):
370 """Read parameters from directory restart_path."""
372 self.set_label(restart_path)
374 if not op.exists(restart_path + '/deMon.inp'):
375 raise ReadError('The restart_path file {} does not exist'
376 .format(restart_path))
378 self.atoms = self.deMon_inp_to_atoms(restart_path + '/deMon.inp')
380 self.read_results()
382 def _write_input_arguments(self, fd):
383 """Write directly given input-arguments."""
384 input_arguments = self.parameters['input_arguments']
386 # Early return
387 if input_arguments is None:
388 return
390 for key, value in input_arguments.items():
391 self._write_argument(key, value, fd)
393 def _write_argument(self, key, value, fd):
394 """Write an argument to file.
395 key : a string coresponding to the input keyword
396 value : the arguments, can be a string, a number or a list
397 f : and open file
398 """
400 # for only one argument, write on same line
401 if not isinstance(value, (tuple, list)):
402 line = key.upper()
403 line += ' ' + str(value).upper()
404 fd.write(line)
405 fd.write('\n')
407 # for a list, write first argument on the first line,
408 # then the rest on new lines
409 else:
410 line = key
411 if not isinstance(value[0], (tuple, list)):
412 for i in range(len(value)):
413 line += ' ' + str(value[i].upper())
414 fd.write(line)
415 fd.write('\n')
416 else:
417 for i in range(len(value)):
418 for j in range(len(value[i])):
419 line += ' ' + str(value[i][j]).upper()
420 fd.write(line)
421 fd.write('\n')
422 line = ''
424 def _write_atomic_coordinates(self, fd, atoms):
425 """Write atomic coordinates.
427 Parameters:
428 - f: An open file object.
429 - atoms: An atoms object.
430 """
432 fd.write('#\n')
433 fd.write('# Atomic coordinates\n')
434 fd.write('#\n')
435 fd.write('GEOMETRY CARTESIAN ANGSTROM\n')
437 for i in range(len(atoms)):
438 xyz = atoms.get_positions()[i]
439 chem_symbol = atoms.get_chemical_symbols()[i]
440 chem_symbol += str(i + 1)
442 # if tag is set to 1 then we have a ghost atom,
443 # set nuclear charge to 0
444 if atoms.get_tags()[i] == 1:
445 nuc_charge = str(0)
446 else:
447 nuc_charge = str(atoms.get_atomic_numbers()[i])
449 mass = atoms.get_masses()[i]
451 line = f'{chem_symbol:6s}'.rjust(10) + ' '
452 line += f'{xyz[0]:.5f}'.rjust(10) + ' '
453 line += f'{xyz[1]:.5f}'.rjust(10) + ' '
454 line += f'{xyz[2]:.5f}'.rjust(10) + ' '
455 line += f'{nuc_charge:5s}'.rjust(10) + ' '
456 line += f'{mass:.5f}'.rjust(10) + ' '
458 fd.write(line)
459 fd.write('\n')
461 # routine to write basis set inormation, including ecps and auxis
462 def _write_basis(self, fd, atoms, basis={}, string='BASIS'):
463 """Write basis set, ECPs, AUXIS, or AUGMENT basis
465 Parameters:
466 - f: An open file object.
467 - atoms: An atoms object.
468 - basis: A dictionary specifying the basis set
469 - string: 'BASIS', 'ECP','AUXIS' or 'AUGMENT'
470 """
472 # basis for all atoms
473 line = f'{string}'.ljust(10)
475 if 'all' in basis:
476 default_basis = basis['all']
477 line += f'({default_basis})'.rjust(16)
479 fd.write(line)
480 fd.write('\n')
482 # basis for all atomic species
483 chemical_symbols = atoms.get_chemical_symbols()
484 chemical_symbols_set = set(chemical_symbols)
486 for i in range(chemical_symbols_set.__len__()):
487 symbol = chemical_symbols_set.pop()
489 if symbol in basis:
490 line = f'{symbol}'.ljust(10)
491 line += f'({basis[symbol]})'.rjust(16)
492 fd.write(line)
493 fd.write('\n')
495 # basis for individual atoms
496 for i in range(len(atoms)):
498 if i in basis:
499 symbol = str(chemical_symbols[i])
500 symbol += str(i + 1)
502 line = f'{symbol}'.ljust(10)
503 line += f'({basis[i]})'.rjust(16)
504 fd.write(line)
505 fd.write('\n')
507 # Analysis routines
508 def read_results(self):
509 """Read the results from output files."""
510 self.read_energy()
511 self.read_forces(self.atoms)
512 self.read_eigenvalues()
513 self.read_dipole()
514 self.read_xray()
516 def read_energy(self):
517 """Read energy from deMon's text-output file."""
518 with open(self.label + '/deMon.out') as fd:
519 text = fd.read().upper()
521 lines = iter(text.split('\n'))
523 for line in lines:
524 if line.startswith(' TOTAL ENERGY ='):
525 self.results['energy'] = float(line.split()[-1]) * Hartree
526 break
527 else:
528 raise RuntimeError
530 def read_forces(self, atoms):
531 """Read the forces from the deMon.out file."""
533 natoms = len(atoms)
534 filename = self.label + '/deMon.out'
536 if op.isfile(filename):
537 with open(filename) as fd:
538 lines = fd.readlines()
540 # find line where the orbitals start
541 flag_found = False
542 for i in range(len(lines)):
543 if lines[i].rfind('GRADIENTS OF TIME STEP 0 IN A.U.') > -1:
544 start = i + 4
545 flag_found = True
546 break
548 if flag_found:
549 self.results['forces'] = np.zeros((natoms, 3), float)
550 for i in range(natoms):
551 line = [s for s in lines[i + start].strip().split(' ')
552 if len(s) > 0]
553 f = -np.array([float(x) for x in line[2:5]])
554 self.results['forces'][i, :] = f * (Hartree / Bohr)
556 def read_eigenvalues(self):
557 """Read eigenvalues from the 'deMon.out' file."""
558 assert os.access(self.label + '/deMon.out', os.F_OK)
560 # Read eigenvalues
561 with open(self.label + '/deMon.out') as fd:
562 lines = fd.readlines()
564 # try PRINT MOE
565 eig_alpha, occ_alpha = self.read_eigenvalues_one_spin(
566 lines, 'ALPHA MO ENERGIES', 6)
567 eig_beta, occ_beta = self.read_eigenvalues_one_spin(
568 lines, 'BETA MO ENERGIES', 6)
570 # otherwise try PRINT MOS
571 if len(eig_alpha) == 0 and len(eig_beta) == 0:
572 eig_alpha, occ_alpha = self.read_eigenvalues_one_spin(
573 lines, 'ALPHA MO COEFFICIENTS', 5)
574 eig_beta, occ_beta = self.read_eigenvalues_one_spin(
575 lines, 'BETA MO COEFFICIENTS', 5)
577 self.results['eigenvalues'] = np.array([eig_alpha, eig_beta]) * Hartree
578 self.results['occupations'] = np.array([occ_alpha, occ_beta])
580 def read_eigenvalues_one_spin(self, lines, string, neigs_per_line):
581 """Utility method for retreiving eigenvalues after the string "string"
582 with neigs_per_line eigenvlaues written per line
583 """
584 eig = []
585 occ = []
587 skip_line = False
588 more_eigs = False
590 # find line where the orbitals start
591 for i in range(len(lines)):
592 if lines[i].rfind(string) > -1:
593 ii = i
594 more_eigs = True
595 break
597 while more_eigs:
598 # search for two empty lines in a row preceding a line with
599 # numbers
600 for i in range(ii + 1, len(lines)):
601 if len(lines[i].split()) == 0 and \
602 len(lines[i + 1].split()) == 0 and \
603 len(lines[i + 2].split()) > 0:
604 ii = i + 2
605 break
607 # read eigenvalues, occupations
608 line = lines[ii].split()
609 if len(line) < neigs_per_line:
610 # last row
611 more_eigs = False
612 if line[0] != str(len(eig) + 1):
613 more_eigs = False
614 skip_line = True
616 if not skip_line:
617 line = lines[ii + 1].split()
618 for l in line:
619 eig.append(float(l))
620 line = lines[ii + 3].split()
621 for l in line:
622 occ.append(float(l))
623 ii = ii + 3
625 return eig, occ
627 def read_dipole(self):
628 """Read dipole moment."""
629 dipole = np.zeros(3)
630 with open(self.label + '/deMon.out') as fd:
631 lines = fd.readlines()
633 for i in range(len(lines)):
634 if lines[i].rfind('DIPOLE') > - \
635 1 and lines[i].rfind('XAS') == -1:
636 dipole[0] = float(lines[i + 1].split()[3])
637 dipole[1] = float(lines[i + 2].split()[3])
638 dipole[2] = float(lines[i + 3].split()[3])
640 # debye to e*Ang
641 self.results['dipole'] = dipole * 0.2081943482534
643 break
645 def read_xray(self):
646 """Read deMon.xry if present."""
648 # try to read core IP from, .out file
649 filename = self.label + '/deMon.out'
650 core_IP = None
651 if op.isfile(filename):
652 with open(filename) as fd:
653 lines = fd.readlines()
655 for i in range(len(lines)):
656 if lines[i].rfind('IONIZATION POTENTIAL') > -1:
657 core_IP = float(lines[i].split()[3])
659 try:
660 mode, ntrans, E_trans, osc_strength, trans_dip = parse_xray(
661 self.label + '/deMon.xry')
662 except ReadError:
663 pass
664 else:
665 xray_results = {'xray_mode': mode,
666 'ntrans': ntrans,
667 'E_trans': E_trans,
668 'osc_strength': osc_strength, # units?
669 'trans_dip': trans_dip, # units?
670 'core_IP': core_IP}
672 self.results['xray'] = xray_results
674 def deMon_inp_to_atoms(self, filename):
675 """Routine to read deMon.inp and convert it to an atoms object."""
677 with open(filename) as fd:
678 lines = fd.readlines()
680 # find line where geometry starts
681 for i in range(len(lines)):
682 if lines[i].rfind('GEOMETRY') > -1:
683 if lines[i].rfind('ANGSTROM'):
684 coord_units = 'Ang'
685 elif lines.rfind('Bohr'):
686 coord_units = 'Bohr'
687 ii = i
688 break
690 chemical_symbols = []
691 xyz = []
692 atomic_numbers = []
693 masses = []
695 for i in range(ii + 1, len(lines)):
696 try:
697 line = lines[i].split()
699 if len(line) > 0:
700 for symbol in ase.data.chemical_symbols:
701 found = None
702 if line[0].upper().rfind(symbol.upper()) > -1:
703 found = symbol
704 break
706 if found is not None:
707 chemical_symbols.append(found)
708 else:
709 break
711 xyz.append(
712 [float(line[1]), float(line[2]), float(line[3])])
714 if len(line) > 4:
715 atomic_numbers.append(int(line[4]))
717 if len(line) > 5:
718 masses.append(float(line[5]))
720 except Exception: # XXX Which kind of exception?
721 raise RuntimeError
723 if coord_units == 'Bohr':
724 xyz *= Bohr
726 natoms = len(chemical_symbols)
728 # set atoms object
729 atoms = ase.Atoms(symbols=chemical_symbols, positions=xyz)
731 # if atomic numbers were read in, set them
732 if len(atomic_numbers) == natoms:
733 atoms.set_atomic_numbers(atomic_numbers)
735 # if masses were read in, set them
736 if len(masses) == natoms:
737 atoms.set_masses(masses)
739 return atoms