Coverage for /builds/kinetik161/ase/ase/calculators/exciting/exciting.py: 81.16%

69 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +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 ( 

21 SimpleBinaryRunner, 

22 SubprocessRunResults, 

23) 

24 

25import ase.calculators.exciting.runner 

26 

27from ase.calculators.genericfileio import ( 

28 BaseProfile, 

29 CalculatorTemplate, 

30 GenericFileIOCalculator, 

31) 

32 

33 

34class ExcitingProfile(BaseProfile): 

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

36 

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

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

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

40 method, which is part of the BinaryRunner class. 

41 """ 

42 

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

44 super().__init__(**kwargs) 

45 

46 self.species_path = species_path 

47 self.binary = binary 

48 

49 def version(self): 

50 """Return exciting version.""" 

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

52 return None 

53 

54 # Machine specific config files in the config 

55 # species_file goes in the config 

56 # binary file in the config. 

57 # options for that, parallel info dictionary. 

58 # Number of threads and stuff like that. 

59 

60 def get_calculator_command(self, input_file): 

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

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

63 # directory. 

64 if input_file is None: 

65 return [self.binary] 

66 else: 

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

68 

69 

70class ExcitingGroundStateTemplate(CalculatorTemplate): 

71 """Template for Ground State Exciting Calculator 

72 

73 Abstract methods inherited from the base class: 

74 * write_input 

75 * execute 

76 * read_results 

77 """ 

78 

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

80 output_names = list(parser) 

81 # Use frozenset since the CalculatorTemplate enforces it. 

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

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 

93 @staticmethod 

94 def _require_forces(input_parameters): 

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

96 

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

98 as a dictionary or ExcitingGroundStateInput. 

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

100 forces" set to true. 

101 """ 

102 from excitingtools import ExcitingGroundStateInput 

103 

104 input_parameters = ExcitingGroundStateInput(input_parameters) 

105 input_parameters.tforce = True 

106 return input_parameters 

107 

108 def write_input( 

109 self, 

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

111 # be consistent with the 

112 # abstract method that it implements 

113 directory: PathLike, 

114 atoms: ase.Atoms, 

115 parameters: dict, 

116 properties=None, 

117 ): 

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

119 

120 :param profile: an Exciting code profile 

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

122 :param atoms: ASE atoms object. 

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

124 dictionary. Expect species_path, title and ground_state data, 

125 either in an object or as dict. 

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

127 expected from a ground state calculation, for example energies 

128 and forces. For us this is not used. 

129 """ 

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

131 # modify the callers dictionary. 

132 parameters_dict = parameters 

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

134 'title', 'species_path', 'ground_state_input', 

135 'properties_input'}, \ 

136 'Keys should be defined by ExcitingGroundState calculator' 

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 

146 

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

152 

153 def execute( 

154 self, directory: PathLike, 

155 profile) -> SubprocessRunResults: 

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

157 

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

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

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

161 just pass a SimpleBinaryRunner. 

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

163 It contains machine specific information to run the 

164 calculation. 

165 

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

167 """ 

168 return profile.run(directory, f"{directory}/input.xml") 

169 

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

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

172 

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

174 

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

176 simulation. 

177 :return: Dictionary containing important output properties. 

178 """ 

179 results = {} 

180 for file_name in self.output_names: 

181 full_file_path = Path(directory) / file_name 

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

183 results.update(result) 

184 return results 

185 

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

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

188 

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

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

191 """ 

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

193 

194 

195class ExcitingGroundStateResults: 

196 """Exciting Ground State Results.""" 

197 

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

199 self.results = results 

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

201 

202 def total_energy(self) -> float: 

203 """Return total energy of system.""" 

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

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

206 return float( 

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

208 ) 

209 

210 def band_gap(self) -> float: 

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

212 return float( 

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

214 'Estimated fundamental gap' 

215 ] 

216 ) 

217 

218 def forces(self): 

219 """Return forces present on the system. 

220 

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

222 definition for future revisions. 

223 """ 

224 raise PropertyNotImplementedError 

225 

226 def stress(self): 

227 """Get the stress on the system. 

228 

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

230 this won't work for the time being. 

231 """ 

232 raise PropertyNotImplementedError 

233 

234 

235class ExcitingGroundStateCalculator(GenericFileIOCalculator): 

236 """Class for the ground state calculation. 

237 

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

239 return a result. 

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

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

242 ExcitingGroundStateInput. 

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

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

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

246 

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

248 

249 

250 Typical usage: 

251 

252 gs_calculator = ExcitingGroundState(runner, ground_state_input) 

253 

254 results: ExcitingGroundStateResults = gs_calculator.calculate( 

255 atoms: Atoms) 

256 """ 

257 

258 def __init__( 

259 self, 

260 *, 

261 runner: SimpleBinaryRunner, 

262 ground_state_input, 

263 directory='./', 

264 species_path='./', 

265 title='ASE-generated input', 

266 parallel=None, 

267 parallel_info=None, 

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 parallel_info=parallel_info, 

294 parallel=parallel, 

295 )