Coverage for /builds/ase/ase/ase/codes.py: 76.09%

138 statements  

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

1# fmt: off 

2 

3from dataclasses import dataclass 

4 

5# Note: There could be more than one "calculator" for any given code; 

6# for example Espresso can work both as GenericFileIOCalculator and 

7# SocketIOCalculator, or as part of some DFTD3 combination. 

8# 

9# Also, DFTD3 is one external code but can be invoked alone (as PureDFTD3) 

10# as well as together with a DFT code (the main DFTD3 calculator). 

11# 

12# The current CodeMetadata object only specifies a single calculator class. 

13# We should be wary of these invisible "one-to-one" restrictions. 

14 

15 

16@dataclass 

17class CodeMetadata: 

18 name: str 

19 longname: str 

20 modulename: str 

21 classname: str 

22 

23 def calculator_class(self): 

24 from importlib import import_module 

25 module = import_module(self.modulename) 

26 cls = getattr(module, self.classname) 

27 return cls 

28 

29 @classmethod 

30 def define_code(cls, name, longname, importpath): 

31 modulename, classname = importpath.rsplit('.', 1) 

32 return cls(name, longname, modulename, classname) 

33 

34 def _description(self): 

35 yield f'Name: {self.longname}' 

36 yield f'Import: {self.modulename}.{self.classname}' 

37 yield f'Type: {self.calculator_type()}' 

38 yield '' 

39 yield from self._config_description() 

40 

41 def description(self, indent=''): 

42 return '\n'.join(indent + line for line in self._description()) 

43 

44 def is_legacy_fileio(self): 

45 from ase.calculators.calculator import FileIOCalculator 

46 return issubclass(self.calculator_class(), FileIOCalculator) 

47 

48 def is_generic_fileio(self): 

49 from ase.calculators.genericfileio import CalculatorTemplate 

50 

51 # It is nicer to check for the template class, since it has the name, 

52 # but then calculator_class() should be renamed. 

53 return issubclass(self.calculator_class(), CalculatorTemplate) 

54 

55 def is_calculator_oldbase(self): 

56 from ase.calculators.calculator import Calculator 

57 return issubclass(self.calculator_class(), Calculator) 

58 

59 def is_base_calculator(self): 

60 from ase.calculators.calculator import BaseCalculator 

61 return issubclass(self.calculator_class(), BaseCalculator) 

62 

63 def calculator_type(self): 

64 cls = self.calculator_class() 

65 

66 if self.is_generic_fileio(): 

67 return 'GenericFileIOCalculator' 

68 

69 if self.is_legacy_fileio(): 

70 return 'FileIOCalculator (legacy)' 

71 

72 if self.is_calculator_oldbase(): 

73 return 'Calculator (legacy base class)' 

74 

75 if self.is_base_calculator(): 

76 return 'Base calculator' 

77 

78 return f'BAD: Not a proper calculator (superclasses: {cls.__mro__})' 

79 

80 def profile(self): 

81 from ase.calculators.calculator import FileIOCalculator 

82 from ase.calculators.genericfileio import CalculatorTemplate 

83 from ase.config import cfg 

84 cls = self.calculator_class() 

85 if issubclass(cls, CalculatorTemplate): 

86 return cls().load_profile(cfg) 

87 elif hasattr(cls, 'fileio_rules'): 

88 assert issubclass(cls, FileIOCalculator) 

89 return cls.load_argv_profile(cfg, self.name) 

90 else: 

91 raise NotImplementedError('profile() not implemented') 

92 

93 def _config_description(self): 

94 from ase.calculators.genericfileio import BadConfiguration 

95 from ase.config import cfg 

96 

97 parser = cfg.parser 

98 if self.name not in parser: 

99 yield f'Not configured: No [{self.name}] section in configuration' 

100 return 

101 

102 try: 

103 profile = self.profile() 

104 except BadConfiguration as ex: 

105 yield f'Error in configuration section [{self.name}]' 

106 yield 'Missing or bad parameters:' 

107 yield f' {ex}' 

108 return 

109 except NotImplementedError as ex: 

110 yield f'N/A: {ex}' 

111 return 

112 

113 yield f'Configured by section [{self.name}]:' 

114 configvars = vars(profile) 

115 for name in sorted(configvars): 

116 yield f' {name} = {configvars[name]}' 

117 

118 return 

119 

120 

121def register_codes(): 

122 

123 codes = {} 

124 

125 def reg(name, *args): 

126 code = CodeMetadata.define_code(name, *args) 

127 codes[name] = code 

128 

129 reg('abinit', 'Abinit', 'ase.calculators.abinit.AbinitTemplate') 

130 reg('ace', 'ACE molecule', 'ase.calculators.acemolecule.ACE') 

131 # internal: reg('acn', 'ACN force field', 'ase.calculators.acn.ACN') 

132 reg('aims', 'FHI-Aims', 'ase.calculators.aims.AimsTemplate') 

133 reg('amber', 'Amber', 'ase.calculators.amber.Amber') 

134 reg('castep', 'Castep', 'ase.calculators.castep.Castep') 

