Coverage for /builds/ase/ase/ase/calculators/exciting/exciting.py: 84.29%

70 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3"""ASE Calculator for the ground state exciting DFT code. 

4 

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. 

11 

12Note: excitingtools must be installed using `pip install excitingtools` to 

13use this calculator. 

14""" 

15 

16from os import PathLike 

17from pathlib import Path 

18from typing import Any, Mapping 

19 

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) 

31 

32 

33class ExcitingProfile(BaseProfile): 

34 """Defines all quantities that are configurable for a given machine. 

35 

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'} 

42 

43 def __init__(self, command, species_path=None, **kwargs): 

44 super().__init__(command, **kwargs) 

45 

46 self.species_path = species_path 

47 

48 def version(self): 

49 """Return exciting version.""" 

50 # TARP No way to get the version for the binary in use 

51 return 

52 

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. 

58 

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)] 

67 

68 

69class ExcitingGroundStateTemplate(CalculatorTemplate): 

70 """Template for Ground State Exciting Calculator 

71 

72 Abstract methods inherited from the base class: 

73 * write_input 

74 * execute 

75 * read_results 

76 """ 

77 

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' 

83 

84 def __init__(self): 

85 """Initialise with constant class attributes. 

86 

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' 

93 

94 @staticmethod 

95 def _require_forces(input_parameters): 

96 """Expect ASE always wants forces, enforce setting in input_parameters. 

97 

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 

104 

105 input_parameters = ExcitingGroundStateInput(input_parameters) 

106 input_parameters.tforce = True 

107 return input_parameters 

108 

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. 

120 

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 assert set(parameters_dict.keys()) == { 

135 'title', 'species_path', 'ground_state_input', 

136 'properties_input'}, \ 

137 'Keys should be defined by ExcitingGroundState calculator' 

138 file_name = Path(directory) / 'input.xml' 

139 species_path = parameters_dict.pop('species_path') 

140 title = parameters_dict.pop('title') 

141 # We can also pass additional parameters which are actually called 

142 # properties in the exciting input xml. We don't use this term 

143 # since ASE use properties to refer to results of a calculation 

144 # (e.g. force, energy). 

145 if 'properties_input' not in parameters_dict: 

146 parameters_dict['properties_input'] = None 

147 

148 ase.io.exciting.write_input_xml_file( 

149 file_name=file_name, atoms=atoms, 

150 ground_state_input=parameters_dict['ground_state_input'], 

151 species_path=species_path, title=title, 

152 properties_input=parameters_dict['properties_input']) 

153 

154 def execute( 

155 self, directory: PathLike, 

156 profile) -> SubprocessRunResults: 

157 """Given an exciting calculation profile, execute the calculation. 

158 

159 :param directory: Directory in which to execute the calculator 

160 exciting_calculation: Base method `execute` expects a profile, 

161 however it is simply used to execute the program, therefore we 

162 just pass a SimpleBinaryRunner. 

163 :param profile: This name comes from the superclass CalculatorTemplate. 

164 It contains machine specific information to run the 

165 calculation. 

166 

167 :return: Results of the subprocess.run command. 

168 """ 

169 return profile.run(directory, f"{directory}/input.xml", None, 

170 erorrfile=self.errorname) 

171 

172 def read_results(self, directory: PathLike) -> Mapping[str, Any]: 

173 """Parse results from each ground state output file. 

174 

175 Note we allow for the ability for there to be multiple output files. 

176 

177 :param directory: Directory path to output file from exciting 

178 simulation. 

179 :return: Dictionary containing important output properties. 

180 """ 

181 results = {} 

182 for file_name in self.output_names: 

183 full_file_path = Path(directory) / file_name 

184 result: dict = self.parser[file_name](full_file_path) 

185 results.update(result) 

186 return results 

187 

188 def load_profile(self, cfg, **kwargs): 

189 """ExcitingProfile can be created via a config file. 

190 

191 Alternative to this method the profile can be created with it's 

192 init method. This method allows for more settings to be passed. 

193 """ 

194 return ExcitingProfile.from_config(cfg, self.name, **kwargs) 

195 

196 

197class ExcitingGroundStateResults: 

198 """Exciting Ground State Results.""" 

199 

200 def __init__(self, results: dict) -> None: 

201 self.results = results 

202 self.final_scl_iteration = list(results['scl'].keys())[-1] 

203 

204 def total_energy(self) -> float: 

205 """Return total energy of system.""" 

206 # TODO(Alex) We should a common list of keys somewhere 

207 # such that parser -> results -> getters are consistent 

208 return float( 

209 self.results['scl'][self.final_scl_iteration]['Total energy'] 

210 ) 

211 

212 def band_gap(self) -> float: 

213 """Return the estimated fundamental gap from the exciting sim.""" 

214 return float( 

215 self.results['scl'][self.final_scl_iteration][ 

216 'Estimated fundamental gap' 

217 ] 

218 ) 

219 

220 def forces(self): 

221 """Return forces present on the system. 

222 

223 Currently, not all exciting simulations return forces. We leave this 

224 definition for future revisions. 

225 """ 

226 raise PropertyNotImplementedError 

227 

228 def stress(self): 

229 """Get the stress on the system. 

230 

231 Right now exciting does not yet calculate the stress on the system so 

232 this won't work for the time being. 

233 """ 

234 raise PropertyNotImplementedError 

235 

236 

237class ExcitingGroundStateCalculator(GenericFileIOCalculator): 

238 """Class for the ground state calculation. 

239 

240 :param runner: Binary runner that will execute an exciting calculation and 

241 return a result. 

242 :param ground_state_input: dictionary of ground state settings for example 

243 {'rgkmax': 8.0, 'autormt': True} or an object of type 

244 ExcitingGroundStateInput. 

245 :param directory: Directory in which to run the job. 

246 :param species_path: Path to the location of exciting's species files. 

247 :param title: job name written to input.xml 

248 

249 :return: Results returned from running the calculate method. 

250 

251 

252 Typical usage: 

253 

254 gs_calculator = ExcitingGroundState(runner, ground_state_input) 

255 

256 results: ExcitingGroundStateResults = gs_calculator.calculate( 

257 atoms: Atoms) 

258 """ 

259 

260 def __init__( 

261 self, 

262 *, 

263 runner: SimpleBinaryRunner, 

264 ground_state_input, 

265 directory='./', 

266 species_path='./', 

267 title='ASE-generated input', 

268 ): 

269 self.runner = runner 

270 # Package data to be passed to 

271 # ExcitingGroundStateTemplate.write_input(..., input_parameters, ...) 

272 # Structure not included, as it's passed when one calls .calculate 

273 # method directly 

274 self.exciting_inputs = { 

275 'title': title, 

276 'species_path': species_path, 

277 'ground_state_input': ground_state_input, 

278 } 

279 self.directory = Path(directory) 

280 

281 # GenericFileIOCalculator expects a `profile` 

282 # containing machine-specific settings, however, in exciting's case, 

283 # the species file are defined in the input XML (hence passed in the 

284 # parameters argument) and the only other machine-specific setting is 

285 # the BinaryRunner. Furthermore, in GenericFileIOCalculator.calculate, 

286 # profile is only used to provide a run method. We therefore pass the 

287 # BinaryRunner in the place of a profile. 

288 super().__init__( 

289 profile=runner, 

290 template=ExcitingGroundStateTemplate(), 

291 directory=directory, 

292 parameters=self.exciting_inputs, 

293 )