Coverage for /builds/debichem-team/python-ase/ase/calculators/demon/demon.py: 40.85%
355 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +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 (
16 CalculatorSetupError,
17 FileIOCalculator,
18 Parameters,
19 ReadError,
20 all_changes,
21 equal,
22)
23from ase.units import Bohr, Hartree
25from .demon_io import parse_xray
27m_e_to_amu = 1822.88839
30class Parameters_deMon(Parameters):
31 """Parameters class for the calculator.
32 Documented in Base_deMon.__init__
34 The options here are the most important ones that the user needs to be
35 aware of. Further options accepted by deMon can be set in the dictionary
36 input_arguments.
38 """
40 def __init__(
41 self,
42 label='rundir',
43 atoms=None,
44 restart=None,
45 basis_path=None,
46 ignore_bad_restart_file=FileIOCalculator._deprecated,
47 deMon_restart_path='.',
48 title='deMon input file',
49 scftype='RKS',
50 forces=False,
51 dipole=False,
52 xc='VWN',
53 guess='TB',
54 print_out='MOE',
55 basis={},
56 ecps={},
57 mcps={},
58 auxis={},
59 augment={},
60 input_arguments=None):
61 kwargs = locals()
62 kwargs.pop('self')
63 Parameters.__init__(self, **kwargs)
66class Demon(FileIOCalculator):
67 """Calculator interface to the deMon code. """
69 implemented_properties = [
70 'energy',
71 'forces',
72 'dipole',
73 'eigenvalues']
75 def __init__(self, *, command=None, **kwargs):
76 """ASE interface to the deMon code.
78 The deMon2k code can be obtained from http://www.demon-software.com
80 The DEMON_COMMAND environment variable must be set to run the
81 executable, in bash it would be set along the lines of
82 export DEMON_COMMAND="deMon.4.3.6.std > deMon_ase.out 2>&1"
84 Parameters:
86 label : str
87 relative path to the run directory
88 atoms : Atoms object
89 the atoms object
90 command : str
91 Command to run deMon. If not present the environment
92 variable DEMON_COMMAND will be used
93 restart : str
94 Relative path to ASE restart directory for parameters and
95 atoms object and results
96 basis_path : str
97 Relative path to the directory containing
98 BASIS, AUXIS, ECPS, MCPS and AUGMENT
99 ignore_bad_restart_file : bool
100 Ignore broken or missing ASE restart files
101 By default, it is an error if the restart
102 file is missing or broken.
103 deMon_restart_path : str
104 Relative path to the deMon restart dir
105 title : str
106 Title in the deMon input file.
107 scftype : str
108 Type of scf
109 forces : bool
110 If True a force calculation will be enforced.
111 dipole : bool
112 If True a dipole calculation will be enforced
113 xc : str
114 xc-functional
115 guess : str
116 guess for initial density and wave functions
117 print_out : str | list
118 Options for the printing in deMon
119 basis : dict
120 Definition of basis sets.
121 ecps : dict
122 Definition of ECPs
123 mcps : dict
124 Definition of MCPs
125 auxis : dict
126 Definition of AUXIS
127 augment : dict
128 Definition of AUGMENT
129 input_arguments : dict
130 Explicitly given input arguments. The key is the input keyword
131 and the value is either a str, a list of str (will be written
132 on the same line as the keyword),
133 or a list of lists of str (first list is written on the first
134 line, the others on following lines.)
136 For example usage, see the tests h2o.py and h2o_xas_xes.py in
137 the directory ase/test/demon
139 """
141 parameters = Parameters_deMon(**kwargs)
143 # Setup the run command
144 if command is None:
145 command = self.cfg.get('DEMON_COMMAND')
147 FileIOCalculator.__init__(
148 self,
149 command=command,
150 **parameters)
152 def __getitem__(self, key):
153 """Convenience method to retrieve a parameter as
154 calculator[key] rather than calculator.parameters[key]
156 Parameters:
157 key : str, the name of the parameters to get.
158 """
159 return self.parameters[key]
161 def set(self, **kwargs):
162 """Set all parameters.
164 Parameters:
165 kwargs : Dictionary containing the keywords for deMon
166 """
167 # Put in the default arguments.
168 kwargs = self.default_parameters.__class__(**kwargs)
170 if 'parameters' in kwargs:
171 filename = kwargs.pop('parameters')
172 parameters = Parameters.read(filename)
173 parameters.update(kwargs)
174 kwargs = parameters
176 changed_parameters = {}
178 for key, value in kwargs.items():
179 oldvalue = self.parameters.get(key)
180 if key not in self.parameters or not equal(value, oldvalue):
181 changed_parameters[key] = value
182 self.parameters[key] = value
184 return changed_parameters
186 def link_file(self, fromdir, todir, filename):
187 if op.exists(todir + '/' + filename):
188 os.remove(todir + '/' + filename)
190 if op.exists(fromdir + '/' + filename):
191 os.symlink(fromdir + '/' + filename,
192 todir + '/' + filename)
193 else:
194 raise RuntimeError(
195 "{} doesn't exist".format(fromdir + '/' + filename))
197 def calculate(self,
198 atoms=None,
199 properties=['energy'],
200 system_changes=all_changes):
201 """Capture the RuntimeError from FileIOCalculator.calculate
202 and add a little debug information from the deMon output.
204 See base FileIocalculator for documentation.
205 """
207 if atoms is not None:
208 self.atoms = atoms.copy()
210 self.write_input(self.atoms, properties, system_changes)
211 command = self.command
213 # basis path
214 basis_path = self.parameters['basis_path']
215 if basis_path is None:
216 basis_path = self.cfg.get('DEMON_BASIS_PATH')
218 if basis_path is None:
219 raise RuntimeError('Please set basis_path keyword,' +
220 ' or the DEMON_BASIS_PATH' +
221 ' environment variable')
223 # link restart file
224 value = self.parameters['guess']
225 if value.upper() == 'RESTART':
226 value2 = self.parameters['deMon_restart_path']
228 if op.exists(self.directory + '/deMon.rst')\
229 or op.islink(self.directory + '/deMon.rst'):
230 os.remove(self.directory + '/deMon.rst')
231 abspath = op.abspath(value2)
233 if op.exists(abspath + '/deMon.mem') \
234 or op.islink(abspath + '/deMon.mem'):
236 shutil.copy(abspath + '/deMon.mem',
237 self.directory + '/deMon.rst')
238 else:
239 raise RuntimeError(
240 "{} doesn't exist".format(abspath + '/deMon.rst'))
242 abspath = op.abspath(basis_path)
244 for name in ['BASIS', 'AUXIS', 'ECPS', 'MCPS', 'FFDS']:
245 self.link_file(abspath, self.directory, name)
247 if command is None:
248 raise CalculatorSetupError
249 subprocess.check_call(command, shell=True, cwd=self.directory)
251 try:
252 self.read_results()
253 except Exception: # XXX Which kind of exception?
254 with open(self.directory + '/deMon.out') as fd:
255 lines = fd.readlines()
256 debug_lines = 10
257 print('##### %d last lines of the deMon.out' % debug_lines)
258 for line in lines[-20:]:
259 print(line.strip())
260 print('##### end of deMon.out')
261 raise RuntimeError
263 def set_label(self, label):
264 """Set label directory """
266 self.label = label
268 # in our case self.directory = self.label
269 self.directory = self.label
270 if self.directory == '':
271 self.directory = os.curdir
273 def write_input(self, atoms, properties=None, system_changes=None):
274 """Write input (in)-file.
275 See calculator.py for further details.
277 Parameters:
278 atoms : The Atoms object to write.
279 properties : The properties which should be calculated.
280 system_changes : List of properties changed since last run.
282 """
283 # Call base calculator.
284 FileIOCalculator.write_input(
285 self,
286 atoms=atoms,
287 properties=properties,
288 system_changes=system_changes)
290 if system_changes is None and properties is None:
291 return
293 filename = f'{self.directory}/deMon.inp'
295 add_print = ''
297 # Start writing the file.
298 with open(filename, 'w') as fd:
300 # write keyword argument keywords
301 value = self.parameters['title']
302 self._write_argument('TITLE', value, fd)
304 fd.write('#\n')
306 value = self.parameters['scftype']
307 self._write_argument('SCFTYPE', value, fd)
309 value = self.parameters['xc']
310 self._write_argument('VXCTYPE', value, fd)
312 value = self.parameters['guess']
313 self._write_argument('GUESS', value, fd)
315 # obtain forces through a single BOMD step
316 # only if forces is in properties, or if keyword forces is True
317 value = self.parameters['forces']
318 if 'forces' in properties or value:
320 self._write_argument('DYNAMICS',
321 ['INT=1', 'MAX=0', 'STEP=0'], fd)
322 self._write_argument('TRAJECTORY', 'FORCES', fd)
323 self._write_argument('VELOCITIES', 'ZERO', fd)
324 add_print = add_print + ' ' + 'MD OPT'
326 # if dipole is True, enforce dipole calculation.
327 # Otherwise only if asked for
328 value = self.parameters['dipole']
329 if 'dipole' in properties or value:
330 self._write_argument('DIPOLE', '', fd)
332 # print argument, here other options could change this
333 value = self.parameters['print_out']
334 assert isinstance(value, str)
335 value = value + add_print
337 if len(value) != 0:
338 self._write_argument('PRINT', value, fd)
339 fd.write('#\n')
341 # write general input arguments
342 self._write_input_arguments(fd)
344 fd.write('#\n')
346 # write basis set, ecps, mcps, auxis, augment
347 basis = self.parameters['basis']
348 if 'all' not in basis:
349 basis['all'] = 'DZVP'
350 self._write_basis(fd, atoms, basis, string='BASIS')
352 ecps = self.parameters['ecps']
353 if len(ecps) != 0:
354 self._write_basis(fd, atoms, ecps, string='ECPS')
356 mcps = self.parameters['mcps']
357 if len(mcps) != 0:
358 self._write_basis(fd, atoms, mcps, string='MCPS')
360 auxis = self.parameters['auxis']
361 if len(auxis) != 0:
362 self._write_basis(fd, atoms, auxis, string='AUXIS')
364 augment = self.parameters['augment']
365 if len(augment) != 0:
366 self._write_basis(fd, atoms, augment, string='AUGMENT')
368 # write geometry
369 self._write_atomic_coordinates(fd, atoms)
371 # write xyz file for good measure.
372 ase.io.write(f'{self.directory}/deMon_atoms.xyz', self.atoms)
374 def read(self, restart_path):
375 """Read parameters from directory restart_path."""
377 self.set_label(restart_path)
379 if not op.exists(restart_path + '/deMon.inp'):
380 raise ReadError('The restart_path file {} does not exist'
381 .format(restart_path))
383 self.atoms = self.deMon_inp_to_atoms(restart_path + '/deMon.inp')
385 self.read_results()
387 def _write_input_arguments(self, fd):
388 """Write directly given input-arguments."""
389 input_arguments = self.parameters['input_arguments']
391 # Early return
392 if input_arguments is None:
393 return
395 for key, value in input_arguments.items():
396 self._write_argument(key, value, fd)
398 def _write_argument(self, key, value, fd):
399 """Write an argument to file.
400 key : a string coresponding to the input keyword
401 value : the arguments, can be a string, a number or a list
402 f : and open file
403 """
405 # for only one argument, write on same line
406 if not isinstance(value, (tuple, list)):
407 line = key.upper()
408 line += ' ' + str(value).upper()
409 fd.write(line)
410 fd.write('\n')
412 # for a list, write first argument on the first line,
413 # then the rest on new lines
414 else:
415 line = key
416 if not isinstance(value[0], (tuple, list)):
417 for i in range(len(value)):
418 line += ' ' + str(value[i].upper())
419 fd.write(line)
420 fd.write('\n')
421 else:
422 for i in range(len(value)):
423 for j in range(len(value[i])):
424 line += ' ' + str(value[i][j]).upper()
425 fd.write(line)
426 fd.write('\n')
427 line = ''
429 def _write_atomic_coordinates(self, fd, atoms):
430 """Write atomic coordinates.
432 Parameters:
433 - f: An open file object.
434 - atoms: An atoms object.
435 """
437 fd.write('#\n')
438 fd.write('# Atomic coordinates\n')
439 fd.write('#\n')
440 fd.write('GEOMETRY CARTESIAN ANGSTROM\n')
442 for i in range(len(atoms)):
443 xyz = atoms.get_positions()[i]
444 chem_symbol = atoms.get_chemical_symbols()[i]
445 chem_symbol += str(i + 1)
447 # if tag is set to 1 then we have a ghost atom,
448 # set nuclear charge to 0
449 if atoms.get_tags()[i] == 1:
450 nuc_charge = str(0)
451 else:
452 nuc_charge = str(atoms.get_atomic_numbers()[i])
454 mass = atoms.get_masses()[i]
456 line = f'{chem_symbol:6s}'.rjust(10) + ' '
457 line += f'{xyz[0]:.5f}'.rjust(10) + ' '
458 line += f'{xyz[1]:.5f}'.rjust(10) + ' '
459 line += f'{xyz[2]:.5f}'.rjust(10) + ' '
460 line += f'{nuc_charge:5s}'.rjust(10) + ' '
461 line += f'{mass:.5f}'.rjust(10) + ' '
463 fd.write(line)
464 fd.write('\n')
466 # routine to write basis set inormation, including ecps and auxis
467 def _write_basis(self, fd, atoms, basis={}, string='BASIS'):
468 """Write basis set, ECPs, AUXIS, or AUGMENT basis
470 Parameters:
471 - f: An open file object.
472 - atoms: An atoms object.
473 - basis: A dictionary specifying the basis set
474 - string: 'BASIS', 'ECP','AUXIS' or 'AUGMENT'
475 """
477 # basis for all atoms
478 line = f'{string}'.ljust(10)
480 if 'all' in basis:
481 default_basis = basis['all']
482 line += f'({default_basis})'.rjust(16)
484 fd.write(line)
485 fd.write('\n')
487 # basis for all atomic species
488 chemical_symbols = atoms.get_chemical_symbols()
489 chemical_symbols_set = set(chemical_symbols)
491 for _ in range(chemical_symbols_set.__len__()):
492 symbol = chemical_symbols_set.pop()
494 if symbol in basis:
495 line = f'{symbol}'.ljust(10)
496 line += f'({basis[symbol]})'.rjust(16)
497 fd.write(line)
498 fd.write('\n')
500 # basis for individual atoms
501 for i in range(len(atoms)):
503 if i in basis:
504 symbol = str(chemical_symbols[i])
505 symbol += str(i + 1)
507 line = f'{symbol}'.ljust(10)
508 line += f'({basis[i]})'.rjust(16)
509 fd.write(line)
510 fd.write('\n')
512 # Analysis routines
513 def read_results(self):
514 """Read the results from output files."""
515 self.read_energy()
516 self.read_forces(self.atoms)
517 self.read_eigenvalues()
518 self.read_dipole()
519 self.read_xray()
521 def read_energy(self):
522 """Read energy from deMon's text-output file."""
523 with open(self.label + '/deMon.out') as fd:
524 text = fd.read().upper()
526 lines = iter(text.split('\n'))
528 for line in lines:
529 if line.startswith(' TOTAL ENERGY ='):
530 self.results['energy'] = float(line.split()[-1]) * Hartree
531 break
532 else:
533 raise RuntimeError
535 def read_forces(self, atoms):
536 """Read the forces from the deMon.out file."""
538 natoms = len(atoms)
539 filename = self.label + '/deMon.out'
541 if op.isfile(filename):
542 with open(filename) as fd:
543 lines = fd.readlines()
545 # find line where the orbitals start
546 flag_found = False
547 for i in range(len(lines)):
548 if lines[i].rfind('GRADIENTS OF TIME STEP 0 IN A.U.') > -1:
549 start = i + 4
550 flag_found = True
551 break
553 if flag_found:
554 self.results['forces'] = np.zeros((natoms, 3), float)
555 for i in range(natoms):
556 line = [s for s in lines[i + start].strip().split(' ')
557 if len(s) > 0]
558 f = -np.array([float(x) for x in line[2:5]])
559 self.results['forces'][i, :] = f * (Hartree / Bohr)
561 def read_eigenvalues(self):
562 """Read eigenvalues from the 'deMon.out' file."""
563 assert os.access(self.label + '/deMon.out', os.F_OK)
565 # Read eigenvalues
566 with open(self.label + '/deMon.out') as fd:
567 lines = fd.readlines()
569 # try PRINT MOE
570 eig_alpha, occ_alpha = self.read_eigenvalues_one_spin(
571 lines, 'ALPHA MO ENERGIES', 6)
572 eig_beta, occ_beta = self.read_eigenvalues_one_spin(
573 lines, 'BETA MO ENERGIES', 6)
575 # otherwise try PRINT MOS
576 if len(eig_alpha) == 0 and len(eig_beta) == 0:
577 eig_alpha, occ_alpha = self.read_eigenvalues_one_spin(
578 lines, 'ALPHA MO COEFFICIENTS', 5)
579 eig_beta, occ_beta = self.read_eigenvalues_one_spin(
580 lines, 'BETA MO COEFFICIENTS', 5)
582 self.results['eigenvalues'] = np.array([eig_alpha, eig_beta]) * Hartree
583 self.results['occupations'] = np.array([occ_alpha, occ_beta])
585 def read_eigenvalues_one_spin(self, lines, string, neigs_per_line):
586 """Utility method for retreiving eigenvalues after the string "string"
587 with neigs_per_line eigenvlaues written per line
588 """
589 eig = []
590 occ = []
592 skip_line = False
593 more_eigs = False
595 # find line where the orbitals start
596 for i in range(len(lines)):
597 if lines[i].rfind(string) > -1:
598 ii = i
599 more_eigs = True
600 break
602 while more_eigs:
603 # search for two empty lines in a row preceding a line with
604 # numbers
605 for i in range(ii + 1, len(lines)):
606 if len(lines[i].split()) == 0 and \
607 len(lines[i + 1].split()) == 0 and \
608 len(lines[i + 2].split()) > 0:
609 ii = i + 2
610 break
612 # read eigenvalues, occupations
613 line = lines[ii].split()
614 if len(line) < neigs_per_line:
615 # last row
616 more_eigs = False
617 if line[0] != str(len(eig) + 1):
618 more_eigs = False
619 skip_line = True
621 if not skip_line:
622 line = lines[ii + 1].split()
623 for l in line:
624 eig.append(float(l))
625 line = lines[ii + 3].split()
626 for l in line:
627 occ.append(float(l))
628 ii = ii + 3
630 return eig, occ
632 def read_dipole(self):
633 """Read dipole moment."""
634 dipole = np.zeros(3)
635 with open(self.label + '/deMon.out') as fd:
636 lines = fd.readlines()
638 for i in range(len(lines)):
639 if lines[i].rfind('DIPOLE') > - \
640 1 and lines[i].rfind('XAS') == -1:
641 dipole[0] = float(lines[i + 1].split()[3])
642 dipole[1] = float(lines[i + 2].split()[3])
643 dipole[2] = float(lines[i + 3].split()[3])
645 # debye to e*Ang
646 self.results['dipole'] = dipole * 0.2081943482534
648 break
650 def read_xray(self):
651 """Read deMon.xry if present."""
653 # try to read core IP from, .out file
654 filename = self.label + '/deMon.out'
655 core_IP = None
656 if op.isfile(filename):
657 with open(filename) as fd:
658 lines = fd.readlines()
660 for i in range(len(lines)):
661 if lines[i].rfind('IONIZATION POTENTIAL') > -1:
662 core_IP = float(lines[i].split()[3])
664 try:
665 mode, ntrans, E_trans, osc_strength, trans_dip = parse_xray(
666 self.label + '/deMon.xry')
667 except ReadError:
668 pass
669 else:
670 xray_results = {'xray_mode': mode,
671 'ntrans': ntrans,
672 'E_trans': E_trans,
673 'osc_strength': osc_strength, # units?
674 'trans_dip': trans_dip, # units?
675 'core_IP': core_IP}
677 self.results['xray'] = xray_results
679 def deMon_inp_to_atoms(self, filename):
680 """Routine to read deMon.inp and convert it to an atoms object."""
682 with open(filename) as fd:
683 lines = fd.readlines()
685 # find line where geometry starts
686 for i in range(len(lines)):
687 if lines[i].rfind('GEOMETRY') > -1:
688 if lines[i].rfind('ANGSTROM'):
689 coord_units = 'Ang'
690 elif lines.rfind('Bohr'):
691 coord_units = 'Bohr'
692 ii = i
693 break
695 chemical_symbols = []
696 xyz = []
697 atomic_numbers = []
698 masses = []
700 for i in range(ii + 1, len(lines)):
701 try:
702 line = lines[i].split()
704 if len(line) > 0:
705 for symbol in ase.data.chemical_symbols:
706 found = None
707 if line[0].upper().rfind(symbol.upper()) > -1:
708 found = symbol
709 break
711 if found is not None:
712 chemical_symbols.append(found)
713 else:
714 break
716 xyz.append(
717 [float(line[1]), float(line[2]), float(line[3])])
719 if len(line) > 4:
720 atomic_numbers.append(int(line[4]))
722 if len(line) > 5:
723 masses.append(float(line[5]))
725 except Exception: # XXX Which kind of exception?
726 raise RuntimeError
728 if coord_units == 'Bohr':
729 xyz *= Bohr
731 natoms = len(chemical_symbols)
733 # set atoms object
734 atoms = ase.Atoms(symbols=chemical_symbols, positions=xyz)
736 # if atomic numbers were read in, set them
737 if len(atomic_numbers) == natoms:
738 atoms.set_atomic_numbers(atomic_numbers)
740 # if masses were read in, set them
741 if len(masses) == natoms:
742 atoms.set_masses(masses)
744 return atoms