135 # internal: combine_mm 

136 # internal: counterions 

137 reg('cp2k', 'CP2K', 'ase.calculators.cp2k.CP2K') 

138 reg('crystal', 'CRYSTAL', 'ase.calculators.crystal.CRYSTAL') 

139 reg('demon', 'deMon', 'ase.calculators.demon.Demon') 

140 reg('demonnano', 'deMon-nano', 'ase.calculators.demonnano.DemonNano') 

141 reg('dftb', 'DFTB+', 'ase.calculators.dftb.Dftb') 

142 reg('dftd3', 'DFT-D3', 'ase.calculators.dftd3.DFTD3') 

143 # reg('dftd3-pure', 'DFT-D3 (pure)', 'ase.calculators.dftd3.puredftd3') 

144 reg('dmol', 'DMol3', 'ase.calculators.dmol.DMol3') 

145 # internal: reg('eam', 'EAM', 'ase.calculators.eam.EAM') 

146 reg('elk', 'ELK', 'ase.calculators.elk.ELK') 

147 # internal: reg('emt', 'EMT potential', 'ase.calculators.emt.EMT') 

148 reg('espresso', 'Quantum Espresso', 

149 'ase.calculators.espresso.EspressoTemplate') 

150 reg('exciting', 'Exciting', 

151 'ase.calculators.exciting.exciting.ExcitingGroundStateTemplate') 

152 # internal: reg('ff', 'FF', 'ase.calculators.ff.ForceField') 

153 # fleur <- external nowadays 

154 reg('gamess_us', 'GAMESS-US', 'ase.calculators.gamess_us.GAMESSUS') 

155 reg('gaussian', 'Gaussian', 'ase.calculators.gaussian.Gaussian') 

156 reg('gromacs', 'Gromacs', 'ase.calculators.gromacs.Gromacs') 

157 reg('gulp', 'GULP', 'ase.calculators.gulp.GULP') 

158 # h2morse.py do we need a specific H2 morse calculator when we have morse?? 

159 # internal: reg('harmonic', 'Harmonic potential', 

160 # 'ase.calculators.harmonic.HarmonicCalculator') 

161 # internal: reg('idealgas', 'Ideal gas (dummy)', 

162 # 'ase.calculators.idealgas.IdealGas') 

163 # XXX cannot import without kimpy installed, fixme: 

164 # reg('kim', 'OpenKIM', 'ase.calculators.kim.kim.KIM') 

165 reg('lammpslib', 'Lammps (python library)', 

166 'ase.calculators.lammpslib.LAMMPSlib') 

167 reg('lammpsrun', 'Lammps (external)', 'ase.calculators.lammpsrun.LAMMPS') 

168 # internal: reg('lj', 'Lennard–Jones potential', 

169 # 'ase.calculators.lj.LennardJones') 

170 # internal: loggingcalc.py 

171 # internal: mixing.py 

172 reg('mopac', 'MOPAC', 'ase.calculators.mopac.MOPAC') 

173 # internal: reg('morse', 'Morse potential', 

174 # 'ase.calculators.morse.MorsePotential') 

175 reg('nwchem', 'NWChem', 'ase.calculators.nwchem.NWChem') 

176 reg('octopus', 'Octopus', 'ase.calculators.octopus.OctopusTemplate') 

177 reg('onetep', 'Onetep', 'ase.calculators.onetep.OnetepTemplate') 

178 reg('openmx', 'OpenMX', 'ase.calculators.openmx.OpenMX') 

179 reg('orca', 'ORCA', 'ase.calculators.orca.OrcaTemplate') 

180 reg('plumed', 'Plumed', 'ase.calculators.plumed.Plumed') 

181 reg('psi4', 'Psi4', 'ase.calculators.psi4.Psi4') 

182 reg('qchem', 'QChem', 'ase.calculators.qchem.QChem') 

183 # internal: qmmm.py 

184 reg('siesta', 'SIESTA', 'ase.calculators.siesta.Siesta') 

185 # internal: test.py 

186 # internal: reg('tip3p', 'TIP3P', 'ase.calculators.tip3p.TIP3P') 

187 # internal: reg('tip4p', 'TIP4P', 'ase.calculators.tip4p.TIP4P') 

188 reg('turbomole', 'Turbomole', 'ase.calculators.turbomole.Turbomole') 

189 reg('vasp', 'VASP', 'ase.calculators.vasp.Vasp') 

190 # internal: vdwcorrection 

191 return codes 

192 

193 

194codes = register_codes() 

195 

196 

197def list_codes(names): 

198 from ase.config import cfg 

199 cfg.print_header() 

200 print() 

201 

202 for name in names: 

203 code = codes[name] 

204 print(code.name) 

205 try: 

206 print(code.description(indent=' ')) 

207 except Exception as ex: 

208 print(f'Bad configuration of {name}: {ex!r}') 

209 print() 

210 

211 

212if __name__ == '__main__': 

213 import sys 

214 names = sys.argv[1:] 

215 if not names: 

216 names = [*codes] 

217 list_codes(names)