Coverage for ase / calculators / exciting / exciting.py: 87.50%
72 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
1# fmt: off
3"""ASE Calculator for the ground state exciting DFT code.
5Exciting calculator class in this file allow for writing exciting input
6files using ASE Atoms object that allow for the compiled exciting binary
7to run DFT on the geometry/material defined in the Atoms object. Also gives
8access to developer to a lightweight parser (lighter weight than NOMAD or
9the exciting parser in the exciting repository) to capture ground state
10properties.
12Note: excitingtools must be installed using `pip install excitingtools` to
13use this calculator.
14"""
16from os import PathLike
17from pathlib import Path
18from typing import Any, Dict, Mapping, Optional
20import ase.io.exciting
21from ase.calculators.calculator import PropertyNotImplementedError
22from ase.calculators.exciting.runner import (
23 SimpleBinaryRunner,
24 SubprocessRunResults,
25)
26from ase.calculators.genericfileio import (
27 BaseProfile,
28 CalculatorTemplate,
29 GenericFileIOCalculator,
30)
33class ExcitingProfile(BaseProfile):
34 """Defines all quantities that are configurable for a given machine.
36 Follows the generic pattern BUT currently not used by our calculator as:
37 * species_path is part of the input file in exciting.
38 * OnlyTypo fix part of the profile used in the base class is the run
39 method, which is part of the BinaryRunner class.
40 """
41 configvars = {'species_path'}
43 def __init__(self, command, species_path=None, **kwargs):
44 super().__init__(command, **kwargs)
46 self.species_path = species_path
48 def version(self):
49 """Return exciting version."""
50 # TARP No way to get the version for the binary in use
51 return
53 # Machine specific config files in the config
54 # species_file goes in the config
55 # binary file in the config.
56 # options for that, parallel info dictionary.
57 # Number of threads and stuff like that.
59 def get_calculator_command(self, input_file):
60 """Returns command to run binary as a list of strings."""
61 # input_file unused for exciting, it looks for input.xml in run
62 # directory.
63 if input_file is None:
64 return []
65 else:
66 return [str(input_file)]
69class ExcitingGroundStateTemplate(CalculatorTemplate):
70 """Template for Ground State Exciting Calculator
72 Abstract methods inherited from the base class:
73 * write_input
74 * execute
75 * read_results
76 """
78 parser = {'info.xml': ase.io.exciting.parse_output}
79 output_names = list(parser)
80 # Use frozenset since the CalculatorTemplate enforces it.
81 implemented_properties = frozenset(['energy', 'forces'])
82 _label = 'exciting'
84 def __init__(self):
85 """Initialise with constant class attributes.
87 :param program_name: The DFT program, should always be exciting.
88 :param implemented_properties: What properties should exciting
89 calculate/read from output.
90 """
91 super().__init__('exciting', self.implemented_properties)
92 self.errorname = f'{self._label}.err'
94 @staticmethod
95 def _require_forces(input_parameters):
96 """Expect ASE always wants forces, enforce setting in input_parameters.
98 :param input_parameters: exciting ground state input parameters, either
99 as a dictionary or ExcitingGroundStateInput.
100 :return: Ground state input parameters, with "compute
101 forces" set to true.
102 """
103 from excitingtools import ExcitingGroundStateInput
105 input_parameters = ExcitingGroundStateInput(input_parameters)
106 input_parameters.tforce = True
107 return input_parameters
109 def write_input(
110 self,
111 profile: ExcitingProfile, # ase test linter enforces method signatures
112 # be consistent with the
113 # abstract method that it implements
114 directory: PathLike,
115 atoms: ase.Atoms,
116 parameters: dict,
117 properties=None,
118 ):
119 """Write an exciting input.xml file based on the input args.
121 :param profile: an Exciting code profile
122 :param directory: Directory in which to run calculator.
123 :param atoms: ASE atoms object.
124 :param parameters: exciting ground state input parameters, in a
125 dictionary. Expect species_path, title and ground_state data,
126 either in an object or as dict.
127 :param properties: Base method's API expects the physical properties
128 expected from a ground state calculation, for example energies
129 and forces. For us this is not used.
130 """
131 # Create a copy of the parameters dictionary so we don't
132 # modify the callers dictionary.
133 parameters_dict = parameters
134 required_keys = {
135 'title', 'species_path', 'ground_state_input'}
136 assert required_keys <= set(parameters_dict)
137 file_name = Path(directory) / 'input.xml'
138 species_path = parameters_dict.pop('species_path')
139 title = parameters_dict.pop('title')
140 # We can also pass additional parameters which are actually called
141 # properties in the exciting input xml. We don't use this term
142 # since ASE use properties to refer to results of a calculation
143 # (e.g. force, energy).
144 if 'properties_input' not in parameters_dict:
145 parameters_dict['properties_input'] = None
147 ase.io.exciting.write_input_xml_file(
148 file_name=file_name, atoms=atoms,
149 ground_state_input=parameters_dict['ground_state_input'],
150 species_path=species_path, title=title,
151 properties_input=parameters_dict['properties_input'])
153 def execute(
154 self, directory: PathLike,
155 profile) -> SubprocessRunResults:
156 """Given an exciting calculation profile, execute the calculation.
158 When executing an exciting calculation, you need to call run on an
159 exciting binary runner which is stored as the profile variable. The
160 binary runner at initialization will set all of the directory
161 information so we don't use this variable again here.
163 :param directory: Not used but comes from the base class.
164 :param profile: This name comes from the superclass CalculatorTemplate.
165 It should be an instance of an exciting.runner.SimpleBinaryRunner.
167 :return: Results of the subprocess.run command.
168 """
169 return profile.run()
171 def read_results(self, directory: PathLike) -> Mapping[str, Any]:
172 """Parse results from each ground state output file.
174 Note we allow for the ability for there to be multiple output files.
176 :param directory: Directory path to output file from exciting
177 simulation.
178 :return: dictionary of results. This can be fed into
179 ExcitingGroundStateResults if the user wants to access properties
180 more easily.
181 """
182 results = {}
183 for file_name in self.output_names:
184 full_file_path = Path(directory) / file_name
185 result: dict = self.parser[file_name](full_file_path)
186 results.update(result)
187 return results
189 def load_profile(self, cfg, **kwargs):
190 """ExcitingProfile can be created via a config file.
192 Alternative to this method the profile can be created with it's
193 init method. This method allows for more settings to be passed.
194 """
195 return ExcitingProfile.from_config(cfg, self.name, **kwargs)
198class ExcitingGroundStateResults:
199 """Exciting Ground State Results."""
201 def __init__(self, results: dict) -> None:
202 self.results = results
203 self.final_scl_iteration = list(results['scl'].keys())[-1]
205 def total_energy(self) -> float:
206 """Return total energy of system."""
207 # TODO(Alex) We should a common list of keys somewhere
208 # such that parser -> results -> getters are consistent
209 return float(
210 self.results['scl'][self.final_scl_iteration][
211 'Total energy']
212 )
214 def band_gap(self) -> float:
215 """Return the estimated fundamental gap from the exciting sim."""
216 return float(
217 self.results['scl'][self.final_scl_iteration][
218 'Estimated fundamental gap'
219 ]
220 )
222 def forces(self):
223 """Return forces present on the system.
225 Currently, not all exciting simulations return forces. We leave this
226 definition for future revisions.
227 """
228 raise PropertyNotImplementedError
230 def stress(self):
231 """Get the stress on the system.
233 Right now exciting does not yet calculate the stress on the system so
234 this won't work for the time being.
235 """
236 raise PropertyNotImplementedError
239class ExcitingGroundStateCalculator(GenericFileIOCalculator):
240 """Class for the ground state calculation.
242 :param runner: Binary runner that will execute an exciting calculation and
243 return a result.
244 :param ground_state_input: dictionary of ground state settings for example
245 {'rgkmax': 8.0, 'autormt': True} or an object of type
246 ExcitingGroundStateInput.
247 :param directory: Directory in which to run the job.
248 :param species_path: Path to the location of exciting's species files.
249 :param title: job name written to input.xml
251 :return: Results returned from running the calculate method.
254 Typical usage:
256 gs_calculator = ExcitingGroundState(runner, ground_state_input)
258 results: ExcitingGroundStateResults = gs_calculator.calculate(
259 atoms: Atoms)
260 """
262 def __init__(
263 self,
264 *,
265 runner: SimpleBinaryRunner,
266 ground_state_input,
267 directory='./',
268 species_path='./',
269 title='ASE-generated input',
270 parameters: Optional[Dict[str, Any]] = None,
271 ):
272 self.runner = runner
273 # Package data to be passed to
274 # ExcitingGroundStateTemplate.write_input(..., input_parameters, ...)
275 # Structure not included, as it's passed when one calls .calculate
276 # method directly.
277 required_params = {
278 'title': title,
279 'species_path': species_path,
280 'ground_state_input': ground_state_input}
281 # Set parameters to an empty dict if it is None (or Falsey). This
282 # is needed for the next line.
283 parameters = parameters or {}
284 # Add key, value paris from required_params if they are not defined
285 # in parmaters.
286 parameters = required_params | parameters
287 # GenericFileIOCalculator expects a `profile`
288 # containing machine-specific settings, however, in exciting's case,
289 # the species file are defined in the input XML (hence passed in the
290 # parameters argument) and the only other machine-specific setting is
291 # the BinaryRunner. Furthermore, in GenericFileIOCalculator.calculate,
292 # profile is only used to provide a run method. We therefore pass the
293 # BinaryRunner in the place of a profile.
294 super().__init__(
295 profile=runner,
296 template=ExcitingGroundStateTemplate(),
297 directory=directory,
298 parameters=parameters,
299 )