Skip to content

Commit ddea5b9

Browse files
committed
overhaul Aims extension
closes #4
1 parent d7ec1cc commit ddea5b9

File tree

1 file changed

+85
-47
lines changed

1 file changed

+85
-47
lines changed

mona/sci/aims/aims.py

Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
# This Source Code Form is subject to the terms of the Mozilla Public
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4-
import shutil
4+
import os
55
from collections import OrderedDict
66
from copy import deepcopy
77
from pathlib import Path
8-
from typing import Any, Callable, Dict, Tuple, cast
8+
from typing import Any, Callable, Dict, List, Tuple, Type, cast
99

1010
from ...dirtask import dir_task
11-
from ...errors import InvalidInput, MonaError
11+
from ...errors import InvalidInput
1212
from ...files import File
1313
from ...pluggable import Pluggable, Plugin
1414
from ...pyhash import hash_function
1515
from ...tasks import Task
1616
from ..geomlib import Atom, Molecule
1717
from .dsl import expand_dicts, parse_aims_input
1818

19-
__version__ = '0.1.0'
20-
__all__ = ['Aims', 'SpeciesDefaults']
19+
__version__ = '0.2.0'
20+
__all__ = ()
2121

2222

2323
class AimsPlugin(Plugin['Aims']):
@@ -29,24 +29,62 @@ def _func_hash(self) -> str:
2929

3030

3131
class Aims(Pluggable):
32-
"""A task factory that creates FHI-aims directory tasks."""
32+
""":term:`Task factory` that creates an FHI-aims :term:`directory task`.
33+
34+
The creation of a task is handeld by a series of plugins that process
35+
keyword arguments passed to the instance of :class:`Aims` to generate
36+
``control.in`` and ``geometry.in``. The default plugins are created with no
37+
extra arguments. Custom plugins can be registered by calling them with the
38+
:class:`Aims` instance as an argument.
39+
40+
Aims tasks require two prerequisites to function:
41+
42+
- ``mona_aims`` executable in ``PATH``, which runs Aims and accepts these
43+
environment variables:
44+
45+
- ``MONA_AIMS`` specifies the Aims executable to run.
46+
- ``MONA_NCORES`` specifies the number of processor cores to run Aims
47+
with.
48+
49+
- ``AIMS_SPECIES_DEFAULTS`` environment variable that points to the
50+
``species_defaults`` Aims directory.
51+
"""
52+
53+
plugin_factories: List[Type[AimsPlugin]] = []
54+
"""List of plugins run in this order."""
3355

3456
def __init__(self) -> None:
3557
Pluggable.__init__(self)
36-
for factory in default_plugins:
58+
for factory in Aims.plugin_factories:
3759
factory()(self)
3860

