settingsbasisset.py 12.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Copyright 2018 Markus Scheidgen
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import OrderedDict
from abc import ABC, abstractmethod
17
from nomad.parsing.backend import Section
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import re
import numpy

J_to_Ry = 4.587425e+17


class SettingsBasisSet(ABC):
    """Abstract base class for basis set settings

    Provides a factory() static method for delegating to the concrete
    implementation applicable for a given calculation.
    """
    def __init__(self, context, backend, logger):
        """
        """
        self.context = context
        self.backend = backend
        self.logger = logger

    @staticmethod
    def factory(ctx, backend, logger):
        """Decide which type of basis set settings are applicable to the entry
        and return a corresponding SettingsBasisSet class.
        """
        # Check if there is a code-specific SettingsBasisSet
        for cls in SettingsBasisSetCodeDependent.__subclasses__():
            if cls.is_basis_set_for(backend):
                return cls(ctx, backend, logger)

        # Check if there is a generic SettingsBasisSet
        for cls in SettingsBasisSetGeneric.__subclasses__():
            if cls.is_basis_set_for(backend):  # pylint: disable=E1120
                return cls(ctx, backend, logger)

        # Raise Exception in case we did not find an implementation
        raise ValueError("No SettingsBasisSet available for this entry.")

    @staticmethod
    @abstractmethod
    def is_basis_set_for(backend):
        """Report if this (sub-)class is applicable to the given calculation"""
        return False

    def to_dict(self, result_dict=None):
        """Dictionary representation of basis set settings; optionally, add
        as new entries to a pre-existing dictionary 'result_dict'"""
        # create new dictionary if necessary
        if result_dict is None:
            result_dict = OrderedDict()
        return result_dict


class SettingsBasisSetGeneric(SettingsBasisSet):
    """Base class for code-independent / generic basis set settings"""
    pass


class SettingsBasisSetCodeDependent(SettingsBasisSet):
    """Base class for code-dependent basis set settings"""
    pass


class SettingsBasisSetAuxiliary(SettingsBasisSet):
    """Base class for auxiliary data; to be inherited by other classes
    """
    def is_basis_set_for(self, backend):
        # auxiliary data should never be autodetected as basis set
        return False


class SettingsBasisSetAuxiliary_Pseudopotentials(SettingsBasisSetAuxiliary):
    """Utility class for any calculations employing pseudopotentials
    """
    def to_dict(self, result_dict=None):
        """Extract the name of the pseudopotential used for each atom,
        and return the resulting list in dictionary key
        'atoms_pseudopotentials'
        """
        result_dict = super().to_dict(result_dict)
        atom_kind_list = self.backend.get('section_method_atom_kind')
        if atom_kind_list is None:
            return result_dict

        # Default lookup strategy is by atom label
        #   build lookup table: label -> atom_kind_index
        atom_kind_by_label = {}
        for atom_kind_i in range(len(atom_kind_list)):
            labels = atom_kind_list[atom_kind_i].get('method_atom_kind_label')
            if labels is not None:
                for label in labels:
                    atom_kind_by_label[label] = atom_kind_list[atom_kind_i]
        sec_sys = self.context.representative_system
        atom_labels = sec_sys.get('atom_labels')
        atom_kinds = [None] * len(atom_labels)
        for atom_i in range(len(atom_labels)):
            atom_kinds[atom_i] = atom_kind_by_label.get(
                atom_labels[atom_i], None)

        # Fallback for VASP, which provides atom->index ref list but no
        #   'method_atom_kind_label'
        sec_method = self.context.representative_method
        atom_kind_index = sec_method.get('x_vasp_atom_kind_refs')
        if atom_kind_index is not None:
            for atom_i in range(len(atom_kind_index)):
                kind_i = atom_kind_index[atom_i]
                if kind_i < len(atom_kind_list):
                    atom_kinds[atom_i] = atom_kind_list[kind_i]
        result = []
        have_names = 0
        for atom_kind in atom_kinds:
            if atom_kind is None:
                result.append(None)
                continue
            pp_name = atom_kind.get('method_atom_kind_pseudopotential_name')
            if pp_name is not None:
                pp_name = pp_name[0]
            elif self.backend['program_name'] == "Quantum Espresso":
                # Fallback for quantum espresso code-specific metaInfo
                fname = atom_kind.get('x_qe_pp_filename')
                if fname is not None:
                    # Use basename of PP file
                    m = re.match(r".*?([^/]+)$", fname[0])
                    pp_name = m.group(1)
            if pp_name is not None:
                have_names += 1
            result.append(pp_name)
        if have_names > 0:
            result_dict['atoms_pseudopotentials'] = result
        return result_dict


class SettingsBasisSetGenericPlaneWaves(
    SettingsBasisSetGeneric,
    SettingsBasisSetAuxiliary_Pseudopotentials,
):
    """Basis set settings for plane-waves codes
    """
    @staticmethod
    def is_basis_set_for(backend):
        return backend.get('program_basis_set_type') == 'plane waves'

    def to_dict(self, result_dict=None):
        result_dict = super().to_dict(result_dict)
        try:
            result_dict['plane_wave_cutoff_wavefunction'] = '%.4f' % (
                self._plane_wave_cutoff('wavefunction'))
        except Exception:
            result_dict['plane_wave_cutoff_wavefunction'] = None

        try:
            result_dict['plane_wave_cutoff_density'] = '%.4f' % (
                self._plane_wave_cutoff('density'))
        except Exception:
            # NOTE: treat charge-density cutoff for now as optional
            pass

        return result_dict

    def _plane_wave_cutoff(self, kind):
        """get plane-wave cutoff for a kind of grid; presently defined kinds
        are 'wavefunction' and 'density'.
        Returns value in Rydberg units."""
        meth_bs = self.backend.get('section_method_basis_set')
        if meth_bs is None:
            return None
        for component in meth_bs:
            component_kind = component.get('method_basis_set_kind')
            if component_kind != kind:
                continue
            bs_id = component.get(
                'mapping_section_method_basis_set_cell_associated')
            bs_ref = 'section_basis_set_cell_dependent:%d' % (bs_id)
            bs = self.backend.get(bs_ref)
            cutoff = bs.get('basis_set_planewave_cutoff')[0] * J_to_Ry
            return cutoff


