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
55from collections import OrderedDict
66from copy import deepcopy
77from pathlib import Path
8- from typing import Any , Callable , Dict , Tuple , cast
8+ from typing import Any , Callable , Dict , List , Tuple , Type , cast
99
1010from ...dirtask import dir_task
11- from ...errors import InvalidInput , MonaError
11+ from ...errors import InvalidInput
1212from ...files import File
1313from ...pluggable import Pluggable , Plugin
1414from ...pyhash import hash_function
1515from ...tasks import Task
1616from ..geomlib import Atom , Molecule
1717from .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
2323class AimsPlugin (Plugin ['Aims' ]):
@@ -29,24 +29,62 @@ def _func_hash(self) -> str:
2929
3030
3131class 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
85109class 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
117148class 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
148184class 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
161194class 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
173211def p2f (value : Any , nospace : bool = False ) -> str :
0 commit comments