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

70 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-22 11:22 +0000

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

2 

3Exciting calculator class in this file allow for writing exciting input 

4files using ASE Atoms object that allow for the compiled exciting binary 

5to run DFT on the geometry/material defined in the Atoms object. Also gives 

6access to developer to a lightweight parser (lighter weight than NOMAD or 

7the exciting parser in the exciting repository) to capture ground state 

8properties. 

9 

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

11use this calculator. 

12""" 

13 

14from os import PathLike 

15from pathlib import Path 

16from typing import Any, Mapping 

17 

18import ase.io.exciting 

19from ase.calculators.calculator import PropertyNotImplementedError 

20from ase.calculators.exciting.runner import (SimpleBinaryRunner, 

21 SubprocessRunResults) 

22from ase.calculators.genericfileio import (BaseProfile, CalculatorTemplate, 

23 GenericFileIOCalculator) 

24 

25 

26class ExcitingProfile(BaseProfile): 

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

28 

29 Follows the generic pattern BUT currently not used by our calculator as: 

30 * species_path is part of the input file in exciting. 

31 * OnlyTypo fix part of the profile used in the base class is the run 

32 method, which is part of the BinaryRunner class. 

33 """ 

34 

35 def __init__(self, binary, species_path=None, **kwargs): 

36 super().__init__(**kwargs) 

37 

38 self.species_path = species_path 

39 self.binary = binary 

40 

41 def version(self): 

42 """Return exciting version.""" 

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

44 return 

45 

46 # Machine specific config files in the config 

47 # species_file goes in the config 

48 # binary file in the config. 

49 # options for that, parallel info dictionary. 

50 # Number of threads and stuff like that. 

51 

52 def get_calculator_command(self, input_file): 

53 """Returns command to run binary as a list of strings.""" 

54 # input_file unused for exciting, it looks for input.xml in run 

55 # directory. 

56 if input_file is None: 

57 return [self.binary] 

58 else: 

59 return [self.binary, str(input_file)] 

60 

61 

62class ExcitingGroundStateTemplate(CalculatorTemplate): 

63 """Template for Ground State Exciting Calculator 

64 

65 Abstract methods inherited from the base class: 

66 * write_input 

67 * execute 

68 * read_results 

69 """ 

70 

71 parser = {'info.xml': ase.io.exciting.parse_output} 

72 output_names = list(parser) 

73 # Use frozenset since the CalculatorTemplate enforces it. 

74 implemented_properties = frozenset(['energy', 'forces']) 

75 _label = 'exciting' 

76 

77 def __init__(self): 

78 """Initialise with constant class attributes. 

79 

80 :param program_name: The DFT program, should always be exciting. 

81 :param implemented_properties: What properties should exciting 

82 calculate/read from output. 

83 """ 

84 super().__init__('exciting', self.implemented_properties) 

85 self.errorname = f'{self._label}.err' 

86 

87 @staticmethod 

88 def _require_forces(input_parameters): 

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

90 

91 :param input_parameters: exciting ground state input parameters, either 

92 as a dictionary or ExcitingGroundStateInput. 

93 :return: Ground state input parameters, with "compute 

94 forces" set to true. 

95 """ 

96 from excitingtools import ExcitingGroundStateInput 

97 

98 input_parameters = ExcitingGroundStateInput(input_parameters) 

99 input_parameters.tforce = True 

100 return input_parameters 

101 

102 def write_input( 

103 self, 

104 profile: ExcitingProfile, # ase test linter enforces method signatures 

105 # be consistent with the 

106 # abstract method that it implements 

107 directory: PathLike, 

108 atoms: ase.Atoms, 

109 parameters: dict, 

110 properties=None, 

111 ): 

112 """Write an exciting input.xml file based on the input args. 

113 

114 :param profile: an Exciting code profile 

115 :param directory: Directory in which to run calculator. 

116 :param atoms: ASE atoms object. 

117 :param parameters: exciting ground state input parameters, in a 