195
196
class SettingsBasisSetCodeDependentFhiAims(SettingsBasisSetCodeDependent):
    """Basis set settings for 'FHI-Aims' (code-dependent).
197
198
199
200
201
202
203
204
205
206
207
    """
    @staticmethod
    def is_basis_set_for(backend):
        return backend.get('program_name') == 'FHI-aims'

    def to_dict(self, result_dict=None):
        """Special case of basis set settings for FHI-Aims code.
        """
        result_dict = super().to_dict(result_dict)
        result_dict['FhiAims_basis'] = None

208
        # Try to find repeated sections that contain basis set definitions
209
210
211
212
213
214
215
        aims_bs = self.backend.get('x_fhi_aims_section_controlIn_basis_set')
        if aims_bs is None:
            self.logger.warning(
                "could not resolve x_fhi_aims_section_controlIn_basis_set"
            )
            return result_dict

216
        # Get basis set settings for each species
217
218
219
220
221
222
        bs_by_species = {}
        for this_aims_bs in aims_bs:
            this_bs_dict = self._values_to_dict(this_aims_bs, level=2)
            this_species = this_aims_bs['x_fhi_aims_controlIn_species_name'][0]
            bs_by_species[this_species] = this_bs_dict

223
        # Sorted alphabetically by species label
224
225
226
227
        if bs_by_species:
            result_dict['FhiAims_basis'] = OrderedDict()
            for k in sorted(bs_by_species.keys()):
                result_dict['FhiAims_basis'][k] = bs_by_species[k]
228

229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
        return result_dict

    # TODO: this should go as .keys() method to archive.py hdf5/json
    @classmethod
    def _filtered_section_keys(cls, section):
        for k in section.keys():
            # skip json-specific keys
            if k == 'gIndex':
                continue
            if k == 'name':
                continue
            if k == 'references':
                continue
            if k == 'type':
                continue
            # skip hdf5-specific keys
            if k.endswith("-index"):
                continue
            if k.endswith("-v"):
                # hdf5 values
                yield k[:-2]
            else:
                # json values and subsections
                yield k

    @classmethod
    def _values_to_dict(cls, data, level=0):
        result = None
        if data is None:
            return None
259
        elif isinstance(data, (Section, dict)):
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
            result = OrderedDict()
            for k in sorted(cls._filtered_section_keys(data)):
                v = data.get(k, None)
                result[k] = cls._values_to_dict(v, level=level + 1)
        elif isinstance(data, (list)):
            result = []
            for k in range(len(data)):
                v = data[k]
                result.append(cls._values_to_dict(v, level=level + 1))
        elif isinstance(data, (numpy.ndarray)):
            result = data.tolist()
        else:
            result = data
        return result


276
277
class SettingsBasisSetCodeDependent_Exciting(SettingsBasisSetCodeDependent):
    """Basis set settings for 'Exciting' (code-dependent).
278
279
280
281
282
283
284
285
286
287
288
289
    """
    @staticmethod
    def is_basis_set_for(backend):
        return backend.get('program_name') == 'exciting'

    def to_dict(self, result_dict=None):
        """Special case of basis set settings for Exciting code. See list at:
        https://gitlab.mpcdf.mpg.de/nomad-lab/encyclopedia-general/wikis/FHI-visit-preparation
        """
        result_dict = super().to_dict(result_dict)
        system = self.context.representative_system

290
        # Add the muffin-tin settings for each species ordered alphabetically by atom label
291
        try:
292
293
294
295
296
297
298
299
300
301
302
303
304
305
            groups = self.backend["x_exciting_section_atoms_group"]
            groups = sorted(groups, key=lambda group: group["x_exciting_geometry_atom_labels"])
            for group in groups:
                label = group["x_exciting_geometry_atom_labels"]
                try:
                    result_dict["{}_muffin_tin_radius".format(label)] = "%.6f" % (1e+10 * group['x_exciting_muffin_tin_radius'])
                except KeyError:
                    result_dict["{}_muffin_tin_radius".format(label)] = None
                try:
                    result_dict["{}_muffin_tin_points".format(label)] = "%d" % group['x_exciting_muffin_tin_points']
                except KeyError:
                    result_dict["{}_muffin_tin_points".format(label)] = None
        except KeyError:
            pass
306

307
        # Other important method settings
308
309
        try:
            result_dict['rgkmax'] = "%.6f" % (system['x_exciting_rgkmax'])
310
311
312
        except Exception:
            result_dict['rgkmax'] = None
        try:
313
            result_dict['gkmax'] = "%.6f" % (1e-10 * system['x_exciting_gkmax'])
314
315
316
        except Exception:
            result_dict['gkmax'] = None
        try:
317
            result_dict['lo'] = "%d" % (system['x_exciting_lo'])
318
319
320
        except Exception:
            result_dict['lo'] = None
        try:
321
            result_dict['lmaxapw'] = "%d" % (system['x_exciting_lmaxapw'])
322
323
324
325
        except Exception:
            result_dict['lmaxapw'] = None

        return result_dict