Coverage for /builds/ase/ase/ase/calculators/demonnano.py: 44.00%
150 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3# flake8: noqa
4"""This module defines an ASE interface to deMon-nano.
6Link to the open-source DFTB code deMon-nano:
7http://demon-nano.ups-tlse.fr/
9export ASE_DEMONNANO_COMMAND="/path/to/bin/deMon.username.x"
10export DEMONNANO_BASIS_PATH="/path/to/basis/"
12The file 'deMon.inp' contains the input geometry and parameters
13The file 'deMon.out' contains the results
15"""
16import os
17import os.path as op
18# import subprocess
19import pathlib as pl
21import numpy as np
23import ase.data
24import ase.io
25from ase.calculators.calculator import FileIOCalculator, Parameters, ReadError
26from ase.units import Bohr, Hartree
29class DemonNanoParameters(Parameters):
30 """Parameters class for the calculator.
32 The options here are the most important ones that the user needs to be
33 aware of. Further options accepted by deMon can be set in the dictionary
34 input_arguments.
36 """
38 def __init__(
39 self,
40 label='.',
41 atoms=None,
42 command=None,
43 basis_path=None,
44 restart_path='.',
45 print_out='ASE',
46 title='deMonNano input file',
47 forces=False,
48 input_arguments=None):
49 kwargs = locals()
50 kwargs.pop('self')
51 Parameters.__init__(self, **kwargs)
54class DemonNano(FileIOCalculator):
55 """Calculator interface to the deMon-nano code. """
57 implemented_properties = ['energy', 'forces']
59 def __init__(self, **kwargs):
60 """ASE interface to the deMon-nano code.
62 The deMon-nano code can be obtained from http://demon-nano.ups-tlse.fr/
64 The ASE_DEMONNANO_COMMAND environment variable must be set to run the executable, in bash it would be set along the lines of
65 export ASE_DEMONNANO_COMMAND="pathway-to-deMon-binary/deMon.username.x"
67 Parameters:
69 label : str
70 relative path to the run directory
71 atoms : Atoms object
72 the atoms object
73 command : str
74 Command to run deMon. If not present, the environment variable ASE_DEMONNANO_COMMAND is used
75 basis_path : str
76 Relative path to the directory containing DFTB-SCC or DFTB-0 parameters
77 If not present, the environment variable DEMONNANO_BASIS_PATH is used
78 restart_path : str
79 Relative path to the deMon restart dir
80 title : str
81 Title in the deMon input file.
82 forces : bool
83 If True a force calculation is enforced
84 print_out : str | list
85 Options for the printing in deMon
86 input_arguments : dict
87 Explicitly given input arguments. The key is the input keyword
88 and the value is either a str, a list of str (will be written on the same line as the keyword),
89 or a list of lists of str (first list is written on the first line, the others on following lines.)
90 """
92 parameters = DemonNanoParameters(**kwargs)
94 # basis path
95 basis_path = parameters['basis_path']
96 if basis_path is None:
97 basis_path = self.cfg.get('DEMONNANO_BASIS_PATH')
99 if basis_path is None:
100 mess = 'The "DEMONNANO_BASIS_PATH" environment is not defined.'
101 raise ValueError(mess)
102 else:
103 parameters['basis_path'] = basis_path
105 # Call the base class.
106 FileIOCalculator.__init__(
107 self,
108 **parameters)
110 def __getitem__(self, key):
111 """Convenience method to retrieve a parameter as
112 calculator[key] rather than calculator.parameters[key]
114 Parameters:
115 key : str, the name of the parameters to get.
116 """
117 return self.parameters[key]
119 def write_input(self, atoms, properties=None, system_changes=None):
120 """Write input (in)-file.
121 See calculator.py for further details.
123 Parameters:
124 atoms : The Atoms object to write.
125 properties : The properties which should be calculated.
126 system_changes : List of properties changed since last run.
128 """
129 # Call base calculator.
130 FileIOCalculator.write_input(
131 self,
132 atoms=atoms,
133 properties=properties,
134 system_changes=system_changes)
136 if system_changes is None and properties is None:
137 return
139 filename = self.label + '/deMon.inp'
141 # Start writing the file.
142 with open(filename, 'w') as fd:
143 # write keyword argument keywords
144 value = self.parameters['title']
145 self._write_argument('TITLE', value, fd)
146 fd.write('\n')
148 # obtain forces through a single BOMD step
149 # only if forces is in properties, or if keyword forces is True
150 value = self.parameters['forces']
151 if 'forces' in properties or value:
152 self._write_argument('MDYNAMICS', 'ZERO', fd)
153 self._write_argument('MDSTEP', 'MAX=1', fd)
154 # default timestep is 0.25 fs if not enough - uncomment the line below
155 # self._write_argument('TIMESTEP', '0.1', fd)
157 # print argument, here other options could change this
158 value = self.parameters['print_out']
159 assert (isinstance(value, str))
161 if len(value) != 0:
162 self._write_argument('PRINT', value, fd)
163 fd.write('\n')
165 # write general input arguments
166 self._write_input_arguments(fd)
168 if 'BASISPATH' not in self.parameters['input_arguments']:
169 value = self.parameters['basis_path']
170 fd.write(value)
171 fd.write('\n')
173 # write geometry
174 self._write_atomic_coordinates(fd, atoms)
176 # write xyz file for good measure.
177 ase.io.write(self.label + '/deMon_atoms.xyz', self.atoms)
179 def read(self, restart_path):
180 """Read parameters from directory restart_path."""
182 self.set_label(restart_path)
183 rpath = pl.Path(restart_path)
185 if not (rpath / 'deMon.inp').exists():
186 raise ReadError('The restart_path file {} does not exist'
187 .format(rpath))
189 self.atoms = self.deMon_inp_to_atoms(rpath / 'deMon.inp')
191 self.read_results()
193 def _write_input_arguments(self, fd):
194 """Write directly given input-arguments."""
195 input_arguments = self.parameters['input_arguments']
197 # Early return
198 if input_arguments is None:
199 return
201 for key, value in input_arguments.items():
202 self._write_argument(key, value, fd)
204 def _write_argument(self, key, value, fd):
205 """Write an argument to file.
206 key : a string coresponding to the input keyword
207 value : the arguments, can be a string, a number or a list
208 fd : and open file
209 """
210 if key == 'BASISPATH':
211 # Write a basis path to file.
212 # Has to be in lowercase for deMon-nano to work
213 line = value.lower()
214 fd.write(line)
215 fd.write('\n')
216 elif not isinstance(value, (tuple, list)):
217 # for only one argument, write on same line
218 line = key.upper()
219 line += ' ' + str(value).upper()
220 fd.write(line)
221 fd.write('\n')
223 # for a list, write first argument on the first line,
224 # then the rest on new lines
225 else:
226 line = key
227 if not isinstance(value[0], (tuple, list)):
228 for i in range(len(value)):
229 line += ' ' + str(value[i].upper())
230 fd.write(line)
231 fd.write('\n')
232 else:
233 for i in range(len(value)):
234 for j in range(len(value[i])):
235 line += ' ' + str(value[i][j]).upper()
236 fd.write(line)
237 fd.write('\n')
238 line = ''
240 def _write_atomic_coordinates(self, fd, atoms):
241 """Write atomic coordinates.
242 Parameters:
243 - fd: An open file object.
244 - atoms: An atoms object.
245 """
246 # fd.write('#\n')
247 # fd.write('# Atomic coordinates\n')
248 # fd.write('#\n')
249 fd.write('GEOMETRY CARTESIAN ANGSTROM\n')
251 for sym, pos in zip(atoms.symbols, atoms.positions):
252 fd.write('{:9s} {:10.5f} {:10.5f} {:10.5f}\n'.format(sym, *pos))
254 fd.write('\n')
256# Analysis routines
257 def read_results(self):
258 """Read the results from output files."""
259 self.read_energy()
260 self.read_forces(self.atoms)
261 # self.read_eigenvalues()
263 def read_energy(self):
264 """Read energy from deMon.ase output file."""
266 epath = pl.Path(self.label)
268 if not (epath / 'deMon.ase').exists():
269 raise ReadError('The deMonNano output file for ASE {} does not exist'
270 .format(epath))
272 filename = self.label + '/deMon.ase'
274 if op.isfile(filename):
275 with open(filename) as fd:
276 lines = fd.readlines()
278 for i in range(len(lines)):
279 if lines[i].startswith(' DFTB total energy [Hartree]'):
280 self.results['energy'] = float(lines[i + 1]) * Hartree
281 break
283 def read_forces(self, atoms):
284 """Read forces from the deMon.ase file."""
286 natoms = len(atoms)
287 epath = pl.Path(self.label)
289 if not (epath / 'deMon.ase').exists():
290 raise ReadError('The deMonNano output file for ASE {} does not exist'
291 .format(epath))
293 filename = self.label + '/deMon.ase'
295 with open(filename) as fd:
296 lines = fd.readlines()
298 # find line where the forces start
299 flag_found = False
300 for i in range(len(lines)):
301 if 'DFTB gradients at 0 time step in a.u.' in lines[i]:
302 start = i + 1
303 flag_found = True
304 break
306 if flag_found:
307 self.results['forces'] = np.zeros((natoms, 3), float)
308 for i in range(natoms):
309 line = [s for s in lines[i + start].strip().split(' ')
310 if len(s) > 0]
311 f = -np.array([float(x) for x in line[1:4]])
312 # output forces in a.u.
313 # self.results['forces'][i, :] = f
314 # output forces with real dimension
315 self.results['forces'][i, :] = f * (Hartree / Bohr)
317 def deMon_inp_to_atoms(self, filename):
318 """Routine to read deMon.inp and convert it to an atoms object."""
320 read_flag = False
321 chem_symbols = []
322 xyz = []
324 with open(filename) as fd:
325 for line in fd:
326 if 'GEOMETRY' in line:
327 read_flag = True
328 if 'ANGSTROM' in line:
329 coord_units = 'Ang'
330 elif 'BOHR' in line:
331 coord_units = 'Bohr'
333 if read_flag:
334 tokens = line.split()
335 symbol = tokens[0]
336 xyz_loc = np.array(tokens[1:4]).astype(float)
337 if read_flag and tokens:
338 chem_symbols.append(symbol)
339 xyz.append(xyz_loc)
341 if coord_units == 'Bohr':
342 xyz *= Bohr
344 # set atoms object
345 atoms = ase.Atoms(symbols=chem_symbols, positions=xyz)
347 return atoms