3961
def __call__(self, *, label: str = None, **kwargs: Any) -> Task[Dict[str, File]]:
40-
"""Create an FHI-aims.
62+
"""Create an Aims task.
4163
42-
:param kwargs: processed by individual plugins
64+
:param kwargs: keyword arguments processed by plugins
65+
:param label: passed to :func:`~mona.dirtask.dir_task`
66+
67+
Existing keywords (some are generated by plugins, see below):
68+
69+
- ``atoms``: List of two-tuples of a species and a coordinate.
70+
- ``geom``: Instance of :class:`~mona.sci.geomlib.Molecule`.
71+
- ``species_specs``: List of species specifications.
72+
- ``tags``: Dictionary of tags for ``control.in``.
73+
- ``aims``: Aims executable, must be present in ``PATH``.
74+
- ``check``: Whether task should error out on abnormal Aims exit.
75+
- ``ncores``: Number of cores to use, all available if not given.
4376
"""
4477
self.run_plugins('process', kwargs)
4578
script = File.from_str('aims.sh', kwargs.pop('script'))
46-
inputs = [File.from_str(name, cont) for name, cont in kwargs.pop('inputs')]
79+
inputs = [
80+
File.from_str(name, kwargs.pop(name))
81+
for name in ['control.in', 'geometry.in']
82+
]
83+
task = dir_task(script, inputs, label=label)
84+
task.storage['ncores'] = kwargs.pop('ncores', -1)
4785
if kwargs:
4886
raise InvalidInput(f'Unknown Aims kwargs: {list(kwargs.keys())}')
49-
return dir_task(script, inputs, label=label)
87+
return task
5088

5189
def _func_hash(self) -> str:
5290
return ','.join(
@@ -57,42 +95,35 @@ def _func_hash(self) -> str:
5795
)
5896

5997

60-
class SpeciesDir(AimsPlugin):
61-
def __init__(self) -> None:
62-
self._speciesdirs: Dict[Tuple[str, str], Path] = {}
63-
64-
def process(self, kwargs: Dict[str, Any]) -> None:
65-
sp_def_key = aims, sp_def = kwargs['aims'], kwargs.pop('species_defaults')
66-
speciesdir = self._speciesdirs.get(sp_def_key)
67-
if not speciesdir:
68-
pathname = shutil.which(aims)
69-
if not pathname:
70-
pathname = shutil.which('aims-master')
71-
if not pathname:
72-
raise MonaError(f'Aims "{aims}" not found')
73-
path = Path(pathname)
74-
speciesdir = path.parents[1] / 'aimsfiles/species_defaults' / sp_def
75-
self._speciesdirs[sp_def_key] = speciesdir # type: ignore
76-
kwargs['speciesdir'] = speciesdir
98+
class Atoms(AimsPlugin):
99+
"""Aims plugin handling geometry.
77100
101+
Keywords processed: ``atoms``. Keywords added: ``geom``.
102+
"""
78103

79-
class Atoms(AimsPlugin):
80104
def process(self, kwargs: Dict[str, Any]) -> None:
81105
if 'atoms' in kwargs:
82106
kwargs['geom'] = Molecule([Atom(*args) for args in kwargs.pop('atoms')])
83107

84108

85109
class SpeciesDefaults(AimsPlugin):
86-
"""Aims plugin that handles adding species defaults to control.in."""
110+
"""Aims plugin handling species specifications.
111+
112+
:param mod: Callable that is passed the species specifications for modification.
113+
114+
Keywords added: ``species_specs``. Keywords used: ``geom``, ``species_defaults``.
115+
"""
87116

88117
def __init__(self, mod: Callable[..., Any] = None) -> None:
89118
self._species_defs: Dict[Tuple[Path, str], Dict[str, Any]] = {}
90119
self._mod = mod
91120

92121
def process(self, kwargs: Dict[str, Any]) -> None: # noqa: D102
93-
speciesdir = kwargs.pop('speciesdir')
122+
speciesdir = Path(os.environ['AIMS_SPECIES_DEFAULTS']) / kwargs.pop(
123+
'species_defaults'
124+
)
94125
all_species = {(a.number, a.species) for a in kwargs['geom'].centers}
95-
species_defs = []
126+
species_specs = []
96127
for Z, species in sorted(all_species):
97128
if (speciesdir, species) not in self._species_defs:
98129
species_def = parse_aims_input(
@@ -101,11 +132,11 @@ def process(self, kwargs: Dict[str, Any]) -> None: # noqa: D102
101132
self._species_defs[speciesdir, species] = species_def
102133
else:
103134
species_def = self._species_defs[speciesdir, species]
104-
species_defs.append(species_def)
135+
species_specs.append(species_def)
105136
if self._mod:
106-
species_defs = deepcopy(species_defs)
107-
self._mod(species_defs, kwargs)
108-
kwargs['species_defs'] = species_defs
137+
species_specs = deepcopy(species_specs)
138+
self._mod(species_specs, kwargs)
139+
kwargs['species_specs'] = species_specs
109140

110141
def _func_hash(self) -> str:
111142
if not self._mod:
@@ -115,9 +146,14 @@ def _func_hash(self) -> str:
115146

116147

117148
class Control(AimsPlugin):
149+
"""Aims plugin generating ``control.in``.
150+
151+
Keywords processed: ``species_specs``, ``tags``. Keywords added: ``control.in``.
152+
"""
153+
118154
def process(self, kwargs: Dict[str, Any]) -> None:
119155
species_tags = []
120-
for spec in kwargs.pop('species_defs'):
156+
for spec in kwargs.pop('species_specs'):
121157
spec = OrderedDict(spec)
122158
while spec:
123159
tag, value = spec.popitem(last=False)
@@ -142,32 +178,34 @@ def process(self, kwargs: Dict[str, Any]) -> None:
142178
lines.extend(f'{tag} {p2f(v)}' for v in value)
143179
else:
144180
lines.append(f'{tag} {p2f(value)}')
145-
kwargs['control'] = '\n'.join(lines)
181+
kwargs['control.in'] = '\n'.join(lines)
146182

147183

148184
class Geom(AimsPlugin):
149-
def process(self, kwargs: Dict[str, Any]) -> None:
150-
kwargs['geometry'] = kwargs.pop('geom').dumps('aims')
185+
"""Aims plugin generating ``geometry.in``.
151186
187+
Keywords processed: ``geom``. Keywords added: ``geometry.in``.
188+
"""
152189

153-
class Core(AimsPlugin):
154190
def process(self, kwargs: Dict[str, Any]) -> None:
155-
kwargs['inputs'] = [
156-
('control.in', kwargs.pop('control')),
157-
('geometry.in', kwargs.pop('geometry')),
158-
]
191+
kwargs['geometry.in'] = kwargs.pop('geom').dumps('aims')
159192

160193

161194
class Script(AimsPlugin):
195+
"""Aims plugin generating script for the Aims taks.
196+
197+
Keywords processed: ``aims``, ``check``. Keywords added: ``script``.
198+
"""
199+
162200
def process(self, kwargs: Dict[str, Any]) -> None:
163201
aims, check = kwargs.pop('aims'), kwargs.pop('check', True)
164-
lines = ['#!/bin/bash', 'set -e', f'AIMS={aims} run_aims']
202+
lines = ['#!/bin/bash', 'set -e', f'MONA_AIMS={aims} mona_aims']
165203
if check:
166204
lines.append('egrep "Have a nice day|stop_if_parser" STDOUT >/dev/null')
167205
kwargs['script'] = '\n'.join(lines)
168206

169207

170-
default_plugins = [SpeciesDir, Atoms, SpeciesDefaults, Control, Geom, Core, Script]
208+
Aims.plugin_factories = [Atoms, SpeciesDefaults, Control, Geom, Script]
171209

172210

173211
def p2f(value: Any, nospace: bool = False) -> str:

0 commit comments

Comments
 (0)