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
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
1"""ASE-interface to Octopus.
3Ask Hjorth Larsen <asklarsen@gmail.com>
4Carlos de Armas
6https://octopus-code.org/
7"""
9import numpy as np
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
20class OctopusIOError(IOError):
21 pass
24class OctopusProfile(BaseProfile):
25 def get_calculator_command(self, inputfile):
26 return []
28 def version(self):
29 import re
30 from subprocess import check_output
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)
41class OctopusTemplate(CalculatorTemplate):
42 _label = 'octopus'
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'
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))
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
76 def execute(self, directory, profile):
77 profile.run(directory, None, self.outputname, errorfile=self.errorname)
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)
84 def load_profile(self, cfg, **kwargs):
85 return OctopusProfile.from_config(cfg, self.name, **kwargs)
88class Octopus(GenericFileIOCalculator):
89 """Octopus calculator.
91 The label is always assumed to be a directory."""
93 def __init__(self, profile=None, directory='.', **kwargs):
94 """Create Octopus calculator.
96 Label is always taken as a subdirectory.
97 Restart is taken to be a label."""
99 super().__init__(
100 profile=profile,
101 template=OctopusTemplate(),
102 directory=directory,
103 parameters=kwargs,
104 )
106 @classmethod
107 def recipe(cls, **kwargs):
108 from ase import Atoms
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 )