Coverage for /builds/ase/ase/ase/calculators/aims.py: 42.28%
123 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"""This module defines an ASE interface to FHI-aims.
5Felix Hanke hanke@liverpool.ac.uk
6Jonas Bjork j.bjork@liverpool.ac.uk
7Simon P. Rittmeyer simon.rittmeyer@tum.de
9Edits on (24.11.2021) by Thomas A. R. Purcell purcell@fhi-berlin.mpg.de
10"""
12import os
13import re
15import numpy as np
17from ase.calculators.genericfileio import (
18 BaseProfile,
19 CalculatorTemplate,
20 GenericFileIOCalculator,
21 read_stdout,
22)
23from ase.io.aims import write_aims, write_control
26def get_aims_version(string):
27 match = re.search(r'\s*FHI-aims version\s*:\s*(\S+)', string, re.M)
28 return match.group(1)
31class AimsProfile(BaseProfile):
32 configvars = {'default_species_directory'}
34 def __init__(self, command, default_species_directory=None, **kwargs):
35 super().__init__(command, **kwargs)
36 self.default_species_directory = default_species_directory
38 def get_calculator_command(self, inputfile):
39 return []
41 def version(self):
42 return get_aims_version(read_stdout(self._split_command))
45class AimsTemplate(CalculatorTemplate):
46 _label = 'aims'
48 def __init__(self):
49 super().__init__(
50 'aims',
51 [
52 'energy',
53 'free_energy',
54 'forces',
55 'stress',
56 'stresses',
57 'dipole',
58 'magmom',
59 ],
60 )
62 self.outputname = f'{self._label}.out'
63 self.errorname = f'{self._label}.err'
65 def update_parameters(self, properties, parameters):
66 """Check and update the parameters to match the desired calculation
68 Parameters
69 ----------
70 properties: list of str
71 The list of properties to calculate
72 parameters: dict
73 The parameters used to perform the calculation.
75 Returns
76 -------
77 dict
78 The updated parameters object
79 """
80 parameters = dict(parameters)
81 property_flags = {
82 'forces': 'compute_forces',
83 'stress': 'compute_analytical_stress',
84 'stresses': 'compute_heat_flux',
85 }
86 # Ensure FHI-aims will calculate all desired properties
87 for property in properties:
88 aims_name = property_flags.get(property, None)
89 if aims_name is not None:
90 parameters[aims_name] = True
92 if 'dipole' in properties:
93 if 'output' in parameters and 'dipole' not in parameters['output']:
94 parameters['output'] = list(parameters['output'])
95 parameters['output'].append('dipole')
96 elif 'output' not in parameters:
97 parameters['output'] = ['dipole']
99 return parameters
101 def write_input(self, profile, directory, atoms, parameters, properties):
102 """Write the geometry.in and control.in files for the calculation
104 Parameters
105 ----------
106 directory : Path
107 The working directory to store the input files.
108 atoms : atoms.Atoms
109 The atoms object to perform the calculation on.
110 parameters: dict
111 The parameters used to perform the calculation.
112 properties: list of str
113 The list of properties to calculate
114 """
115 parameters = self.update_parameters(properties, parameters)
117 ghosts = parameters.pop('ghosts', None)
118 geo_constrain = parameters.pop('geo_constrain', None)
119 scaled = parameters.pop('scaled', None)
120 write_velocities = parameters.pop('write_velocities', None)
122 if scaled is None:
123 scaled = np.all(atoms.pbc)
124 if write_velocities is None:
125 write_velocities = atoms.has('momenta')
127 if geo_constrain is None:
128 geo_constrain = scaled and 'relax_geometry' in parameters
130 have_lattice_vectors = atoms.pbc.any()
131 have_k_grid = (
132 'k_grid' in parameters
133 or 'kpts' in parameters
134 or 'k_grid_density' in parameters
135 )
136 if have_lattice_vectors and not have_k_grid:
137 raise RuntimeError('Found lattice vectors but no k-grid!')
138 if not have_lattice_vectors and have_k_grid:
139 raise RuntimeError('Found k-grid but no lattice vectors!')
141 geometry_in = directory / 'geometry.in'
143 write_aims(
144 geometry_in,
145 atoms,
146 scaled,
147 geo_constrain,
148 write_velocities=write_velocities,
149 ghosts=ghosts,
150 )
152 control = directory / 'control.in'
154 if (
155 'species_dir' not in parameters
156 and profile.default_species_directory is not None
157 ):
158 parameters['species_dir'] = profile.default_species_directory
160 write_control(control, atoms, parameters)
162 def execute(self, directory, profile):
163 profile.run(directory, None, self.outputname, errorfile=self.errorname)
165 def read_results(self, directory):
166 from ase.io.aims import read_aims_results
168 dst = directory / self.outputname
169 return read_aims_results(dst, index=-1)
171 def load_profile(self, cfg, **kwargs):
172 return AimsProfile.from_config(cfg, self.name, **kwargs)
174 def socketio_argv(self, profile, unixsocket, port):
175 return profile._split_command
177 def socketio_parameters(self, unixsocket, port):
178 if port:
179 use_pimd_wrapper = ('localhost', port)
180 else:
181 # (INET port number should be unused.)
182 use_pimd_wrapper = (f'UNIX:{unixsocket}', 31415)
184 return dict(use_pimd_wrapper=use_pimd_wrapper, compute_forces=True)
187class Aims(GenericFileIOCalculator):
188 def __init__(
189 self,
190 profile=None,
191 directory='.',
192 **kwargs,
193 ):
194 """Construct the FHI-aims calculator.
196 The keyword arguments (kwargs) can be one of the ASE standard
197 keywords: 'xc', 'kpts' and 'smearing' or any of FHI-aims'
198 native keywords.
201 Arguments:
203 cubes: AimsCube object
204 Cube file specification.
206 tier: int or array of ints
207 Set basis set tier for all atomic species.
209 plus_u : dict
210 For DFT+U. Adds a +U term to one specific shell of the species.
212 kwargs : dict
213 Any of the base class arguments.
215 """
217 super().__init__(
218 template=AimsTemplate(),
219 profile=profile,
220 parameters=kwargs,
221 directory=directory,
222 )
225class AimsCube:
226 'Object to ensure the output of cube files, can be attached to Aims object'
228 def __init__(
229 self,
230 origin=(0, 0, 0),
231 edges=[(0.1, 0.0, 0.0), (0.0, 0.1, 0.0), (0.0, 0.0, 0.1)],
232 points=(50, 50, 50),
233 plots=(),
234 ):
235 """parameters:
237 origin, edges, points:
238 Same as in the FHI-aims output
239 plots:
240 what to print, same names as in FHI-aims"""
242 self.name = 'AimsCube'
243 self.origin = origin
244 self.edges = edges
245 self.points = points
246 self.plots = plots
248 def ncubes(self):
249 """returns the number of cube files to output"""
250 return len(self.plots)
252 def move_to_base_name(self, basename):
253 """when output tracking is on or the base namem is not standard,
254 this routine will rename add the base to the cube file output for
255 easier tracking"""
256 for plot in self.plots:
257 found = False
258 cube = plot.split()
259 if (
260 cube[0] == 'total_density'
261 or cube[0] == 'spin_density'
262 or cube[0] == 'delta_density'
263 ):
264 found = True
265 old_name = cube[0] + '.cube'
266 new_name = basename + '.' + old_name
267 if cube[0] == 'eigenstate' or cube[0] == 'eigenstate_density':
268 found = True
269 state = int(cube[1])
270 s_state = cube[1]
271 for i in [10, 100, 1000, 10000]:
272 if state < i:
273 s_state = '0' + s_state
274 old_name = cube[0] + '_' + s_state + '_spin_1.cube'
275 new_name = basename + '.' + old_name
276 if found:
277 # XXX Should not use platform dependent commands!
278 os.system('mv ' + old_name + ' ' + new_name)
280 def add_plot(self, name):
281 """in case you forgot one ..."""
282 self.plots += [name]
284 def write(self, file):
285 """write the necessary output to the already opened control.in"""
286 file.write('output cube ' + self.plots[0] + '\n')
287 file.write(' cube origin ')
288 for ival in self.origin:
289 file.write(str(ival) + ' ')
290 file.write('\n')
291 for i in range(3):
292 file.write(' cube edge ' + str(self.points[i]) + ' ')
293 for ival in self.edges[i]:
294 file.write(str(ival) + ' ')
295 file.write('\n')
296 if self.ncubes() > 1:
297 for i in range(self.ncubes() - 1):
298 file.write('output cube ' + self.plots[i + 1] + '\n')