Coverage for ase / calculators / octopus.py: 75.93%

54 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-04 10:20 +0000

1"""ASE-interface to Octopus. 

2 

3Ask Hjorth Larsen <asklarsen@gmail.com> 

4Carlos de Armas 

5 

6https://octopus-code.org/ 

7""" 

8 

9import numpy as np 

10 

11from ase.calculators.genericfileio import ( 

12 BaseProfile, 

13 CalculatorTemplate, 

14 GenericFileIOCalculator, 

15) 

16from ase.io.octopus.input import generate_input, process_special_kwargs 

17from ase.io.octopus.output import read_eigenvalues_file, read_static_info 

18 

19 

20class OctopusIOError(IOError): 

21 pass 

22 

23 

24class OctopusProfile(BaseProfile): 

25 def get_calculator_command(self, inputfile): 

26 return [] 

27 

28 def version(self): 

29 import re 

30 from subprocess import check_output 

31 

32 txt = check_output( 

33 [*self._split_command, '--version'], encoding='ascii' 

34 ) 

35 match = re.match(r'octopus\s*(.+)', txt) 

36 # With MPI it prints the line for each rank, but we just match 

37 # the first line. 

38 return match.group(1) 

39 

40 

41class OctopusTemplate(CalculatorTemplate): 

42 _label = 'octopus' 

43 

44 def __init__(self): 

45 super().__init__( 

46 'octopus', 

47 implemented_properties=['energy', 'forces', 'dipole', 'stress'], 

48 ) 

49 self.outputname = f'{self._label}.out' 

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

51 

52 def read_results(self, directory): 

53 """Read octopus output files and extract data.""" 

54 results = {} 

55 with open(directory / 'static/info') as fd: 

56 results.update(read_static_info(fd)) 

57 

58 # If the eigenvalues file exists, we get the eigs/occs from that one. 

59 # This probably means someone ran Octopus in 'unocc' mode to 

60 # get eigenvalues (e.g. for band structures), and the values in 

61 # static/info will be the old (selfconsistent) ones. 

62 eigpath = directory / 'static/eigenvalues' 

63 if eigpath.is_file(): 

64 with open(eigpath) as fd: 

65 kpts, eigs, occs = read_eigenvalues_file(fd) 

66 kpt_weights = np.ones(len(kpts)) # XXX ? Or 1 / len(kpts) ? 

67 # XXX New Octopus probably has symmetry reduction !! 

68 results.update( 

69 eigenvalues=eigs, 

70 occupations=occs, 

71 ibz_kpoints=kpts, 

72 kpoint_weights=kpt_weights, 

73 ) 

74 return results 

75 

76 def execute(self, directory, profile): 

77 profile.run(directory, None, self.outputname, errorfile=self.errorname) 

78 

79 def write_input(self, profile, directory, atoms, parameters, properties): 

80 txt = generate_input(atoms, process_special_kwargs(atoms, parameters)) 

81 inp = directory / 'inp' 

82 inp.write_text(txt) 

83 

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

85 return OctopusProfile.from_config(cfg, self.name, **kwargs) 

86 

87 

88class Octopus(GenericFileIOCalculator): 

89 """Octopus calculator. 

90 

91 The label is always assumed to be a directory.""" 

92 

93 def __init__(self, profile=None, directory='.', **kwargs): 

94 """Create Octopus calculator. 

95 

96 Label is always taken as a subdirectory. 

97 Restart is taken to be a label.""" 

98 

99 super().__init__( 

100 profile=profile, 

101 template=OctopusTemplate(), 

102 directory=directory, 

103 parameters=kwargs, 

104 ) 

105 

106 @classmethod 

107 def recipe(cls, **kwargs): 

108 from ase import Atoms 

109 

110 system = Atoms() 

111 calc = Octopus(CalculationMode='recipe', **kwargs) 

112 system.calc = calc 

113 try: 

114 system.get_potential_energy() 

115 except OctopusIOError: 

116 pass 

117 else: 

118 raise OctopusIOError( 

119 'Expected recipe, but found useful physical output!' 

120 )