118 dictionary. Expect species_path, title and ground_state data, 

119 either in an object or as dict. 

120 :param properties: Base method's API expects the physical properties 

121 expected from a ground state calculation, for example energies 

122 and forces. For us this is not used. 

123 """ 

124 # Create a copy of the parameters dictionary so we don't 

125 # modify the callers dictionary. 

126 parameters_dict = parameters 

127 assert set(parameters_dict.keys()) == { 

128 'title', 'species_path', 'ground_state_input', 

129 'properties_input'}, \ 

130 'Keys should be defined by ExcitingGroundState calculator' 

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

132 species_path = parameters_dict.pop('species_path') 

133 title = parameters_dict.pop('title') 

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

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

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

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

138 if 'properties_input' not in parameters_dict: 

139 parameters_dict['properties_input'] = None 

140 

141 ase.io.exciting.write_input_xml_file( 

142 file_name=file_name, atoms=atoms, 

143 ground_state_input=parameters_dict['ground_state_input'], 

144 species_path=species_path, title=title, 

145 properties_input=parameters_dict['properties_input']) 

146 

147 def execute( 

148 self, directory: PathLike, 

149 profile) -> SubprocessRunResults: 

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

151 

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

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

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

155 just pass a SimpleBinaryRunner. 

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

157 It contains machine specific information to run the 

158 calculation. 

159 

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

161 """ 

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

163 erorrfile=self.errorname) 

164 

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

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

167 

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

169 

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

171 simulation. 

172 :return: Dictionary containing important output properties. 

173 """ 

174 results = {} 

175 for file_name in self.output_names: 

176 full_file_path = Path(directory) / file_name 

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

178 results.update(result) 

179 return results 

180 

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

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

183 

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

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

186 """ 

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

188 

189 

190class ExcitingGroundStateResults: 

191 """Exciting Ground State Results.""" 

192 

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

194 self.results = results 

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

196 

197 def total_energy(self) -> float: 

198 """Return total energy of system.""" 

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

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

201 return float( 

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

203 ) 

204 

205 def band_gap(self) -> float: 

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

207 return float( 

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

209 'Estimated fundamental gap' 

210 ] 

211 ) 

212 

213 def forces(self): 

214 """Return forces present on the system. 

215 

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

217 definition for future revisions. 

218 """ 

219 raise PropertyNotImplementedError 

220 

221 def stress(self): 

222 """Get the stress on the system. 

223 

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

225 this won't work for the time being. 

226 """ 

227 raise PropertyNotImplementedError 

228 

229 

230class ExcitingGroundStateCalculator(GenericFileIOCalculator): 

231 """Class for the ground state calculation. 

232 

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

234 return a result. 

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

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

237 ExcitingGroundStateInput. 

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

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

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

241 

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

243 

244 

245 Typical usage: 

246 

247 gs_calculator = ExcitingGroundState(runner, ground_state_input) 

248 

249 results: ExcitingGroundStateResults = gs_calculator.calculate( 

250 atoms: Atoms) 

251 """ 

252 

253 def __init__( 

254 self, 

255 *, 

256 runner: SimpleBinaryRunner, 

257 ground_state_input, 

258 directory='./', 

259 species_path='./', 

260 title='ASE-generated input', 

261 parallel=None, 

262 parallel_info=None, 

263 ): 

264 self.runner = runner 

265 # Package data to be passed to 

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

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

268 # method directly 

269 self.exciting_inputs = { 

270 'title': title, 

271 'species_path': species_path, 

272 'ground_state_input': ground_state_input, 

273 } 

274 self.directory = Path(directory) 

275 

276 # GenericFileIOCalculator expects a `profile` 

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

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

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

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

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

282 # BinaryRunner in the place of a profile. 

283 super().__init__( 

284 profile=runner, 

285 template=ExcitingGroundStateTemplate(), 

286 directory=directory, 

287 parameters=self.exciting_inputs, 

288 parallel_info=parallel_info, 

289 parallel=parallel, 

290 )