Coverage for /builds/ase/ase/ase/calculators/gaussian.py: 42.17%

83 statements  

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

1# fmt: off 

2 

3import copy 

4import os 

5from collections.abc import Iterable 

6from typing import Dict, Optional 

7 

8from ase.calculators.calculator import FileIOCalculator 

9from ase.io import read, write 

10 

11 

12class GaussianDynamics: 

13 calctype = 'optimizer' 

14 delete = ['force'] 

15 keyword: Optional[str] = None 

16 special_keywords: Dict[str, str] = {} 

17 

18 def __init__(self, atoms, calc=None): 

19 self.atoms = atoms 

20 if calc is not None: 

21 self.calc = calc 

22 else: 

23 if self.atoms.calc is None: 

24 raise ValueError("{} requires a valid Gaussian calculator " 

25 "object!".format(self.__class__.__name__)) 

26 

27 self.calc = self.atoms.calc 

28 

29 def todict(self): 

30 return {'type': self.calctype, 

31 'optimizer': self.__class__.__name__} 

32 

33 def delete_keywords(self, kwargs): 

34 """removes list of keywords (delete) from kwargs""" 

35 for d in self.delete: 

36 kwargs.pop(d, None) 

37 

38 def set_keywords(self, kwargs): 

39 args = kwargs.pop(self.keyword, []) 

40 if isinstance(args, str): 

41 args = [args] 

42 elif isinstance(args, Iterable): 

43 args = list(args) 

44 

45 for key, template in self.special_keywords.items(): 

46 if key in kwargs: 

47 val = kwargs.pop(key) 

48 args.append(template.format(val)) 

49 

50 kwargs[self.keyword] = args 

51 

52 def run(self, **kwargs): 

53 calc_old = self.atoms.calc 

54 params_old = copy.deepcopy(self.calc.parameters) 

55 

56 self.delete_keywords(kwargs) 

57 self.delete_keywords(self.calc.parameters) 

58 self.set_keywords(kwargs) 

59 

60 self.calc.set(**kwargs) 

61 self.atoms.calc = self.calc 

62 

63 try: 

64 self.atoms.get_potential_energy() 

65 except OSError: 

66 converged = False 

67 else: 

68 converged = True 

69 

70 atoms = read(self.calc.label + '.log') 

71 self.atoms.cell = atoms.cell 

72 self.atoms.positions = atoms.positions 

73 

74 self.calc.parameters = params_old 

75 self.calc.reset() 

76 if calc_old is not None: 

77 self.atoms.calc = calc_old 

78 

79 return converged 

80 

81 

82class GaussianOptimizer(GaussianDynamics): 

83 keyword = 'opt' 

84 special_keywords = { 

85 'fmax': '{}', 

86 'steps': 'maxcycle={}', 

87 } 

88 

89 

90class GaussianIRC(GaussianDynamics): 

91 keyword = 'irc' 

92 special_keywords = { 

93 'direction': '{}', 

94 'steps': 'maxpoints={}', 

95 } 

96 

97 

98class Gaussian(FileIOCalculator): 

99 _legacy_default_command = 'g16 < PREFIX.com > PREFIX.log' 

100 implemented_properties = ['energy', 'forces', 'dipole'] 

101 discard_results_on_any_change = True 

102 

103 fileio_rules = FileIOCalculator.ruleset( 

104 stdin_name='{prefix}.com', 

105 stdout_name='{prefix}.log') 

106 

107 def __init__(self, *args, label='Gaussian', **kwargs): 

108 super().__init__(*args, label=label, **kwargs) 

109 

110 def write_input(self, atoms, properties=None, system_changes=None): 

111 super().write_input(atoms, properties, system_changes) 

112 write(self.label + '.com', atoms, properties=properties, 

113 format='gaussian-in', parallel=False, **self.parameters) 

114 

115 def read_results(self): 

116 output = read(self.label + '.log', format='gaussian-out') 

117 self.calc = output.calc 

118 self.results = output.calc.results 

119 

120 # Method(s) defined in the old calculator, added here for 

121 # backwards compatibility 

122 def clean(self): 

123 for suffix in ['.com', '.chk', '.log']: 

124 try: 

125 os.remove(os.path.join(self.directory, self.label + suffix)) 

126 except OSError: 

127 pass 

128 

129 def get_version(self): 

130 raise NotImplementedError # not sure how to do this yet