• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.12
/pymatgen/io/cp2k/inputs.py
1
"""
2
This module defines the building blocks of a CP2K input file. The cp2k input structure is
3
essentially a collection of "sections" which are similar to dictionary objects that activate
4
modules of the cp2k executable, and then "keywords" which adjust variables inside of those
5
modules. For example, FORCE_EVAL section will activate CP2K's ability to calculate forces,
6
and inside FORCE_EVAL, the Keyword "METHOD can be set to "QS" to set the method of force
7
evaluation to be the quickstep (DFT) module.
8

9
A quick overview of the module:
10

11
-- Section class defines the basis of Cp2k input and contains methods for manipulating these
12
   objects similarly to Dicts.
13
-- Keyword class defines the keywords used inside of Section objects that changes variables in
14
   Cp2k programs.
15
-- SectionList and KeywordList classes are lists of Section and Keyword objects that have
16
   the same dictionary key. This deals with repeated sections and keywords.
17
-- Cp2kInput class is special instantiation of Section that is used to represent the full cp2k
18
   calculation input.
19
-- The rest of the classes are children of Section intended to make initialization of common
20
   sections easier.
21

22
"""
23

24
from __future__ import annotations
1✔
25

26
import copy
1✔
27
import itertools
1✔
28
import os
1✔
29
import re
1✔
30
import textwrap
1✔
31
from dataclasses import dataclass, field
1✔
32
from hashlib import md5
1✔
33
from pathlib import Path
1✔
34
from typing import Any, Iterable, Literal, Sequence
1✔
35

36
from monty.io import zopen
1✔
37
from monty.json import MSONable
1✔
38

39
from pymatgen.core.lattice import Lattice
1✔
40
from pymatgen.core.periodic_table import Element
1✔
41
from pymatgen.core.structure import Molecule, Structure
1✔
42
from pymatgen.io.cp2k.utils import chunk, postprocessor, preprocessor
1✔
43
from pymatgen.io.vasp.inputs import Kpoints as VaspKpoints
1✔
44
from pymatgen.io.vasp.inputs import Kpoints_supported_modes
1✔
45
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1✔
46

47
__author__ = "Nicholas Winner"
1✔
48
__version__ = "2.0"
1✔
49
__email__ = "nwinner@berkeley.edu"
1✔
50
__date__ = "September 2022"
1✔
51

52
MODULE_DIR = Path(__file__).resolve().parent
1✔
53

54

55
class Keyword(MSONable):
1✔
56
    """
57
    Class representing a keyword argument in CP2K. Within CP2K Sections, which activate features
58
    of the CP2K code, the keywords are arguments that control the functionality of that feature.
59
    For example, the section "FORCE_EVAL" activates the evaluation of forces/energies, but within
60
    "FORCE_EVAL" the keyword "METHOD" controls whether or not this will be done with, say,
61
    "Quickstep" (DFT) or "EIP" (empirical interatomic potential).
62
    """
63

64
    def __init__(
1✔
65
        self,
66
        name: str,
67
        *values,
68
        description: str | None = None,
69
        units: str | None = None,
70
        verbose: bool | None = True,
71
        repeats: bool | None = False,
72
    ):
73
        """
74
        Initializes a keyword. These Keywords and the value passed to them are sometimes as simple
75
        as KEYWORD VALUE, but can also be more elaborate such as KEYWORD [UNITS] VALUE1 VALUE2,
76
        which is why this class exists: to handle many values and control easy printing to an
77
        input file.
78

79
        Args:
80
            name: The name of this keyword. Must match an acceptable keyword from CP2K
81
            values: All non-keyword arguments after 'name' are interpreted as the values to set for
82
                this keyword. i.e: KEYWORD ARG1 ARG2 would provide two values to the keyword.
83
            description: The description for this keyword. This can make readability of
84
                input files easier for some. Default=None.
85
            units: The units for this keyword. If not specified, CP2K default units will be
86
                used. Consult manual for default units. Default=None.
87
            verbose: Whether the description should be printed with the string of this keyword
88
            repeats: Whether or not this keyword may be repeated. Default=False.
89
        """
90
        self.name = name
1✔
91
        self.values = values
1✔
92
        self.description = description
1✔
93
        self.repeats = repeats
1✔
94
        self.units = units
1✔
95
        self.verbose = verbose
1✔
96

97
    def __str__(self):
1✔
98
        return (
1✔
99
            self.name.__str__()
100
            + " "
101
            + (f"[{self.units}] " if self.units else "")
102
            + " ".join(map(str, self.values))
103
            + (" ! " + self.description if (self.description and self.verbose) else "")
104
        )
105

106
    def __eq__(self, other: object) -> bool:
1✔
107
        if not isinstance(other, Keyword):
1✔
108
            return NotImplemented
×
109
        if self.name.upper() == other.name.upper():
1✔
110
            v1 = [_.upper() if isinstance(_, str) else _ for _ in self.values]
1✔
111
            v2 = [_.upper() if isinstance(_, str) else _ for _ in other.values]
1✔
112
            if v1 == v2:
1✔
113
                if self.units == self.units:
1✔
114
                    return True
1✔
115
        return False
×
116

117
    def __add__(self, other):
1✔
118
        return KeywordList(keywords=[self, other])
1✔
119

120
    def __getitem__(self, item):
1✔
121
        return self.values[item]
×
122

123
    def as_dict(self):
1✔
124
        """
125
        Get a dictionary representation of the Keyword
126
        """
127
        d = {}
1✔
128
        d["@module"] = type(self).__module__
1✔
129
        d["@class"] = type(self).__name__
1✔
130
        d["name"] = self.name
1✔
131
        d["values"] = self.values
1✔
132
        d["description"] = self.description
1✔
133
        d["repeats"] = self.repeats
1✔
134
        d["units"] = self.units
1✔
135
        d["verbose"] = self.verbose
1✔
136
        return d
1✔
137

138
    def get_string(self):
1✔
139
        """
140
        String representation of Keyword
141
        """
142
        return str(self)
1✔
143

144
    @classmethod
1✔
145
    def from_dict(cls, d):
1✔
146
        """
147
        Initialise from dictionary
148
        """
149
        return Keyword(
1✔
150
            d["name"],
151
            *d["values"],
152
            description=d["description"],
153
            repeats=d["repeats"],
154
            units=d["units"],
155
            verbose=d["verbose"],
156
        )
157

158
    @staticmethod
1✔
159
    def from_string(s):
1✔
160
        """
161
        Initialise from a string.
162

163
        Keywords must be labeled with strings. If the postprocessor finds
164
        that the keywords is a number, then None is return (used by
165
        the file reader).
166

167
        Returns:
168
            Keyword or None
169
        """
170
        s = s.strip()
1✔
171
        if "!" in s or "#" in s:
1✔
172
            s, description = re.split("(?:!|#)", s)
1✔
173
            description = description.strip()
1✔
174
        else:
175
            description = None
1✔
176
        units = re.findall(r"\[(.*)\]", s) or [None]
1✔
177
        s = re.sub(r"\[(.*)\]", "", s)
1✔
178
        args = list(map(postprocessor, s.split()))
1✔
179
        args[0] = str(args[0])
1✔
180
        return Keyword(*args, units=units[0], description=description)
1✔
181

182
    def verbosity(self, v):
1✔
183
        """
184
        Change the printing of this keyword's description.
185
        """
186
        self.verbose = v
×
187

188

189
class KeywordList(MSONable):
1✔
190
    """
191
    Some keywords can be repeated, which makes accessing them via the normal dictionary
192
    methods a little unnatural. This class deals with this by defining a collection
193
    of same-named keywords that are accessed by one name.
194
    """
195

196
    def __init__(self, keywords: Sequence[Keyword]):
1✔
197
        """
198
        Initializes a keyword list given a sequence of keywords.
199

200
        Args:
201
            keywords: A list of keywords. Must all have the same name (case-insensitive)
202
        """
203
        assert all(k.name.upper() == keywords[0].name.upper() for k in keywords) if keywords else True
1✔
204
        self.name = keywords[0].name if keywords else None
1✔
205
        self.keywords = keywords
1✔
206

207
    def __str__(self):
1✔
208
        return self.get_string()
×
209

210
    def __eq__(self, other: object) -> bool:
1✔
211
        if not isinstance(other, type(self)):
×
212
            return NotImplemented
×
213
        return all(k == o for k, o in zip(self.keywords, other.keywords))
×
214

215
    def __add__(self, other):
1✔
216
        return self.extend(other)
×
217

218
    def __len__(self):
1✔
219
        return len(self.keywords)
1✔
220

221
    def __getitem__(self, item):
1✔
222
        return self.keywords[item]
1✔
223

224
    def append(self, item):
1✔
225
        """
226
        Append the keyword list
227
        """
228
        self.keywords.append(item)
×
229

230
    def extend(self, l):
1✔
231
        """
232
        Extend the keyword list
233
        """
234
        self.keywords.extend(l)
×
235

236
    def get_string(self, indent=0):
1✔
237
        """
238
        String representation of Keyword
239
        """
240
        return " \n".join("\t" * indent + str(k) for k in self.keywords)
1✔
241

242
    def verbosity(self, verbosity):
1✔
243
        """
244
        Silence all keywords in keyword list
245
        """
246
        for k in self.keywords:
×
247
            k.verbosity(verbosity)
×
248

249

250
class Section(MSONable):
1✔
251
    """
252
    Basic input representation of input to Cp2k. Activates functionality inside of the
253
    Cp2k executable.
254
    """
255

256
    def __init__(
1✔
257
        self,
258
        name: str,
259
        subsections: dict | None = None,
260
        repeats: bool = False,
261
        description: str | None = None,
262
        keywords: dict | None = None,
263
        section_parameters: list | tuple | None = None,
264
        location: str | None = None,
265
        verbose: bool | None = True,
266
        alias: str | None = None,
267
        **kwargs,
268
    ):
269
        """
270
        Basic object representing a CP2K Section. Sections activate different parts of the
271
        calculation. For example, FORCE_EVAL section will activate CP2K's ability to calculate
272
        forces.
273

274
        Args:
275
            name: The name of the section (must match name in CP2K)
276
            subsections: A dictionary of subsections that are nested in this section.
277
                Format is {'NAME': Section(*args, **kwargs). The name you chose for 'NAME'
278
                to index that subsection does not *have* to be the same as the section's true name,
279
                but we recommend matching them. You can specify a blank dictionary if there are
280
                no subsections, or if you want to insert the subsections later.
281
            repeats: Whether or not this section can be repeated. Most sections cannot.
282
                Default=False.
283
            description: Description of this section for easier readability
284
            keywords: the keywords to be set for this section. Each element should be a
285
                Keyword object. This can be more cumbersome than simply using kwargs for building
286
                a class in a script, but is more convenient for the class instantiations of CP2K
287
                sections (see below).
288
            section_parameters: the section parameters for this section. Section parameters
289
                are specialized keywords that modify the behavior of the section overall. Most
290
                sections do not have section parameters, but some do. Unlike normal Keywords,
291
                these are specified as strings and not as Keyword objects.
292
            location: the path to the section in the form 'SECTION/SUBSECTION1/SUBSECTION3',
293
                example for QS module: 'FORCE_EVAL/DFT/QS'. This location is used to automatically
294
                determine if a subsection requires a supersection to be activated.
295
            verbose: Controls how much is printed to Cp2k input files (Also see Keyword).
296
                If True, then a description of the section will be printed with it as a comment
297
                (if description is set). Default=True.
298
            alias: An alias for this class to use in place of the name.
299

300
            kwargs are interpreted as keyword, value pairs and added to the keywords array as
301
            Keyword objects
302
        """
303
        self.name = name
1✔
304
        self.subsections = subsections if subsections else {}
1✔
305
        self.repeats = repeats
1✔
306
        self.description = description
1✔
307
        keywords = keywords if keywords else {}
1✔
308
        self.keywords = keywords
1✔
309
        self.section_parameters = section_parameters if section_parameters else []
1✔
310
        self.location = location
1✔
311
        self.verbose = verbose
1✔
312
        self.alias = alias
1✔
313
        self.kwargs = kwargs
1✔
314
        for k, v in self.kwargs.items():
1✔
315
            self.keywords[k] = Keyword(k, v)
1✔
316

317
    def __str__(self):
1✔
318
        return self.get_string()
×
319

320
    def __eq__(self, d):
1✔
321
        d2 = copy.deepcopy(d)
×
322
        s2 = copy.deepcopy(self)
×
323
        d2.silence()
×
324
        s2.silence()
×
325
        return d2.as_dict() == s2.as_dict()
×
326

327
    def __deepcopy__(self, memodict=None):
1✔
328
        c = copy.deepcopy(self.as_dict())
1✔
329
        return getattr(__import__(c["@module"], globals(), locals(), c["@class"], 0), c["@class"]).from_dict(
1✔
330
            copy.deepcopy(self.as_dict())
331
        )
332

333
    def __getitem__(self, d):
1✔
334
        r = self.get_keyword(d)
1✔
335
        if not r:
1✔
336
            r = self.get_section(d)
1✔
337
        if r:
1✔
338
            return r
1✔
339
        raise KeyError
×
340

341
    def __add__(self, other):
1✔
342
        if isinstance(other, (Keyword, KeywordList)):
1✔
343
            if other.name in self.keywords:
1✔
344
                self.keywords[other.name] += other
×
345
            else:
346
                self.keywords[other.name] = other
1✔
347
        elif isinstance(other, (Section, SectionList)):
×
348
            self.insert(other)
×
349
        else:
350
            TypeError("Can only add sections or keywords.")
×
351

352
    def __setitem__(self, key, value):
1✔
353
        self.setitem(key, value)
1✔
354

355
    def setitem(self, key, value, strict=False):
1✔
356
        """
357
        Helper function for setting items. Kept separate from the double-underscore function so that
358
        "strict" option can be made possible.
359

360
        strict will only set values for items that already have a key entry (no insertion).
361
        """
362
        if isinstance(value, (Section, SectionList)):
1✔
363
            if key in self.subsections:
1✔
364
                self.subsections[key] = copy.deepcopy(value)
1✔
365
            elif not strict:
1✔
366
                self.insert(value)
1✔
367
        else:
368
            if not isinstance(value, (Keyword, KeywordList)):
1✔
369
                value = Keyword(key, value)
×
370
            match = [k for k in self.keywords if key.upper() == k.upper()]
1✔
371
            if match:
1✔
372
                del self.keywords[match[0]]
1✔
373
                self.keywords[key] = value
1✔
374
            elif not strict:
1✔
375
                self.keywords[key] = value
1✔
376

377
    def __delitem__(self, key):
1✔
378
        """
379
        Delete section with name matching key OR delete all keywords
380
        with names matching this key
381
        """
382
        l = [s for s in self.subsections if s.upper() == key.upper()]
1✔
383
        if l:
1✔
384
            del self.subsections[l[0]]
×
385
            return
×
386
        l = [k for k in self.keywords if k.upper() == key.upper()]
1✔
387
        if l:
1✔
388
            del self.keywords[l[0]]
1✔
389
            return
1✔
390
        raise KeyError("No section or keyword matching the given key.")
×
391

392
    def __sub__(self, other):
1✔
393
        return self.__delitem__(other)
×
394

395
    def add(self, other):
1✔
396
        """
397
        Add another keyword to the current section
398
        """
399
        assert isinstance(other, (Keyword, KeywordList))
1✔
400
        self + other
1✔
401

402
    def get(self, d, default=None):
1✔
403
        """
404
        Similar to get for dictionaries. This will attempt to retrieve the
405
        section or keyword matching d. Will not raise an error if d does not
406
        exist.
407

408
        Args:
409
             d: the key to retrieve, if present
410
             default: what to return if d is not found
411
        """
412
        r = self.get_keyword(d)
1✔
413
        if r:
1✔
414
            return r
1✔
415
        r = self.get_section(d)
1✔
416
        if r:
1✔
417
            return r
×
418
        return default
1✔
419

420
    def get_section(self, d, default=None):
1✔
421
        """
422
        Get function, only for subsections
423

424
        Args:
425
            d: Name of section to get
426
            default: return if d is not found in subsections
427
        """
428
        for k, v in self.subsections.items():
1✔
429
            if str(k).upper() == str(d).upper():
1✔
430
                return v
1✔
431
        return default
1✔
432

433
    def get_keyword(self, d, default=None):
1✔
434
        """
435
        Get function, only for subsections
436

437
        Args:
438
            d: Name of keyword to get
439
            default: return if d is not found in keyword list
440
        """
441
        for k, v in self.keywords.items():
1✔
442
            if str(k).upper() == str(d).upper():
1✔
443
                return v
1✔
444
        return default
1✔
445

446
    def update(self, d: dict, strict=False):
1✔
447
        """
448
        Update the Section according to a dictionary argument. This is most useful
449
        for providing user-override settings to default parameters. As you pass a
450
        dictionary the class variables like "description", "location", or "repeats"
451
        are not included. Therefore, it is recommended that this be used to modify
452
        existing Section objects to a user's needs, but not used for the creation
453
        of new Section child-classes.
454

455
        Args:
456
            d (dict): A dictionary containing the update information. Should use nested dictionaries
457
                to specify the full path of the update. If a section or keyword does not exist, it
458
                will be created, but only with the values that are provided in "d", not using
459
                default values from a Section object.
460
                Example: {
461
                    'SUBSECTION1': {
462
                        'SUBSEC2': {'NEW_KEYWORD': 'NEW_VAL'},
463
                        'NEW_SUBSEC': {'NEW_KWD': 'NEW_VAL'}
464
                        }
465
                    }
466

467
            strict (bool): If true, only update existing sections and keywords. If false, allow
468
                new sections and keywords. Default: False
469
        """
470
        Section._update(self, d, strict=strict)
1✔
471

472
    @staticmethod
1✔
473
    def _update(d1, d2, strict=False):
1✔
474
        """
475
        Helper method for self.update(d) method (see above).
476
        """
477
        for k, v in d2.items():
1✔
478
            if isinstance(v, (str, float, int, bool)):
1✔
479
                d1.setitem(k, Keyword(k, v), strict=strict)
1✔
480
            elif isinstance(v, (Keyword, KeywordList)):
1✔
481
                d1.setitem(k, v, strict=strict)
×
482
            elif isinstance(v, dict):
1✔
483
                tmp = [_ for _ in d1.subsections if k.upper() == _.upper()]
1✔
484
                if not tmp:
1✔
485
                    if strict:
1✔
486
                        continue
×
487
                    d1.insert(Section(k, subsections={}))
1✔
488
                    Section._update(d1.subsections[k], v, strict=strict)
1✔
489
                else:
490
                    Section._update(d1.subsections[tmp[0]], v, strict=strict)
1✔
491
            elif isinstance(v, Section):
×
492
                if not strict:
×
493
                    d1.insert(v)
×
494
            else:
495
                raise TypeError(f"Unrecognized type: {type(v)}")
×
496

497
    def set(self, d: dict):
1✔
498
        """
499
        Alias for update. Used by custodian.
500
        """
501
        self.update(d)
1✔
502

503
    def safeset(self, d: dict):
1✔
504
        """
505
        Alias for update with strict (no insertions). Used by custodian.
506
        """
507
        self.update(d, strict=True)
×
508

509
    def unset(self, d: dict):
1✔
510
        """
511
        Dict based deletion. Used by custodian.
512
        """
513
        for k, v in d.items():
1✔
514
            if isinstance(v, (str, float, int, bool)):
1✔
515
                del self[k][v]
1✔
516
            elif isinstance(v, (Keyword, Section, KeywordList, SectionList)):
×
517
                del self[k][v.name]
×
518
            elif isinstance(v, dict):
×
519
                self[k].unset(v)
×
520
            else:
521
                TypeError("Can only add sections or keywords.")
×
522

523
    def inc(self, d: dict):
1✔
524
        """
525
        Mongo style dict modification. Include.
526
        """
527
        for k, v in d.items():
1✔
528
            if isinstance(v, (str, float, bool, int, list)):
1✔
529
                v = Keyword(k, v)
1✔
530
            if isinstance(v, (Keyword, Section, KeywordList, SectionList)):
1✔
531
                self.add(v)
1✔
532
            elif isinstance(v, dict):
1✔
533
                self[k].inc(v)
1✔
534
            else:
535
                TypeError("Can only add sections or keywords.")
×
536

537
    def insert(self, d):
1✔
538
        """
539
        Insert a new section as a subsection of the current one
540
        """
541
        assert isinstance(d, (Section, SectionList))
1✔
542
        self.subsections[d.alias or d.name] = copy.deepcopy(d)
1✔
543

544
    def check(self, path: str):
1✔
545
        """
546
        Check if section exists within the current using a path. Can be useful for cross-checking
547
        whether or not required dependencies have been satisfied, which CP2K does not enforce.
548

549
        Args:
550
            path (str): Path to section of form 'SUBSECTION1/SUBSECTION2/SUBSECTION_OF_INTEREST'
551
        """
552
        _path = path.split("/")
1✔
553
        s = self.subsections
1✔
554
        for p in _path:
1✔
555
            tmp = [_ for _ in s if p.upper() == _.upper()]
1✔
556
            if tmp:
1✔
557
                s = s[tmp[0]].subsections
1✔
558
            else:
559
                return False
1✔
560
        return True
1✔
561

562
    def by_path(self, path: str):
1✔
563
        """
564
        Access a sub-section using a path. Used by the file parser.
565

566
        Args:
567
            path (str): Path to section of form 'SUBSECTION1/SUBSECTION2/SUBSECTION_OF_INTEREST'
568

569
        """
570
        _path = path.split("/")
1✔
571
        if _path[0].upper() == self.name.upper():
1✔
572
            _path = _path[1:]
1✔
573
        s = self
1✔
574
        for p in _path:
1✔
575
            s = s.get_section(p)
1✔
576
        return s
1✔
577

578
    def get_string(self):
1✔
579
        """
580
        Get string representation of Section
581
        """
582
        return Section._get_string(self)
1✔
583

584
    @staticmethod
1✔
585
    def _get_string(d, indent=0):
1✔
586
        """
587
        Helper function to return a pretty string of the section. Includes indentation and
588
        descriptions (if present).
589
        """
590
        string = ""
1✔
591
        if d.description and d.verbose:
1✔
592
            string += (
1✔
593
                "\n"
594
                + textwrap.fill(
595
                    d.description,
596
                    initial_indent="\t" * indent + "! ",
597
                    subsequent_indent="\t" * indent + "! ",
598
                    width=50,
599
                )
600
                + "\n"
601
            )
602
        string += "\t" * indent + "&" + d.name
1✔
603
        string += " " + " ".join(map(str, d.section_parameters)) + "\n"
1✔
604

605
        for v in d.keywords.values():
1✔
606
            if isinstance(v, KeywordList):
1✔
607
                string += v.get_string(indent=indent + 1) + "\n"
1✔
608
            else:
609
                string += "\t" * (indent + 1) + v.get_string() + "\n"
1✔
610
        for v in d.subsections.values():
1✔
611
            string += v._get_string(v, indent + 1)
1✔
612
        string += "\t" * indent + "&END " + d.name + "\n"
1✔
613

614
        return string
1✔
615

616
    def verbosity(self, verbosity):
1✔
617
        """
618
        Change the section verbossity recursively by turning on/off the printing of descriptions.
619
        Turning off descriptions may reduce the appealing documentation of input files, but also
620
        helps de-clutter them.
621
        """
622
        self.verbose = verbosity
×
623
        for v in self.keywords.values():
×
624
            v.verbosity(verbosity)
×
625
        for v in self.subsections.values():
×
626
            v.verbosity(verbosity)
×
627

628
    def silence(self):
1✔
629
        """
630
        Recursively delete all print sections so that only defaults are printed out.
631
        """
632
        if self.subsections:
×
633
            if self.subsections.get("PRINT"):
×
634
                del self.subsections["PRINT"]
×
635
            for v in self.subsections.values():
×
636
                v.silence()
×
637

638

639
class SectionList(MSONable):
1✔
640
    """
641
    Section list
642
    """
643

644
    def __init__(self, sections: Sequence[Section]):
1✔
645
        """
646
        Initializes a SectionList object using a sequence of sections.
647

648
        Args:
649
            sections: A list of keywords. Must all have the same name (case-insensitive)
650
        """
651
        assert all(k.name.upper() == sections[0].name.upper() for k in sections) if sections else True
1✔
652
        self.name = sections[0].name if sections else None
1✔
653
        self.alias = sections[0].alias if sections else None
1✔
654
        self.sections = sections
1✔
655

656
    def __str__(self):
1✔
657
        return self.get_string()
×
658

659
    def __eq__(self, other: object) -> bool:
1✔
660
        if not isinstance(other, SectionList):
×
661
            return NotImplemented
×
662
        return all(k == o for k, o in zip(self.sections, other.sections))
×
663

664
    def __add__(self, other):
1✔
665
        self.append(other)
1✔
666
        return self
1✔
667

668
    def __len__(self):
1✔
669
        return len(self.sections)
1✔
670

671
    def __getitem__(self, item):
1✔
672
        return self.sections[item]
1✔
673

674
    def __deepcopy__(self, memodict=None):
1✔
675
        return SectionList(sections=[d.__deepcopy__() for d in self.sections])
1✔
676

677
    @staticmethod
1✔
678
    def _get_string(d, indent=0):
1✔
679
        return " \n".join(s._get_string(s, indent) for s in d)
1✔
680

681
    def get_string(self):
1✔
682
        """
683
        Return string representation of section list
684
        """
685
        return SectionList._get_string(self.sections)
×
686

687
    def get(self, d, index=-1):
1✔
688
        """
689
        Get for section list. If index is specified, return the section at that index.
690
        Otherwise, return a get on the last section.
691
        """
692
        return self.sections[index].get(d)
1✔
693

694
    def append(self, item):
1✔
695
        """
696
        Append the section list
697
        """
698
        self.sections.append(item)
1✔
699

700
    def extend(self, l):
1✔
701
        """
702
        Extend the section list
703
        """
704
        self.sections.extend(l)
×
705

706
    def verbosity(self, verbosity):
1✔
707
        """
708
        Silence all sections in section list
709
        """
710
        for k in self.sections:
×
711
            k.verbosity(verbosity)
×
712

713

714
class Cp2kInput(Section):
1✔
715
    """
716
    Special instance of 'Section' class that is meant to represent the overall cp2k input.
717
    Distinguishes itself from Section by overriding get_string() to not print this section's
718
    title and by implementing the file i/o.
719
    """
720

721
    def __init__(self, name: str = "CP2K_INPUT", subsections: dict | None = None, **kwargs):
1✔
722
        """
723
        Initialize Cp2kInput by calling the super
724
        """
725
        self.name = name
1✔
726
        self.subsections = subsections if subsections else {}
1✔
727
        self.kwargs = kwargs
1✔
728

729
        description = "CP2K Input"
1✔
730
        super().__init__(
1✔
731
            name,
732
            repeats=False,
733
            description=description,
734
            section_parameters=[],
735
            subsections=subsections,
736
            **kwargs,
737
        )
738

739
    def get_string(self):
1✔
740
        """
741
        Get string representation of the Cp2kInput
742
        """
743
        s = ""
1✔
744
        for v in self.subsections.values():
1✔
745
            s += v.get_string()
1✔
746
        return s
1✔
747

748
    @classmethod
1✔
749
    def _from_dict(cls, d):
1✔
750
        """
751
        Initialize from a dictionary
752
        """
753
        return Cp2kInput(
×
754
            "CP2K_INPUT",
755
            subsections=getattr(
756
                __import__(d["@module"], globals(), locals(), d["@class"], 0),
757
                d["@class"],
758
            )
759
            .from_dict(d)
760
            .subsections,
761
        )
762

763
    @staticmethod
1✔
764
    def from_file(file: str):
1✔
765
        """
766
        Initialize from a file
767
        """
768
        with zopen(file, "rt") as f:
1✔
769
            txt = preprocessor(f.read(), os.path.dirname(f.name))
1✔
770
            return Cp2kInput.from_string(txt)
1✔
771

772
    @staticmethod
1✔
773
    def from_string(s: str):
1✔
774
        """
775
        Initialize from a string
776
        """
777
        lines = s.splitlines()
1✔
778
        lines = [line.replace("\t", "") for line in lines]
1✔
779
        lines = [line.strip() for line in lines]
1✔
780
        lines = [line for line in lines if line]
1✔
781
        return Cp2kInput.from_lines(lines)
1✔
782

783
    @classmethod
1✔
784
    def from_lines(cls, lines: list | tuple):
1✔
785
        """
786
        Helper method to read lines of file
787
        """
788
        cp2k_input = Cp2kInput("CP2K_INPUT", subsections={})
1✔
789
        Cp2kInput._from_lines(cp2k_input, lines)
1✔
790
        return cp2k_input
1✔
791

792
    def _from_lines(self, lines):
1✔
793
        """
794
        Helper method, reads lines of text to get a Cp2kInput
795
        """
796
        current = self.name
1✔
797
        description = ""
1✔
798
        for line in lines:
1✔
799
            if line.startswith("!") or line.startswith("#"):
1✔
800
                description += line[1:].strip()
1✔
801
            elif line.upper().startswith("&END"):
1✔
802
                current = "/".join(current.split("/")[:-1])
1✔
803
            elif line.startswith("&"):
1✔
804
                name, subsection_params = line.split()[0][1:], line.split()[1:]
1✔
805
                subsection_params = (
1✔
806
                    []
807
                    if len(subsection_params) == 1 and subsection_params[0].upper() in ("T", "TRUE", "F", "FALSE", "ON")
808
                    else subsection_params
809
                )
810
                alias = name + " " + " ".join(subsection_params) if subsection_params else None
1✔
811
                s = Section(
1✔
812
                    name,
813
                    section_parameters=subsection_params,
814
                    alias=alias,
815
                    subsections={},
816
                    description=description,
817
                )
818
                description = ""
1✔
819
                tmp = self.by_path(current).get_section(s.alias or s.name)
1✔
820
                if tmp:
1✔
821
                    if isinstance(tmp, SectionList):
1✔
822
                        self.by_path(current)[s.alias or s.name].append(s)
1✔
823
                    else:
824
                        self.by_path(current)[s.alias or s.name] = SectionList(sections=[tmp, s])
1✔
825
                else:
826
                    self.by_path(current).insert(s)
1✔
827
                current = current + "/" + alias if alias else current + "/" + name
1✔
828
            else:
829
                kwd = Keyword.from_string(line)
1✔
830
                tmp = self.by_path(current).get(kwd.name)
1✔
831
                if tmp:
1✔
832
                    if isinstance(tmp, KeywordList):
1✔
833
                        self.by_path(current).get(kwd.name).append(kwd)
×
834
                    else:
835
                        if isinstance(self.by_path(current), SectionList):
1✔
836
                            self.by_path(current)[-1][kwd.name] = KeywordList(keywords=[tmp, kwd])
1✔
837
                        else:
838
                            self.by_path(current)[kwd.name] = KeywordList(keywords=[kwd, tmp])
1✔
839
                else:
840
                    if isinstance(self.by_path(current), SectionList):
1✔
841
                        self.by_path(current)[-1].keywords[kwd.name] = kwd
1✔
842
                    else:
843
                        self.by_path(current).keywords[kwd.name] = kwd
1✔
844

845
    def write_file(
1✔
846
        self,
847
        input_filename: str = "cp2k.inp",
848
        output_dir: str = ".",
849
        make_dir_if_not_present: bool = True,
850
    ):
851
        """Write input to a file.
852

853
        Args:
854
            input_filename (str, optional): Defaults to "cp2k.inp".
855
            output_dir (str, optional): Defaults to ".".
856
            make_dir_if_not_present (bool, optional): Defaults to True.
857
        """
858
        if not os.path.isdir(output_dir) and make_dir_if_not_present:
×
859
            os.mkdir(output_dir)
×
860
        filepath = os.path.join(output_dir, input_filename)
×
861
        with open(filepath, "w") as f:
×
862
            f.write(self.get_string())
×
863

864

865
class Global(Section):
1✔
866
    """
867
    Controls 'global' settings for cp2k execution such as RUN_TYPE and PROJECT_NAME
868
    """
869

870
    def __init__(
1✔
871
        self,
872
        project_name: str = "CP2K",
873
        run_type: str = "ENERGY_FORCE",
874
        keywords: dict | None = None,
875
        **kwargs,
876
    ):
877
        """Initialize the global section
878

879
        Args:
880
            project_name: Defaults to "CP2K".
881
            run_type: what type of calculation to run
882
            keywords: Additional keywords to add
883
        """
884
        self.project_name = project_name
1✔
885
        self.run_type = run_type
1✔
886
        keywords = keywords if keywords else {}
1✔
887

888
        description = (
1✔
889
            "Section with general information regarding which kind of simulation" + " to perform an general settings"
890
        )
891

892
        _keywords = {
1✔
893
            "PROJECT_NAME": Keyword("PROJECT_NAME", project_name),
894
            "RUN_TYPE": Keyword("RUN_TYPE", run_type),
895
            "EXTENDED_FFT_LENGTHS": Keyword("EXTENDED_FFT_LENGTHS", True),
896
        }
897
        keywords.update(_keywords)
1✔
898
        super().__init__(
1✔
899
            "GLOBAL",
900
            description=description,
901
            keywords=keywords,
902
            subsections={},
903
            **kwargs,
904
        )
905

906

907
class ForceEval(Section):
1✔
908
    """
909
    Controls the calculation of energy and forces in Cp2k
910
    """
911

912
    def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs):
1✔
913
        """Initialize the ForceEval section"""
914
        keywords = keywords if keywords else {}
1✔
915
        subsections = subsections if subsections else {}
1✔
916

917
        description = (
1✔
918
            "Parameters needed to calculate energy and forces" + " and describe the system you want to analyze."
919
        )
920

921
        _keywords = {
1✔
922
            "METHOD": Keyword("METHOD", kwargs.get("METHOD", "QS")),
923
            "STRESS_TENSOR": Keyword("STRESS_TENSOR", kwargs.get("STRESS_TENSOR", "ANALYTICAL")),
924
        }
925
        keywords.update(_keywords)
1✔
926
        super().__init__(
1✔
927
            "FORCE_EVAL",
928
            repeats=True,
929
            description=description,
930
            keywords=keywords,
931
            subsections=subsections,
932
            **kwargs,
933
        )
934

935

936
class Dft(Section):
1✔
937
    """
938
    Controls the DFT parameters in Cp2k
939
    """
940

941
    def __init__(
1✔
942
        self,
943
        basis_set_filenames: Iterable = ("BASIS_MOLOPT",),
944
        potential_filename="GTH_POTENTIALS",
945
        uks: bool = True,
946
        wfn_restart_file_name: str | None = None,
947
        keywords: dict | None = None,
948
        subsections: dict | None = None,
949
        **kwargs,
950
    ):
951
        """Initialize the DFT section.
952

953
        Args:
954
            basis_set_filenames: Name of the file that contains the basis set
955
                information. Defaults to "BASIS_MOLOPT".
956
            potential_filename: Name of the file that contains the pseudopotential
957
                information. Defaults to "GTH_POTENTIALS".
958
            uks: Whether to run unrestricted Kohn Sham (spin polarized).
959
                Defaults to True.
960
            wfn_restart_file_name: Defaults to None.
961
            keywords: additional keywords to add.
962
            subsections: Any subsections to initialize with. Defaults to None.
963
        """
964
        self.basis_set_filenames = basis_set_filenames
1✔
965
        self.potential_filename = potential_filename
1✔
966
        self.uks = uks
1✔
967
        self.wfn_restart_file_name = wfn_restart_file_name
1✔
968
        keywords = keywords if keywords else {}
1✔
969
        subsections = subsections if subsections else {}
1✔
970

971
        description = "Parameter needed by dft programs"
1✔
972

973
        _keywords = {
1✔
974
            "BASIS_SET_FILE_NAME": KeywordList([Keyword("BASIS_SET_FILE_NAME", k) for k in basis_set_filenames]),
975
            "POTENTIAL_FILE_NAME": Keyword("POTENTIAL_FILE_NAME", potential_filename),
976
            "UKS": Keyword(
977
                "UKS",
978
                uks,
979
                description="Whether to run unrestricted Kohn Sham (i.e. spin polarized)",
980
            ),
981
        }
982

983
        if wfn_restart_file_name:
1✔
984
            _keywords["WFN_RESTART_FILE_NAME"] = Keyword("WFN_RESTART_FILE_NAME", wfn_restart_file_name)
×
985

986
        keywords.update(_keywords)
1✔
987
        super().__init__(
1✔
988
            "DFT",
989
            description=description,
990
            keywords=keywords,
991
            subsections=subsections,
992
            **kwargs,
993
        )
994

995

996
class Subsys(Section):
1✔
997
    """
998
    Controls the definition of the system to be simulated
999
    """
1000

1001
    def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs):
1✔
1002
        """Initialize the subsys section"""
1003
        keywords = keywords if keywords else {}
1✔
1004
        subsections = subsections if subsections else {}
1✔
1005
        description = "A subsystem: coordinates, topology, molecules and cell"
1✔
1006
        super().__init__("SUBSYS", keywords=keywords, description=description, subsections=subsections, **kwargs)
1✔
1007

1008

1009
class QS(Section):
1✔
1010
    """Controls the quickstep settings (DFT driver)"""
1011

1012
    def __init__(
1✔
1013
        self,
1014
        method: str = "GPW",
1015
        eps_default: float = 1e-10,
1016
        eps_pgf_orb: float | None = None,
1017
        extrapolation: str = "ASPC",
1018
        keywords: dict | None = None,
1019
        subsections: dict | None = None,
1020
        **kwargs,
1021
    ):
1022
        """
1023
        Initialize the QS Section
1024

1025
        Args:
1026
            method ("GPW" | "GAPW"): What DFT methodology to use. GPW (Gaussian Plane Waves) for
1027
                DFT with pseudopotentials or GAPW (Gaussian Augmented Plane Waves) for all
1028
                electron calculations.
1029
            eps_default (float): The default level of convergence accuracy. NOTE: This is a
1030
                global value for all the numerical value of all EPS_* values in QS module.
1031
                It is not the same as EPS_SCF, which sets convergence accuracy of the SCF cycle
1032
                alone.
1033
            eps_pgf_orb: Precision for the overlap matrix. Default is to use sqrt(eps_default)
1034
            extrapolation ("PS" | "ASPC"): Method use for extrapolation. If using
1035
                gamma-point-only calculation, then one should either PS
1036
                or ASPC (ASPC especially for MD runs). See the manual for other options.
1037
            keywords: Additional keywords to add
1038
            subsections: Subsections to initialize with.
1039
        """
1040
        self.method = method
1✔
1041
        self.eps_default = eps_default
1✔
1042
        self.eps_pgf_orb = eps_pgf_orb
1✔
1043
        self.extrapolation = extrapolation
1✔
1044
        keywords = keywords if keywords else {}
1✔
1045
        subsections = subsections if subsections else {}
1✔
1046
        description = "Parameters needed to set up the Quickstep framework"
1✔
1047

1048
        _keywords = {
1✔
1049
            "METHOD": Keyword("METHOD", self.method),
1050
            "EPS_DEFAULT": Keyword("EPS_DEFAULT", self.eps_default, description="Base precision level (in Ha)"),
1051
            "EXTRAPOLATION": Keyword(
1052
                "EXTRAPOLATION", self.extrapolation, description="WFN extrapolation between steps"
1053
            ),
1054
        }
1055
        if eps_pgf_orb:
1✔
1056
            _keywords["EPS_PGF_ORB"] = Keyword("EPS_PGF_ORB", self.eps_pgf_orb, description="Overlap matrix precision")
×
1057
        keywords.update(_keywords)
1✔
1058
        super().__init__(
1✔
1059
            "QS",
1060
            description=description,
1061
            keywords=keywords,
1062
            subsections=subsections,
1063
            **kwargs,
1064
        )
1065

1066

1067
class Scf(Section):
1✔
1068
    """Controls the self consistent field loop"""
1069

1070
    def __init__(
1✔
1071
        self,
1072
        max_scf: int = 50,
1073
        eps_scf: float = 1e-6,
1074
        scf_guess: str = "RESTART",
1075
        keywords: dict | None = None,
1076
        subsections: dict | None = None,
1077
        **kwargs,
1078
    ):
1079
        """
1080
        Initialize the Scf section
1081

1082
        Args:
1083
            max_scf (int): Maximum number of SCF loops before terminating. Defaults to 50.
1084
            eps_scf (float): Convergence criteria for SCF loop. Defaults to 1e-6.
1085
            scf_guess: Initial guess for SCF loop.
1086
                "ATOMIC": Generate an atomic density using the atomic code
1087
                "CORE": Diagonalize the core Hamiltonian for an initial guess.
1088
                "HISTORY_RESTART": Extrapolated from previous RESTART files.
1089
                "MOPAC": Use same guess as MOPAC for semi-empirical methods or a simple
1090
                    diagonal density matrix for other methods.
1091
                "NONE": Skip initial guess (only for NON-SCC DFTB).
1092
                "RANDOM": Use random wavefunction coefficients.
1093
                "RESTART": Use the RESTART file as an initial guess (and ATOMIC if not present).
1094
                "SPARSE": Generate a sparse wavefunction using the atomic code (for OT based
1095
                    methods).
1096
            keywords: Additional keywords
1097
            subsections: Additional subsections
1098
        """
1099
        self.max_scf = max_scf
1✔
1100
        self.eps_scf = eps_scf
1✔
1101
        self.scf_guess = scf_guess
1✔
1102

1103
        keywords = keywords if keywords else {}
1✔
1104
        subsections = subsections if subsections else {}
1✔
1105

1106
        description = "Parameters needed to perform an SCF run."
1✔
1107

1108
        _keywords = {
1✔
1109
            "MAX_SCF": Keyword("MAX_SCF", max_scf, description="Max number of steps for an inner SCF loop"),
1110
            "EPS_SCF": Keyword("EPS_SCF", eps_scf, description="Convergence threshold for SCF"),
1111
            "SCF_GUESS": Keyword("SCF_GUESS", scf_guess, description="How to initialize the density matrix"),
1112
            "MAX_ITER_LUMO": Keyword(
1113
                "MAX_ITER_LUMO",
1114
                kwargs.get("max_iter_lumo", 400),
1115
                description="Iterations for solving for unoccupied levels when running OT",
1116
            ),
1117
        }
1118
        keywords.update(_keywords)
1✔
1119
        super().__init__(
1✔
1120
            "SCF",
1121
            description=description,
1122
            keywords=keywords,
1123
            subsections=subsections,
1124
            **kwargs,
1125
        )
1126

1127

1128
class Mgrid(Section):
1✔
1129
    """Controls the multigrid for numerical integration"""
1130

1131
    def __init__(
1✔
1132
        self,
1133
        cutoff: int | float = 1200,
1134
        rel_cutoff: int | float = 80,
1135
        ngrids: int = 5,
1136
        progression_factor: int = 3,
1137
        keywords: dict | None = None,
1138
        subsections: dict | None = None,
1139
        **kwargs,
1140
    ):
1141
        """
1142
        Initialize the MGRID section
1143

1144
        Args:
1145
            cutoff: Cutoff energy (in Rydbergs for historical reasons) defining how find of
1146
                Gaussians will be used
1147
            rel_cutoff: The relative cutoff energy, which defines how to map the Gaussians onto
1148
                the multigrid. If the the value is too low then, even if you have a high cutoff
1149
                with sharp Gaussians, they will be mapped to the course part of the multigrid
1150
            ngrids: number of grids to use
1151
            progression_factor: divisor that decides how to map Gaussians the multigrid after
1152
                the highest mapping is decided by rel_cutoff
1153
            keywords: additional keywords
1154
            subsections: additional subsections
1155
        """
1156
        self.cutoff = cutoff
1✔
1157
        self.rel_cutoff = rel_cutoff
1✔
1158
        self.ngrids = ngrids
1✔
1159
        self.progression_factor = progression_factor
1✔
1160

1161
        keywords = keywords if keywords else {}
1✔
1162
        subsections = subsections if subsections else {}
1✔
1163
        description = (
1✔
1164
            "Multigrid information. Multigrid allows for sharp gaussians and diffuse "
1165
            + "gaussians to be treated on different grids, where the spacing of FFT integration "
1166
            + "points can be tailored to the degree of sharpness/diffusiveness"
1167
        )
1168

1169
        _keywords = {
1✔
1170
            "CUTOFF": Keyword("CUTOFF", cutoff, description="Cutoff in [Ry] for finest level of the MG."),
1171
            "REL_CUTOFF": Keyword(
1172
                "REL_CUTOFF",
1173
                rel_cutoff,
1174
                description="Controls which gaussians are mapped to which level of the MG",
1175
            ),
1176
            "NGRIDS": Keyword("NGRIDS", ngrids, description="Number of grid levels in the MG"),
1177
            "PROGRESSION_FACTOR": Keyword("PROGRESSION_FACTOR", progression_factor),
1178
        }
1179
        keywords.update(_keywords)
1✔
1180
        super().__init__(
1✔
1181
            "MGRID",
1182
            description=description,
1183
            keywords=keywords,
1184
            subsections=subsections,
1185
            **kwargs,
1186
        )
1187

1188

1189
class Diagonalization(Section):
1✔
1190
    """Controls diagonalization settings (if using traditional diagonalization)."""
1191

1192
    def __init__(
1✔
1193
        self,
1194
        eps_adapt: float = 0,
1195
        eps_iter: float = 1e-8,
1196
        eps_jacobi: float = 0,
1197
        jacobi_threshold: float = 1e-7,
1198
        keywords: dict | None = None,
1199
        subsections: dict | None = None,
1200
        **kwargs,
1201
    ):
1202
        """Initialize the diagronalization section"""
1203
        self.eps_adapt = eps_adapt
×
1204
        self.eps_iter = eps_iter
×
1205
        self.eps_jacobi = eps_jacobi
×
1206
        self.jacobi_threshold = jacobi_threshold
×
1207
        keywords = keywords if keywords else {}
×
1208
        subsections = subsections if subsections else {}
×
1209
        location = "CP2K_INPUT/FORCE_EVAL/DFT/SCF/DIAGONALIZATION"
×
1210
        description = "Settings for the SCF's diagonalization routines"
×
1211

1212
        _keywords = {
×
1213
            "EPS_ADAPT": Keyword("EPS_ADAPT", eps_adapt),
1214
            "EPS_ITER": Keyword("EPS_ITER", eps_iter),
1215
            "EPS_JACOBI": Keyword("EPS_JACOBI", eps_jacobi),
1216
            "JACOBI_THRESHOLD": Keyword("JACOBI_THRESHOLD", jacobi_threshold),
1217
        }
1218
        keywords.update(_keywords)
×
1219
        super().__init__(
×
1220
            "DIAGONALIZATION",
1221
            keywords=keywords,
1222
            repeats=False,
1223
            location=location,
1224
            description=description,
1225
            subsections=subsections,
1226
            **kwargs,
1227
        )
1228

1229

1230
class Davidson(Section):
1✔
1231
    """Parameters for davidson diagonalization"""
1232

1233
    def __init__(
1✔
1234
        self,
1235
        new_prec_each: int = 20,
1236
        preconditioner: str = "FULL_SINGLE_INVERSE",
1237
        keywords: dict | None = None,
1238
        subsections: dict | None = None,
1239
        **kwargs,
1240
    ):
1241
        """
1242
        Args:
1243
            new_prec_each (int): How often to recalculate the preconditioner.
1244
            preconditioner (str): Preconditioner to use.
1245
                "FULL_ALL": Most effective state selective preconditioner based on diagonalization,
1246
                    requires the ENERGY_GAP parameter to be an underestimate of the HOMO-LUMO gap.
1247
                    This preconditioner is recommended for almost all systems, except very large
1248
                    systems where make_preconditioner would dominate the total computational cost.
1249
                "FULL_KINETIC": Cholesky inversion of S and T, fast construction, robust, use for
1250
                    very large systems.
1251
                "FULL_SINGLE": Based on H-eS diagonalisation, not as good as FULL_ALL, but
1252
                    somewhat cheaper to apply.
1253
                "FULL_SINGLE_INVERSE": Based on H-eS cholesky inversion, similar to FULL_SINGLE
1254
                    in preconditioning efficiency but cheaper to construct, might be somewhat
1255
                    less robust. Recommended for large systems.
1256
                "FULL_S_INVERSE": Cholesky inversion of S, not as good as FULL_KINETIC,
1257
                    yet equally expensive.
1258
                "NONE": skip preconditioning
1259
            keywords: additional keywords
1260
            subsections: additional subsections
1261
        """
1262
        self.new_prec_each = new_prec_each
×
1263
        self.preconditioner = preconditioner
×
1264
        keywords = keywords if keywords else {}
×
1265
        subsections = subsections if subsections else {}
×
1266
        _keywords = {
×
1267
            "NEW_PREC_EACH": Keyword("NEW_PREC_EACH", new_prec_each),
1268
            "PRECONDITIONER": Keyword("PRECONDITIONER", preconditioner),
1269
        }
1270
        keywords.update(_keywords)
×
1271
        super().__init__(
×
1272
            "DAVIDSON",
1273
            keywords=keywords,
1274
            repeats=False,
1275
            location=None,
1276
            subsections=subsections,
1277
            **kwargs,
1278
        )
1279

1280

1281
class OrbitalTransformation(Section):
1✔
1282
    """
1283
    Turns on the Orbital Transformation scheme for diagonalizing the Hamiltonian. Often faster
1284
    and with guaranteed convergence compared to normal diagonalization, but requires the system
1285
    to have a band gap.
1286

1287
    NOTE: OT has poor convergence for metallic systems and cannot use SCF mixing or smearing.
1288
    Therefore, you should not use it for metals or systems with 'small' band gaps. In that
1289
    case, use normal diagonalization
1290
    """
1291

1292
    def __init__(
1✔
1293
        self,
1294
        minimizer: str = "CG",
1295
        preconditioner: str = "FULL_ALL",
1296
        algorithm: str = "STRICT",
1297
        rotation: bool = False,
1298
        occupation_preconditioner: bool = False,
1299
        energy_gap: float = -1,
1300
        linesearch: str = "2PNT",
1301
        keywords: dict | None = None,
1302
        subsections: dict | None = None,
1303
        **kwargs,
1304
    ):
1305
        """
1306
        Initialize the OT section
1307

1308
        Args:
1309
            minimizer: The minimizer to use with the OT method. Default is conjugate gradient
1310
                method, which is more robust, but more well-behaved systems should use DIIS, which
1311
                can be as much as 50% faster.
1312
            preconditioner: Preconditioner to use for OT, FULL_ALL tends to be most robust,
1313
                but is not always most efficient. For difficult systems, FULL_SINGLE_INVERSE can be
1314
                more robust, and is reasonably efficient with large systems. For huge, but well
1315
                behaved, systems, where construction of the preconditioner can take a very long
1316
                time, FULL_KINETIC can be a good choice.
1317
            algorithm: What algorithm to use for OT. 'Strict': Taylor or diagonalization
1318
                based algorithm. IRAC: Orbital Transformation based Iterative Refinement of the
1319
                Approximative Congruence transformation (OT/IR).
1320
            rotation: Introduce additional variables to allow subspace rotations (i.e fractional
1321
                occupations)
1322
            occupation_preconditioner: include the fractional occupation in the preconditioning
1323
            energy_gap: Guess for the band gap. For FULL_ALL, should be smaller than the
1324
                actual band gap, so simply using 0.01 is a robust value. Choosing a larger value
1325
                will help if you start with a bad initial guess though. For FULL_SINGLE_INVERSE,
1326
                energy_gap is treated as a lower bound. Values lower than 0.05 in this case can
1327
                lead to stability issues.
1328
            linesearch (str): From the manual: 1D line search algorithm to be used with the OT
1329
                minimizer, in increasing order of robustness and cost. MINIMIZER CG combined with
1330
                LINESEARCH GOLD should always find an electronic minimum. Whereas the 2PNT
1331
                minimizer is almost always OK, 3PNT might be needed for systems in which successive
1332
                OT CG steps do not decrease the total energy.
1333
            keywords: additional keywords
1334
            subsections: additional subsections
1335
        """
1336
        self.minimizer = minimizer
1✔
1337
        self.preconditioner = preconditioner
1✔
1338
        self.algorithm = algorithm
1✔
1339
        self.rotation = rotation
1✔
1340
        self.occupation_preconditioner = occupation_preconditioner
1✔
1341
        self.energy_gap = energy_gap
1✔
1342
        self.linesearch = linesearch
1✔
1343
        keywords = keywords if keywords else {}
1✔
1344
        subsections = subsections if subsections else {}
1✔
1345

1346
        description = (
1✔
1347
            "Sets the various options for the orbital transformation (OT) method. "
1348
            + "Default settings already provide an efficient, yet robust method. Most "
1349
            + "systems benefit from using the FULL_ALL preconditioner combined with a small "
1350
            + "value (0.001) of ENERGY_GAP. Well-behaved systems might benefit from using "
1351
            + "a DIIS minimizer. Advantages: It's fast, because no expensive diagonalization"
1352
            + "is performed. If preconditioned correctly, method guaranteed to find minimum. "
1353
            + "Disadvantages: Sensitive to preconditioning. A good preconditioner can be "
1354
            + "expensive. No smearing, or advanced SCF mixing possible: POOR convergence for "
1355
            + "metallic systems."
1356
        )
1357

1358
        _keywords = {
1✔
1359
            "MINIMIZER": Keyword("MINIMIZER", minimizer),
1360
            "PRECONDITIONER": Keyword("PRECONDITIONER", preconditioner),
1361
            "ENERGY_GAP": Keyword("ENERGY_GAP", energy_gap),
1362
            "ALGORITHM": Keyword("ALGORITHM", algorithm),
1363
            "LINESEARCH": Keyword("LINESEARCH", linesearch),
1364
            "ROTATION": Keyword("ROTATION", rotation),
1365
            "OCCUPATION_PRECONDITIONER": Keyword("OCCUPATION_PRECONDITIONER", occupation_preconditioner),
1366
        }
1367
        keywords.update(_keywords)
1✔
1368
        super().__init__(
1✔
1369
            "OT",
1370
            description=description,
1371
            keywords=keywords,
1372
            subsections=subsections,
1373
            **kwargs,
1374
        )
1375

1376

1377
class Cell(Section):
1✔
1378
    """Defines the simulation cell (lattice)"""
1379

1380
    def __init__(self, lattice: Lattice, keywords: dict | None = None, **kwargs):
1✔
1381
        """
1382
        Initialize the cell section.
1383

1384
        Args:
1385
            lattice: pymatgen lattice object
1386
            keywords: additional keywords
1387
        """
1388
        self.lattice = lattice
1✔
1389
        keywords = keywords if keywords else {}
1✔
1390
        description = "Lattice parameters and optional settings for creating a the CELL"
1✔
1391

1392
        _keywords = {
1✔
1393
            "A": Keyword("A", *lattice.matrix[0]),
1394
            "B": Keyword("B", *lattice.matrix[1]),
1395
            "C": Keyword("C", *lattice.matrix[2]),
1396
        }
1397
        keywords.update(_keywords)
1✔
1398
        super().__init__("CELL", description=description, keywords=keywords, subsections={}, **kwargs)
1✔
1399

1400

1401
class Kind(Section):
1✔
1402
    """Specifies the information for the different atom types being simulated."""
1403

1404
    def __init__(
1✔
1405
        self,
1406
        specie: str,
1407
        alias: str | None = None,
1408
        magnetization: float = 0.0,
1409
        basis_set: GaussianTypeOrbitalBasisSet | str | None = "GTH_BASIS",
1410
        potential: GthPotential | str | None = "GTH_POTENTIALS",
1411
        ghost: bool = False,
1412
        aux_basis: GaussianTypeOrbitalBasisSet | str | None = None,
1413
        keywords: dict | None = None,
1414
        subsections: dict | None = None,
1415
        **kwargs,
1416
    ):
1417
        """
1418
        Initialize a KIND section
1419

1420
        Args:
1421
            specie: Object representing the atom.
1422
            alias: Alias for the atom, can be used for specifying modifications
1423
                to certain atoms but not all, e.g. Mg_1 and Mg_2 to force difference
1424
                oxidation states on the two atoms.
1425
            magnetization: From the CP2K Manual: The magnetization used
1426
                in the atomic initial guess. Adds magnetization/2 spin-alpha
1427
                electrons and removes magnetization/2 spin-beta electrons.
1428
            basis_set: Basis set for this atom, accessible from the
1429
                basis set file specified
1430
            potential: Pseudopotential for this atom, accessible from the
1431
                potential file
1432
            ghost: Turn this into ghost atom (disaple the potential)
1433
            aux_basis: Auxiliary basis to use with ADMM
1434
            keywords: additional keywords
1435
            subsections: additional subsections
1436
            kwargs: Additional kwargs to pass to Section()
1437
        """
1438
        self.name = "KIND"
1✔
1439
        self.specie = specie
1✔
1440
        self.alias = alias
1✔
1441
        self.magnetization = magnetization
1✔
1442
        self.basis_set = basis_set
1✔
1443
        self.potential = potential
1✔
1444
        self.ghost = ghost
1✔
1445
        self.aux_basis = aux_basis
1✔
1446
        keywords = keywords if keywords else {}
1✔
1447
        subsections = subsections if subsections else {}
1✔
1448
        description = "The description of this kind of atom including basis sets, element, etc."
1✔
1449

1450
        # Special case for closed-shell elements. Cannot impose magnetization in cp2k.
1451
        if Element(self.specie).Z in {
1✔
1452
            2,
1453
            4,
1454
            10,
1455
            12,
1456
            18,
1457
            20,
1458
            30,
1459
            36,
1460
            38,
1461
            48,
1462
            54,
1463
            56,
1464
            70,
1465
            80,
1466
            86,
1467
            88,
1468
            102,
1469
            112,
1470
            118,
1471
        }:
1472
            self.magnetization = 0
×
1473

1474
        _keywords = {
1✔
1475
            "ELEMENT": Keyword("ELEMENT", specie.__str__()),
1476
            "MAGNETIZATION": Keyword("MAGNETIZATION", magnetization),
1477
            "GHOST": Keyword("GHOST", ghost),
1478
        }
1479
        if basis_set:
1✔
1480
            _keywords["BASIS_SET"] = (
1✔
1481
                Keyword("BASIS_SET", basis_set) if isinstance(basis_set, str) else basis_set.get_keyword()
1482
            )
1483
        if potential:
1✔
1484
            _keywords["POTENTIAL"] = (
1✔
1485
                Keyword("POTENTIAL", potential) if isinstance(potential, str) else potential.get_keyword()
1486
            )
1487
        if aux_basis:
1✔
1488
            _keywords["BASIS_SET"] += (
1✔
1489
                Keyword("BASIS_SET", f"BASIS_SET AUX_FIT {aux_basis}")
1490
                if isinstance(aux_basis, str)
1491
                else aux_basis.get_keyword()
1492
            )
1493

1494
        kind_name = alias if alias else specie.__str__()
1✔
1495
        alias = kind_name
1✔
1496

1497
        section_parameters = [kind_name]
1✔
1498
        location = "FORCE_EVAL/SUBSYS/KIND"
1✔
1499
        keywords.update(_keywords)
1✔
1500
        super().__init__(
1✔
1501
            name=self.name,
1502
            subsections=subsections,
1503
            description=description,
1504
            keywords=keywords,
1505
            section_parameters=section_parameters,
1506
            alias=alias,
1507
            location=location,
1508
            verbose=True,
1509
            **kwargs,
1510
        )
1511

1512

1513
class DftPlusU(Section):
1✔
1514
    """Controls DFT+U for an atom kind"""
1515

1516
    def __init__(
1✔
1517
        self,
1518
        eps_u_ramping=1e-5,
1519
        init_u_ramping_each_scf=False,
1520
        l=-1,
1521
        u_minus_j=0,
1522
        u_ramping=0,
1523
        keywords: dict | None = None,
1524
        subsections: dict | None = None,
1525
        **kwargs,
1526
    ):
1527
        """
1528
        Initialize the DftPlusU section.
1529

1530
        Args:
1531
            eps_u_ramping: (float) SCF convergence threshold at which to start ramping the U value
1532
            init_u_ramping_each_scf: (bool) Whether or not to do u_ramping each scf cycle
1533
            l: (int) angular moment of the orbital to apply the +U correction
1534
            u_minus_j: (float) the effective U parameter, Ueff = U-J
1535
            u_ramping: (float) stepwise amount to increase during ramping until u_minus_j is reached
1536
            keywords: additional keywords
1537
            subsections: additional subsections
1538
        """
1539
        name = "DFT_PLUS_U"
×
1540
        self.eps_u_ramping = 1e-5
×
1541
        self.init_u_ramping_each_scf = False
×
1542
        self.l = l
×
1543
        self.u_minus_j = u_minus_j
×
1544
        self.u_ramping = u_ramping
×
1545
        keywords = keywords if keywords else {}
×
1546
        subsections = subsections if subsections else {}
×
1547
        description = "Settings for on-site Hubbard +U correction for this atom kind."
×
1548

1549
        _keywords = {
×
1550
            "EPS_U_RAMPING": Keyword("EPS_U_RAMPING", eps_u_ramping),
1551
            "INIT_U_RAMPING_EACH_SCF": Keyword("INIT_U_RAMPING_EACH_SCF", init_u_ramping_each_scf),
1552
            "L": Keyword("L", l),
1553
            "U_MINUS_J": Keyword("U_MINUS_J", u_minus_j),
1554
            "U_RAMPING": Keyword("U_RAMPING", u_ramping),
1555
        }
1556
        keywords.update(_keywords)
×
1557
        super().__init__(name=name, subsections=None, description=description, keywords=keywords, **kwargs)
×
1558

1559

1560
class Coord(Section):
1✔
1561
    """Specifies the coordinates of the atoms using a pymatgen structure object."""
1562

1563
    def __init__(
1✔
1564
        self,
1565
        structure: Structure | Molecule,
1566
        aliases: dict | None = None,
1567
        keywords: dict | None = None,
1568
        subsections: dict | None = None,
1569
        **kwargs,
1570
    ):
1571
        """
1572
        Args:
1573
            structure: Pymatgen structure object
1574
            alias (bool): whether or not to identify the sites by Element + number so you can do
1575
                things like assign unique magnetization do different elements.
1576
            keywords: additional keywords
1577
            subsections: additional subsections
1578
        """
1579
        self.structure = structure
1✔
1580
        self.aliases = aliases
1✔
1581
        keywords = keywords if keywords else {}
1✔
1582
        subsections = subsections if subsections else {}
1✔
1583
        description = (
1✔
1584
            "The coordinates for simple systems (like small QM cells) are specified "
1585
            + "here by default using explicit XYZ coordinates. More complex systems "
1586
            + "should be given via an external coordinate file in the SUBSYS%TOPOLOGY section."
1587
        )
1588

1589
        if aliases:
1✔
1590
            aliases = {index: kind for kind, indices in aliases.items() for index in indices}
1✔
1591

1592
        keywords = {
1✔
1593
            i: Keyword(aliases[i] if aliases else structure[i].species_string, *structure[i].coords)
1594
            for i in range(len(structure))
1595
        }
1596
        super().__init__(
1✔
1597
            name="COORD",
1598
            description=description,
1599
            keywords=keywords,
1600
            subsections=subsections,
1601
            **kwargs,
1602
        )
1603

1604

1605
class DOS(Section):
1✔
1606
    """Controls printing of the density of states."""
1607

1608
    def __init__(self, ndigits: int = 6, keywords: dict | None = None, subsections: dict | None = None, **kwargs):
1✔
1609
        """
1610
        Initialize the DOS section
1611

1612
        Args:
1613
            ndigits: how many digits of precision to print. As of 2022.1,
1614
                this is necessary to not lose information.
1615
            keywords: additional keywords
1616
            subsections: additional subsections
1617
        """
1618
        self.ndigits = ndigits
×
1619
        keywords = keywords if keywords else {}
×
1620
        subsections = subsections if subsections else {}
×
1621
        description = "Controls printing of the overall density of states"
×
1622
        _keywords = {"NDIGITS": Keyword("NDIGITS", ndigits)}
×
1623
        keywords.update(_keywords)
×
1624
        super().__init__("DOS", description=description, keywords=keywords, subsections=subsections, **kwargs)
×
1625

1626

1627
class PDOS(Section):
1✔
1628
    """
1629
    Controls printing of projected density of states onto the different atom KINDS
1630
    (elemental decomposed DOS).
1631
    """
1632

1633
    def __init__(self, nlumo: int = -1, keywords: dict | None = None, subsections: dict | None = None, **kwargs):
1✔
1634
        """
1635
        Initialize the PDOS section
1636

1637
        Args:
1638
            nlumo: how many unoccupied orbitals to include (-1==ALL)
1639
            keywords: additional keywords
1640
            subsections: additional subsections
1641
        """
1642
        self.nlumo = nlumo
1✔
1643
        keywords = keywords if keywords else {}
1✔
1644
        subsections = subsections if subsections else {}
1✔
1645
        description = "Controls printing of the projected density of states"
1✔
1646

1647
        _keywords = {
1✔
1648
            "NLUMO": Keyword("NLUMO", nlumo),
1649
            "COMPONENTS": Keyword("COMPONENTS"),
1650
        }
1651
        keywords.update(_keywords)
1✔
1652
        super().__init__("PDOS", description=description, keywords=keywords, subsections=subsections, **kwargs)
1✔
1653

1654

1655
class LDOS(Section):
1✔
1656
    """
1657
    Controls printing of the LDOS (List-Density of states). i.e. projects onto specific atoms.
1658
    """
1659

1660
    def __init__(
1✔
1661
        self,
1662
        index: int = 1,
1663
        alias: str | None = None,
1664
        keywords: dict | None = None,
1665
        subsections: dict | None = None,
1666
        **kwargs,
1667
    ):
1668
        """
1669
        Initialize the LDOS section
1670

1671
        Args:
1672
            index: Index of the atom to project onto
1673
            alias: section alias
1674
            keywords: additional keywords
1675
            subsections: additional subsections
1676
        """
1677
        self.index = index
×
1678
        keywords = keywords if keywords else {}
×
1679
        subsections = subsections if subsections else {}
×
1680
        description = "Controls printing of the projected density of states decomposed by atom type"
×
1681
        _keywords = {"COMPONENTS": Keyword("COMPONENTS"), "LIST": Keyword("LIST", index)}
×
1682
        keywords.update(_keywords)
×
1683
        super().__init__(
×
1684
            "LDOS",
1685
            subsections=subsections,
1686
            alias=alias,
1687
            description=description,
1688
            keywords=keywords,
1689
            **kwargs,
1690
        )
1691

1692

1693
class V_Hartree_Cube(Section):
1✔
1694
    """Controls printing of the hartree potential as a cube file."""
1695

1696
    def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs):
1✔
1697
        keywords = keywords if keywords else {}
1✔
1698
        subsections = subsections if subsections else {}
1✔
1699
        description = (
1✔
1700
            "Controls the printing of a cube file with eletrostatic potential generated by "
1701
            + "the total density (electrons+ions). It is valid only for QS with GPW formalism. "
1702
            + "Note: by convention the potential has opposite sign than the expected physical one."
1703
        )
1704
        super().__init__(
1✔
1705
            "V_HARTREE_CUBE",
1706
            subsections=subsections,
1707
            description=description,
1708
            keywords=keywords,
1709
            **kwargs,
1710
        )
1711

1712

1713
class MO_Cubes(Section):
1✔
1714
    """Controls printing of the molecular orbital eigenvalues"""
1715

1716
    def __init__(
1✔
1717
        self,
1718
        write_cube: bool = False,
1719
        nhomo: int = 1,
1720
        nlumo: int = 1,
1721
        keywords: dict | None = None,
1722
        subsections: dict | None = None,
1723
        **kwargs,
1724
    ):
1725
        """
1726
        Initialize the MO_CUBES section
1727
        """
1728
        self.write_cube = write_cube
1✔
1729
        self.nhomo = nhomo
1✔
1730
        self.nlumo = nlumo
1✔
1731
        keywords = keywords if keywords else {}
1✔
1732
        subsections = subsections if subsections else {}
1✔
1733
        description = (
1✔
1734
            "Controls the printing of a cube file with eletrostatic potential generated by "
1735
            + "the total density (electrons+ions). It is valid only for QS with GPW formalism. "
1736
            + "Note: by convention the potential has opposite sign than the expected physical one."
1737
        )
1738

1739
        _keywords = {
1✔
1740
            "WRITE_CUBES": Keyword("WRITE_CUBE", write_cube),
1741
            "NHOMO": Keyword("NHOMO", nhomo),
1742
            "NLUMO": Keyword("NLUMO", nlumo),
1743
        }
1744
        keywords.update(_keywords)
1✔
1745
        super().__init__(
1✔
1746
            "MO_CUBES",
1747
            subsections={},
1748
            description=description,
1749
            keywords=keywords,
1750
            **kwargs,
1751
        )
1752

1753

1754
class E_Density_Cube(Section):
1✔
1755
    """Controls printing of the electron density cube file"""
1756

1757
    def __init__(self, keywords: dict | None = None, subsections: dict | None = None, **kwargs):
1✔
1758
        keywords = keywords if keywords else {}
1✔
1759
        subsections = subsections if subsections else {}
1✔
1760
        description = (
1✔
1761
            "Controls the printing of cube files with the electronic density and, for LSD "
1762
            + "calculations, the spin density."
1763
        )
1764

1765
        super().__init__(
1✔
1766
            "E_DENSITY_CUBE",
1767
            subsections=subsections,
1768
            description=description,
1769
            keywords=keywords,
1770
            **kwargs,
1771
        )
1772

1773

1774
class Smear(Section):
1✔
1775
    """Control electron smearing"""
1776

1777
    def __init__(
1✔
1778
        self,
1779
        elec_temp: int | float = 300,
1780
        method: str = "FERMI_DIRAC",
1781
        fixed_magnetic_moment: float = -1e2,
1782
        keywords: dict | None = None,
1783
        subsections: dict | None = None,
1784
        **kwargs,
1785
    ):
1786
        self.elec_temp = elec_temp
×
1787
        self.method = method
×
1788
        self.fixed_magnetic_moment = fixed_magnetic_moment
×
1789
        keywords = keywords if keywords else {}
×
1790
        subsections = subsections if subsections else {}
×
1791
        description = "Activates smearing of electron occupations"
×
1792

1793
        _keywords = {
×
1794
            "ELEC_TEMP": Keyword("ELEC_TEMP", elec_temp),
1795
            "METHOD": Keyword("METHOD", method),
1796
            "FIXED_MAGNETIC_MOMENT": Keyword("FIXED_MAGNETIC_MOMENT", fixed_magnetic_moment),
1797
        }
1798
        keywords.update(_keywords)
×
1799
        super().__init__(
×
1800
            "SMEAR",
1801
            description=description,
1802
            keywords=keywords,
1803
            subsections=subsections,
1804
            **kwargs,
1805
        )
1806

1807

1808
class BrokenSymmetry(Section):
1✔
1809
    """
1810
    Define the required atomic orbital occupation assigned in initialization
1811
    of the density matrix, by adding or subtracting electrons from specific
1812
    angular momentum channels. It works only with GUESS ATOMIC.
1813
    """
1814

1815
    def __init__(
1✔
1816
        self,
1817
        l_alpha: Sequence = (-1,),
1818
        n_alpha: Sequence = (0,),
1819
        nel_alpha: Sequence = (-1,),
1820
        l_beta: Sequence = (-1,),
1821
        n_beta: Sequence = (0,),
1822
        nel_beta: Sequence = (-1,),
1823
    ):
1824
        """
1825
        Initialize the broken symmetry section
1826

1827
        Args:
1828
            l_alpha: Angular momentum quantum number of the orbitals whose occupation is changed
1829
            n_alpha: Principal quantum number of the orbitals whose occupation is changed.
1830
                Default is the first not occupied
1831
            nel_alpha: Orbital occupation change per angular momentum quantum number. In
1832
                unrestricted calculations applied to spin alpha
1833
            l_beta: Same as L_alpha for beta channel
1834
            n_beta: Same as N_alpha for beta channel
1835
            nel_beta: Same as NEL_alpha for beta channel
1836
        """
1837
        self.l_alpha = l_alpha
×
1838
        self.n_alpha = n_alpha
×
1839
        self.nel_alpha = nel_alpha
×
1840
        self.l_beta = l_beta
×
1841
        self.n_beta = n_beta
×
1842
        self.nel_beta = nel_beta
×
1843
        description = (
×
1844
            "Define the required atomic orbital occupation assigned in initialization "
1845
            + "of the density matrix, by adding or subtracting electrons from specific "
1846
            + "angular momentum channels. It works only with GUESS ATOMIC"
1847
        )
1848

1849
        keywords_alpha = {
×
1850
            "L": Keyword("L", *map(int, l_alpha)),
1851
            "N": Keyword("N", *map(int, n_alpha)),
1852
            "NEL": Keyword("NEL", *map(int, nel_alpha)),
1853
        }
1854
        alpha = Section("ALPHA", keywords=keywords_alpha, subsections={}, repeats=False)
×
1855

1856
        keywords_beta = {
×
1857
            "L": Keyword("L", *map(int, l_beta)),
1858
            "N": Keyword("N", *map(int, n_beta)),
1859
            "NEL": Keyword("NEL", *map(int, nel_beta)),
1860
        }
1861
        beta = Section("BETA", keywords=keywords_beta, subsections={}, repeats=False)
×
1862

1863
        super().__init__(
×
1864
            "BS",
1865
            description=description,
1866
            subsections={"ALPHA": alpha, "BETA": beta},
1867
            keywords={},
1868
            repeats=False,
1869
        )
1870

1871
    @classmethod
1✔
1872
    def from_el(cls, el, oxi_state=0, spin=0):
1✔
1873
        """
1874
        Create section from element, oxidation state, and spin.
1875
        """
1876
        el = el if isinstance(el, Element) else Element(el)
×
1877

1878
        def f(x):
×
1879
            return {"s": 0, "p": 1, "d": 2, "f": 4}.get(x)
×
1880

1881
        def f2(x):
×
1882
            return {0: 2, 1: 6, 2: 10, 3: 14}.get(x)
×
1883

1884
        def f3(x):
×
1885
            return {0: 2, 1: 6, 2: 10, 3: 14}.get(x)
×
1886

1887
        es = el.electronic_structure
×
1888
        esv = [(int(_[0]), f(_[1]), int(_[2:])) for _ in es.split(".") if "[" not in _]
×
1889
        esv.sort(key=lambda x: (x[0], x[1]), reverse=True)
×
1890

1891
        tmp = oxi_state
×
1892
        l_alpha = []
×
1893
        l_beta = []
×
1894
        nel_alpha = []
×
1895
        nel_beta = []
×
1896
        n_alpha = []
×
1897
        n_beta = []
×
1898
        unpaired_orbital = None
×
1899
        while tmp:
×
1900
            if tmp > 0:
×
1901
                tmp2 = -min((esv[0][2], tmp))
×
1902
            else:
1903
                tmp2 = min((f2(esv[0][1]) - esv[0][2], -tmp))
×
1904
            l_alpha.append(esv[0][1])
×
1905
            l_beta.append(esv[0][1])
×
1906
            nel_alpha.append(tmp2)
×
1907
            nel_beta.append(tmp2)
×
1908
            n_alpha.append(esv[0][0])
×
1909
            n_beta.append(esv[0][0])
×
1910
            tmp += tmp2
×
1911
            unpaired_orbital = esv[0][0], esv[0][1], esv[0][2] + tmp2
×
1912
            esv.pop(0)
×
1913

1914
        if spin == "low-up":
×
1915
            spin = unpaired_orbital[2] % 2
×
1916
        elif spin == "low-down":
×
1917
            spin = -(unpaired_orbital[2] % 2)
×
1918
        elif spin == "high-up":
×
1919
            spin = unpaired_orbital[2] % (f2(unpaired_orbital[1]) // 2)
×
1920
        elif spin == "high-down":
×
1921
            spin = -(unpaired_orbital[2] % (f2(unpaired_orbital[1]) // 2))
×
1922

1923
        if spin:
×
1924
            for i in reversed(range(len(nel_alpha))):
×
1925
                nel_alpha[i] += min((spin, f3(l_alpha[i]) - oxi_state))
×
1926
                nel_beta[i] -= min((spin, f3(l_beta[i]) - oxi_state))
×
1927
                if spin > 0:
×
1928
                    spin -= min((spin, f3(l_alpha[i]) - oxi_state))
×
1929
                else:
1930
                    spin += min((spin, f3(l_beta[i]) - oxi_state))
×
1931

1932
        return BrokenSymmetry(
×
1933
            l_alpha=l_alpha,
1934
            l_beta=l_beta,
1935
            nel_alpha=nel_alpha,
1936
            nel_beta=nel_beta,
1937
            n_beta=n_beta,
1938
            n_alpha=n_alpha,
1939
        )
1940

1941

1942
class Xc_Functional(Section):
1✔
1943
    """Defines the XC functional(s) to use."""
1944

1945
    def __init__(
1✔
1946
        self,
1947
        functionals: Iterable | None = None,
1948
        keywords: dict | None = None,
1949
        subsections: dict | None = None,
1950
        **kwargs,
1951
    ):
1952
        self.functionals = functionals if functionals else []
1✔
1953
        keywords = keywords if keywords else {}
1✔
1954
        subsections = subsections if subsections else {}
1✔
1955
        location = "CP2K_INPUT/FORCE_EVAL/DFT/XC/XC_FUNCTIONAL"
1✔
1956

1957
        for functional in self.functionals:
1✔
1958
            subsections[functional] = Section(functional, subsections={}, repeats=False)
1✔
1959

1960
        super().__init__(
1✔
1961
            "XC_FUNCTIONAL",
1962
            subsections=subsections,
1963
            keywords=keywords,
1964
            location=location,
1965
            repeats=False,
1966
            **kwargs,
1967
        )
1968

1969

1970
class PBE(Section):
1✔
1971
    """Info about the PBE functional."""
1972

1973
    def __init__(
1✔
1974
        self,
1975
        parameterization: str = "ORIG",
1976
        scale_c: float | int = 1,
1977
        scale_x: float | int = 1,
1978
        keywords: dict | None = None,
1979
        subsections: dict | None = None,
1980
    ):
1981
        """
1982
        Args:
1983
            parameterization (str):
1984
                ORIG: original PBE
1985
                PBESOL: PBE for solids/surfaces
1986
                REVPBE: revised PBE
1987
            scale_c (float): scales the correlation part of the functional.
1988
            scale_x (float): scales the exchange part of the functional.
1989
            keywords: additional keywords
1990
            subsections: additional subsections
1991
        """
1992
        self.parameterization = parameterization
1✔
1993
        self.scale_c = scale_c
1✔
1994
        self.scale_x = scale_x
1✔
1995
        keywords = keywords if keywords else {}
1✔
1996
        subsections = subsections if subsections else {}
1✔
1997

1998
        location = "CP2K_INPUT/FORCE_EVAL/DFT/XC/XC_FUNCTIONAL/PBE"
1✔
1999

2000
        _keywords = {
1✔
2001
            "PARAMETRIZATION": Keyword("PARAMETRIZATION", parameterization),
2002
            "SCALE_C": Keyword("SCALE_C", scale_c),
2003
            "SCALE_X": Keyword("SCALE_X", scale_x),
2004
        }
2005
        keywords.update(_keywords)
1✔
2006
        super().__init__(
1✔
2007
            "PBE",
2008
            subsections=subsections,
2009
            repeats=False,
2010
            location=location,
2011
            section_parameters=[],
2012
            keywords=keywords,
2013
        )
2014

2015

2016
class Kpoints(Section):
1✔
2017
    """Description of the k-points to use for the calculation."""
2018

2019
    def __init__(
1✔
2020
        self,
2021
        kpts: Sequence | Sequence[Sequence[int]],
2022
        weights: Sequence | None = None,
2023
        eps_geo: float = 1e-6,
2024
        full_grid: bool = False,
2025
        parallel_group_size: int = -1,
2026
        scheme: str = "MONKHORST-PACK",
2027
        symmetry: bool = False,
2028
        units: str = "B_VECTOR",
2029
        verbose: bool = False,
2030
        wavefunctions: str = "COMPLEX",
2031
    ):
2032
        """
2033
        Args:
2034
            kpts (list, tuple): a 2D array for the kpoints of the form
2035
                [(1,1,1),]. If len(kpts) == 1. Then it is taken as subdivisions
2036
                for automatic kpoint scheme. If it has more entries, it is
2037
                taken as manual entries for kpoints.
2038
            weights (list, tuple): a weight for each kpoint. Default is to
2039
                weigh each by 1
2040
            eps_geo (float): tolerance for symmetry. Default=1e-6
2041
            full_grid (bool): use full (not reduced) kpoint grid. Default=False.
2042
            parallel_group_size (int): from cp2k manual: Number of processors
2043
                to be used for a single kpoint. This number must divide the
2044
                total number of processes. The number of groups must divide
2045
                the total number of kpoints. Value=-1 (smallest possible
2046
                number of processes per group, satisfying the constraints).
2047
                Value=0 (all processes). Value=n (exactly n processes).
2048
                Default=-1.
2049
            scheme (str): kpoint generation scheme. Default='Monkhorst-Pack'
2050
            symmetry (bool): Use symmetry to reduce the number of kpoints.
2051
                Default=False.
2052
            units (str): Units for the kpoint coordinates (reciprocal coordinates
2053
                or cartesian). Default='B_VECTOR' (reciprocal)
2054
            verbose (bool): verbose output for kpoints. Default=False
2055
            wavefunctions (str): Whether to use complex or real valued wavefunctions
2056
                (if available). Default='complex'
2057
        """
2058
        description = "Sets up the kpoints"
×
2059
        keywords = {}
×
2060

2061
        self.kpts = kpts
×
2062
        self.weights = weights if weights else [1] * len(kpts)
×
2063
        assert len(self.kpts) == len(self.weights)
×
2064
        self.eps_geo = eps_geo
×
2065
        self.full_grid = full_grid
×
2066
        self.parallel_group_size = parallel_group_size
×
2067
        self.scheme = scheme
×
2068
        self.symmetry = symmetry
×
2069
        self.units = units
×
2070
        self.verbose = verbose
×
2071
        self.wavefunctions = wavefunctions
×
2072

2073
        if len(kpts) == 1:
×
2074
            keywords["SCHEME"] = Keyword("SCHEME", scheme, *kpts[0])
×
2075
        elif len(kpts) > 1:
×
2076
            keywords["KPOINT"] = KeywordList([Keyword("KPOINT", *k, w) for k, w in zip(self.kpts, self.weights)])
×
2077
        else:
2078
            raise ValueError("No k-points provided!")
×
2079

2080
        keywords.update(
×
2081
            {
2082
                "SCHEME": Keyword("SCHEME", scheme),
2083
                "EPS_GEO": Keyword("EPS_GEO", eps_geo),
2084
                "FULL_GRID": Keyword("FULL_GRID", full_grid),
2085
                "PARALLEL_GROUP_SIZE": Keyword("PARALLEL_GROUP_SIZE", parallel_group_size),
2086
                "SYMMETRY": Keyword("SYMMETRY", symmetry),
2087
                "UNITS": Keyword("UNITS", units),
2088
                "VERBOSE": Keyword("VERBOSE", verbose),
2089
                "WAVEFUNCTIONS": Keyword("WAVEFUNCTIONS", wavefunctions),
2090
            }
2091
        )
2092

2093
        super().__init__(
×
2094
            name="KPOINTS",
2095
            subsections=None,
2096
            repeats=False,
2097
            description=description,
2098
            keywords=keywords,
2099
        )
2100

2101
    @classmethod
1✔
2102
    def from_kpoints(cls, kpoints: VaspKpoints, structure=None):
1✔
2103
        """
2104
        Initialize the section from a Kpoints object (pymatgen.io.vasp.inputs). CP2K
2105
        does not have an automatic gamma-point constructor, so this is generally used
2106
        to get the number of divisions from a kpoint static constructor and then
2107
        build a Monkhorst-Pack grid, which is sufficient for gamma-recommended systems
2108
        so long as the grid is fine enough.
2109

2110
        Args:
2111
            kpoints: A pymatgen kpoints object.
2112
            structure: Pymatgen structure object. Required for automatically performing
2113
                symmetry analysis and reducing the kpoint grid.
2114
            reduce: whether or not to reduce the grid using symmetry. CP2K itself cannot
2115
                do this automatically without spglib present at execution time.
2116
        """
2117
        kpts = kpoints.kpts
×
2118
        weights = kpoints.kpts_weights
×
2119

2120
        if kpoints.style == Kpoints_supported_modes.Monkhorst:
×
2121
            k = kpts[0]
×
2122
            if isinstance(k, (int, float)):
×
2123
                x, y, z = k, k, k
×
2124
            else:
2125
                x, y, z = k
×
2126
            scheme = f"MONKHORST-PACK {x} {y} {z}"
×
2127
            units = "B_VECTOR"
×
2128
        elif kpoints.style == Kpoints_supported_modes.Reciprocal:
×
2129
            units = "B_VECTOR"
×
2130
            scheme = "GENERAL"
×
2131
        elif kpoints.style == Kpoints_supported_modes.Cartesian:
×
2132
            units = "CART_ANGSTROM"
×
2133
            scheme = "GENERAL"
×
2134
        elif kpoints.style == Kpoints_supported_modes.Gamma:
×
2135
            if (isinstance(kpts[0], Iterable) and tuple(kpts[0]) == (1, 1, 1)) or (
×
2136
                isinstance(kpts[0], (float, int)) and int(kpts[0]) == 1
2137
            ):
2138
                scheme = "GAMMA"
×
2139
                units = "B_VECTOR"
×
2140
            elif not structure:
×
2141
                raise ValueError(
×
2142
                    "No cp2k automatic gamma constructor. " "A structure is required to construct from spglib"
2143
                )
2144
            else:
2145
                sga = SpacegroupAnalyzer(structure)
×
2146
                _kpts, weights = zip(*sga.get_ir_reciprocal_mesh(mesh=kpts))
×
2147
                kpts = list(itertools.chain.from_iterable(_kpts))
×
2148
                scheme = "GENERAL"
×
2149
                units = "B_VECTOR"
×
2150
        elif kpoints.style == Kpoints_supported_modes.Line_mode:
×
2151
            scheme = "GENERAL"
×
2152
            units = "B_VECTOR"
×
2153
        else:
2154
            raise ValueError("Unrecognized k-point style")
×
2155
        return Kpoints(kpts=kpts, weights=weights, scheme=scheme, units=units)
×
2156

2157

2158
class Kpoint_Set(Section):
1✔
2159
    """Specifies a kpoint line to be calculated between special points."""
2160

2161
    def __init__(self, npoints: int, kpoints: Iterable, units: str = "B_VECTOR") -> None:
1✔
2162
        """
2163
        Args:
2164
            npoints (int): Number of kpoints along the line.
2165
            kpoints: A dictionary of {label: kpoint} kpoints defining the path
2166
            units (str): Units for the kpoint coordinates.
2167
                Options: "B_VECTOR" (reciprocal coordinates)
2168
                         "CART_ANGSTROM" (units of 2*Pi/Angstrom)
2169
                         "CART_BOHR" (units of 2*Pi/Bohr)
2170
        """
2171
        self.npoints = npoints
×
2172
        self.kpoints = kpoints
×
2173
        self.units = units
×
2174

2175
        keywords = {
×
2176
            "NPOINTS": Keyword("NPOINTS", npoints),
2177
            "UNITS": Keyword("UNITS", units),
2178
            "SPECIAL_POINT": KeywordList(
2179
                [
2180
                    Keyword("SPECIAL_POINT", "Gamma" if label.upper() == "\\GAMMA" else label, *kpt)
2181
                    for label, kpt in kpoints
2182
                ]
2183
            ),
2184
        }
2185

2186
        super().__init__(
×
2187
            name="KPOINT_SET",
2188
            subsections=None,
2189
            repeats=True,
2190
            description="Specifies a single k-point line for band structure calculations",
2191
            keywords=keywords,
2192
        )
2193

2194

2195
class Band_Structure(Section):
1✔
2196
    """Specifies high symmetry paths for outputting the band structure in CP2K."""
2197

2198
    def __init__(
1✔
2199
        self,
2200
        kpoint_sets: Sequence[Kpoint_Set],
2201
        filename: str = "BAND.bs",
2202
        added_mos: int = -1,
2203
        keywords: dict | None = None,
2204
        subsections: dict | None = None,
2205
    ):
2206
        """
2207
        Args:
2208
            kpoint_sets: Sequence of Kpoint_Set objects for the band structure calculation.
2209
            filename: Filename for the band structure output
2210
            added_mos: Added (unoccupied) molecular orbitals for the calculation.
2211
            keywords: additional keywords
2212
            subsections: additional subsections
2213
        """
2214
        self.kpoint_sets = SectionList(kpoint_sets)
×
2215
        self.filename = filename
×
2216
        self.added_mos = added_mos
×
2217
        keywords = keywords if keywords else {}
×
2218
        _keywords = {
×
2219
            "FILE_NAME": Keyword("FILE_NAME", filename),
2220
            "ADDED_MOS": Keyword("ADDED_MOS", added_mos),
2221
        }
2222
        keywords.update(_keywords)
×
2223
        super().__init__(
×
2224
            name="BAND_STRUCTURE",
2225
            subsections={"KPOINT_SET": self.kpoint_sets},
2226
            repeats=False,
2227
            description="Controls printing of band structure calculation",
2228
            keywords=keywords,
2229
        )
2230

2231
    # TODO kpoints objects are defined in the vasp module instead of a code agnostic module
2232
    # if this changes in the future as other codes are added, then this will need to change
2233
    @staticmethod
1✔
2234
    def from_kpoints(kpoints: VaspKpoints, kpoints_line_density=20):
1✔
2235
        """
2236
        Initialize band structure section from a line-mode Kpoint object
2237

2238
        Args:
2239
            kpoints: a kpoint object from the vasp module, which was constructed in line mode
2240
            kpoints_line_density: Number of kpoints along each path
2241
        """
2242
        if kpoints.style == Kpoints_supported_modes.Line_mode:
×
2243

2244
            def pairwise(iterable):
×
2245
                a = iter(iterable)
×
2246
                return zip(a, a)
×
2247

2248
            kpoint_sets = [
×
2249
                Kpoint_Set(
2250
                    npoints=kpoints_line_density,
2251
                    kpoints=[(lbls[0], kpts[0]), (lbls[1], kpts[1])],
2252
                    units="B_VECTOR",
2253
                )
2254
                for lbls, kpts in zip(pairwise(kpoints.labels), pairwise(kpoints.kpts))
2255
            ]
2256
        elif kpoints.style in (
×
2257
            Kpoints_supported_modes.Reciprocal,
2258
            Kpoints_supported_modes.Cartesian,
2259
        ):
2260
            kpoint_sets = [
×
2261
                Kpoint_Set(
2262
                    npoints=1,
2263
                    kpoints=[("None", kpts) for kpts in kpoints.kpts],
2264
                    units="B_VECTOR" if kpoints.coord_type == "Reciprocal" else "CART_ANGSTROM",
2265
                )
2266
            ]
2267
        else:
2268
            raise ValueError(
×
2269
                "Unsupported k-point style. Must be line-mode or explicit k-points (reciprocal/cartesian)."
2270
            )
2271
        return Band_Structure(kpoint_sets=kpoint_sets, filename="BAND.bs")
×
2272

2273

2274
@dataclass
1✔
2275
class BasisInfo(MSONable):
1✔
2276
    """
2277
    Summary info about a basis set
2278

2279
    Attributes:
2280
        electrons: Number of electrons
2281
        core: Number of basis functions per core electron
2282
        valence: Number of basis functions per valence electron OR number of exp if it
2283
            is a FIT formatted admm basis
2284
        polarization: Number of polarization functions
2285
        diffuse: Number of added, diffuse/augmentation functions
2286
        cc: Correlation consistent
2287
        pc: Polarization consistent
2288
        sr: Short-range optimized
2289
        molopt: Optimized for molecules/solids
2290
        admm: Whether this is an auxiliary basis set for ADMM
2291
        lri: Whether this is a local resolution of identity auxiliary basis
2292
        contracted: Whether this basis set is contracted
2293
        xc: Exchange correlation functional used for creating this potential
2294
    """
2295

2296
    electrons: int | None = None
1✔
2297
    core: int | None = None
1✔
2298
    valence: int | None = None
1✔
2299
    polarization: int | None = None
1✔
2300
    diffuse: int | None = None
1✔
2301
    cc: bool | None = False
1✔
2302
    pc: bool | None = False
1✔
2303
    sr: bool | None = False
1✔
2304
    molopt: bool | None = False
1✔
2305
    admm: bool | None = False
1✔
2306
    lri: bool | None = False
1✔
2307
    contracted: bool | None = None
1✔
2308
    xc: str | None = None
1✔
2309

2310
    def softmatch(self, other):
1✔
2311
        """
2312
        Soft matching to see if two basis sets match.
2313

2314
        Will only match those attributes which *are* defined for this basis info object (one way checking)
2315
        """
2316
        if not isinstance(other, BasisInfo):
1✔
2317
            return False
×
2318
        d1 = self.as_dict()
1✔
2319
        d2 = other.as_dict()
1✔
2320
        for k, v in d1.items():
1✔
2321
            if v is not None and v != d2[k]:
1✔
2322
                return False
1✔
2323
        return True
1✔
2324

2325
    @classmethod
1✔
2326
    def from_string(cls, string: str) -> BasisInfo:
1✔
2327
        """Get summary info from a string"""
2328
        string = string.upper()
1✔
2329
        data: dict[str, Any] = {}
1✔
2330
        data["cc"] = "CC" in string
1✔
2331
        string = string.replace("CC", "")
1✔
2332
        data["pc"] = "PC" in string
1✔
2333
        string = string.replace("PC", "")
1✔
2334
        data["sr"] = "SR" in string
1✔
2335
        string = string.replace("SR", "")
1✔
2336
        data["molopt"] = "MOLOPT" in string
1✔
2337
        string = string.replace("MOLOPT", "")
1✔
2338
        for x in ("LDA", "PADE", "MGGA", "GGA", "HF", "PBE0", "PBE", "BP", "BLYP", "B3LYP", "SCAN"):
1✔
2339
            if x in string:
1✔
2340
                data["xc"] = x
1✔
2341
                string = string.replace(x, "")
1✔
2342
                break
1✔
2343

2344
        tmp = {"S": 1, "D": 2, "T": 3, "Q": 4}
1✔
2345
        if "ADMM" in string or "FIT" in string:
1✔
2346
            data["admm"] = True
1✔
2347
            bool_core = False
1✔
2348
            data["contracted"] = "C" in string
1✔
2349
            nums = "".join(s for s in string if s.isnumeric())
1✔
2350
            data["valence"] = int(nums) if nums else None
1✔
2351
        else:
2352
            data["admm"] = False
1✔
2353
            if "LRI" in string:
1✔
2354
                data["lri"] = True
×
2355
            bool_core = "V" not in string or "ALL" in string
1✔
2356

2357
        data["polarization"] = string.count("P")
1✔
2358
        data["diffuse"] = string.count("X")
1✔
2359
        string = "#" + string
1✔
2360
        for i, s in enumerate(string):
1✔
2361
            if s == "Z":
1✔
2362
                z = int(tmp.get(string[i - 1], string[i - 1]))
1✔
2363
                data["core"] = z if bool_core else None
1✔
2364
                data["valence"] = z
1✔
2365
            elif s == "P" and string[i - 1].isnumeric():
1✔
2366
                data["polarization"] = int(string[i - 1])
×
2367
            elif s == "X" and string[i - 1].isnumeric():
1✔
2368
                data["diffuse"] = int(string[i - 1])
×
2369
            elif s == "Q" and string[i + 1].isnumeric():
1✔
2370
                data["electrons"] = int("".join(_ for _ in string[i + 1 :] if _.isnumeric()))
1✔
2371

2372
        if not data["diffuse"]:
1✔
2373
            data["diffuse"] = string.count("AUG")
1✔
2374

2375
        return cls(**data)
1✔
2376

2377

2378
@dataclass
1✔
2379
class AtomicMetadata(MSONable):
1✔
2380
    """
2381
    Metadata for basis sets and potentials in cp2k
2382

2383
    Attributes:
2384
        info: Info about this object
2385
        element: Element for this object
2386
        potential: The potential for this object
2387
        name: Name of the object
2388
        alias_names: Optional aliases
2389
        filename: Name of the file containing this object
2390
        version: Version
2391
    """
2392

2393
    info: BasisInfo | PotentialInfo | None = None
1✔
2394
    element: Element | None = None
1✔
2395
    potential: Literal["All Electron", "Pseudopotential"] | None = None
1✔
2396
    name: str | None = None
1✔
2397
    alias_names: list = field(default_factory=list)
1✔
2398
    filename: str | None = None
1✔
2399
    version: str | None = None
1✔
2400

2401
    def softmatch(self, other):
1✔
2402
        """
2403
        Soft matching to see if a desired basis/potential matches requirements.
2404

2405
        Does soft matching on the "info" attribute first. Then soft matches against the
2406
        element and name/aliases.
2407
        """
2408
        if not isinstance(other, type(self)):
1✔
2409
            return False
×
2410
        if self.info and not self.info.softmatch(other.info):
1✔
2411
            return False
1✔
2412
        if self.element is not None and self.element != other.element:
1✔
2413
            return False
×
2414
        if self.potential is not None and self.potential != other.potential:
1✔
2415
            return False
×
2416
        this_names = [self.name]
1✔
2417
        if self.alias_names:
1✔
2418
            this_names.extend(self.alias_names)
×
2419
        other_names = [other.name]
1✔
2420
        if other.alias_names:
1✔
2421
            other_names.extend(other.alias_names)
1✔
2422
        for nm in this_names:
1✔
2423
            if nm is not None and nm not in other_names:
1✔
2424
                return False
1✔
2425
        return True
1✔
2426

2427
    def get_hash(self) -> str:
1✔
2428
        """Get a hash of this object"""
2429
        return md5(self.get_string().lower().encode("utf-8")).hexdigest()
×
2430

2431
    def get_string(self):
1✔
2432
        """Get string representation"""
2433
        return str(self)
×
2434

2435

2436
@dataclass
1✔
2437
class GaussianTypeOrbitalBasisSet(AtomicMetadata):
1✔
2438
    """
2439
    Model definition of a GTO basis set
2440

2441
    Attributes:
2442
        info: Cardinality of this basis
2443
        nset: Number of exponent sets
2444
        n: Principle quantum number for each set
2445
        lmax: Maximum angular momentum quantum number for each set
2446
        lmin: Minimum angular momentum quantum number for each set
2447
        nshell: Number of shells for angular momentum l for each set
2448
        exponents: Exponents for each set
2449
        coefficients: Contraction coefficients for each set. Dict[exp->l->shell]
2450
    """
2451

2452
    info: BasisInfo | None = None
1✔
2453
    nset: int | None = None
1✔
2454
    n: list[int] | None = None
1✔
2455
    lmax: list[int] | None = None
1✔
2456
    lmin: list[int] | None = None
1✔
2457
    nshell: list[dict[int, int]] | None = None
1✔
2458
    exponents: list[list[float]] | None = None
1✔
2459
    coefficients: list[dict[int, dict[int, dict[int, float]]]] | None = None
1✔
2460

2461
    def __post_init__(self) -> None:
1✔
2462
        if self.info and self.potential == "All Electron" and self.element:
1✔
2463
            self.info.electrons = self.element.Z
×
2464
        if self.name == "ALLELECTRON":
1✔
2465
            self.name = "ALL"  # cp2k won't parse ALLELECTRON for some reason
×
2466

2467
        def cast(d):
1✔
2468
            new = {}
1✔
2469
            for k, v in d.items():
1✔
2470
                if isinstance(v, dict):
1✔
2471
                    v = cast(v)
1✔
2472
                new[int(k)] = v
1✔
2473
            return new
1✔
2474

2475
        if self.nshell:
1✔
2476
            self.nshell = [cast(n) for n in self.nshell]
1✔
2477
        if self.coefficients:
1✔
2478
            self.coefficients = [cast(c) for c in self.coefficients]
1✔
2479

2480
    def get_keyword(self) -> Keyword:
1✔
2481
        """Convert basis to keyword object"""
2482
        if not self.name:
1✔
2483
            raise ValueError("No name attribute. Cannot create keyword")
×
2484
        vals: Any = []
1✔
2485
        if self.info and self.info.admm:
1✔
2486
            vals.append("AUX_FIT")
1✔
2487
        vals.append(self.name)
1✔
2488
        return Keyword("BASIS_SET", *vals)
1✔
2489

2490
    @property
1✔
2491
    def nexp(self):
1✔
2492
        """Number of exponents"""
2493
        return [len(e) for e in self.exponents]
1✔
2494

2495
    def get_string(self) -> str:
1✔
2496
        """Get standard cp2k GTO formatted string"""
2497
        if (
×
2498
            self.info is None
2499
            or self.nset is None
2500
            or self.n is None
2501
            or self.lmax is None
2502
            or self.lmin is None
2503
            or self.nshell is None
2504
            or self.exponents is None
2505
            or self.coefficients is None
2506
        ):
2507
            raise ValueError("Must have all attributes defined to get string representation")
×
2508

2509
        s = f"{str(self.element)} {self.name} {' '.join(self.alias_names)}\n"
×
2510
        s += f"{str(self.nset)}\n"
×
2511
        for set_index in range(self.nset):
×
2512
            s += (
×
2513
                f"{str(self.n[set_index])} "
2514
                f"{str(self.lmin[set_index])} "
2515
                f"{str(self.lmax[set_index])} "
2516
                f"{str(self.nexp[set_index])} "
2517
                f"{' '.join(map(str, self.nshell[set_index].values()))}\n"
2518
            )
2519
            for exp in self.coefficients[set_index]:
×
2520
                s += f"\t {self.exponents[set_index][exp]: .14f} "
×
2521
                for l in self.coefficients[set_index][exp]:
×
2522
                    for shell in self.coefficients[set_index][exp][l]:
×
2523
                        s += f"{self.coefficients[set_index][exp][l][shell]: .14f} "
×
2524
                s += "\n"
×
2525
        return s
×
2526

2527
    @classmethod
1✔
2528
    def from_string(cls, string: str) -> GaussianTypeOrbitalBasisSet:
1✔
2529
        """
2530
        Read from standard cp2k GTO formatted string
2531
        """
2532
        lines = [line for line in string.split("\n") if line]
1✔
2533
        firstline = lines[0].split()
1✔
2534
        element = Element(firstline[0])
1✔
2535
        names = firstline[1:]
1✔
2536
        name, aliases = names[0], names[1:]
1✔
2537
        _info = BasisInfo.from_string(name).as_dict()
1✔
2538
        for alias in aliases:
1✔
2539
            for k, v in BasisInfo.from_string(alias).as_dict().items():
1✔
2540
                if _info[k] is None:
1✔
2541
                    _info[k] = v
1✔
2542
        info = BasisInfo.from_dict(_info)
1✔
2543
        potential: Literal["All Electron", "Pseudopotential"]
2544
        if any("ALL" in x for x in [name] + aliases):
1✔
2545
            info.electrons = element.Z
×
2546
            potential = "All Electron"
×
2547
        else:
2548
            potential = "Pseudopotential"
1✔
2549
        nset = int(lines[1].split()[0])
1✔
2550
        n = []
1✔
2551
        lmin = []
1✔
2552
        lmax = []
1✔
2553
        nshell = []
1✔
2554
        exponents: list[list[float]] = []
1✔
2555
        coefficients: list[dict[int, dict[int, dict[int, float]]]] = []
1✔
2556

2557
        line_index = 2
1✔
2558
        for set_index in range(nset):
1✔
2559
            setinfo = lines[line_index].split()
1✔
2560
            _n, _lmin, _lmax, _nexp = map(int, setinfo[0:4])
1✔
2561
            n.append(_n)
1✔
2562
            lmin.append(_lmin)
1✔
2563
            lmax.append(_lmax)
1✔
2564

2565
            _nshell = map(int, setinfo[4:])
1✔
2566
            nshell.append({l: int(next(_nshell, 0)) for l in range(_lmin, _lmax + 1)})
1✔
2567
            exponents.append([])
1✔
2568
            coefficients.append({i: {l: {} for l in range(_lmin, _lmax + 1)} for i in range(_nexp)})
1✔
2569
            line_index += 1
1✔
2570

2571
            for i in range(_nexp):
1✔
2572
                line = lines[line_index].split()
1✔
2573
                exponents[set_index].append(float(line[0]))
1✔
2574
                coeffs = list(map(float, line[1:]))
1✔
2575

2576
                j = 0
1✔
2577
                for l in range(_lmin, _lmax + 1):
1✔
2578
                    for shell in range(nshell[set_index][l]):
1✔
2579
                        coefficients[set_index][i][l][shell] = coeffs[j]
1✔
2580
                        j += 1
1✔
2581

2582
                line_index += 1
1✔
2583

2584
        return cls(
1✔
2585
            element=element,
2586
            name=name,
2587
            alias_names=aliases,
2588
            info=info,
2589
            potential=potential,
2590
            nset=nset,
2591
            n=n,
2592
            lmin=lmin,
2593
            lmax=lmax,
2594
            nshell=nshell,
2595
            exponents=exponents,
2596
            coefficients=coefficients,
2597
        )
2598

2599

2600
@dataclass
1✔
2601
class PotentialInfo(MSONable):
1✔
2602
    """
2603
    Metadata for this potential
2604

2605
    Attributes:
2606
        electrons: Total number of electrons
2607
        potential_type: Potential type (e.g. GTH)
2608
        nlcc: Nonlinear core corrected potential
2609
        xc: Exchange correlation functional used for creating this potential
2610
    """
2611

2612
    electrons: int | None = None
1✔
2613
    potential_type: str | None = None
1✔
2614
    nlcc: bool | None = None
1✔
2615
    xc: str | None = None
1✔
2616

2617
    def softmatch(self, other):
1✔
2618
        """
2619
        Soft matching to see if two potentials match.
2620

2621
        Will only match those attributes which *are* defined for this basis info object (one way checking)
2622
        """
2623
        if not isinstance(other, PotentialInfo):
1✔
2624
            return False
×
2625
        d1 = self.as_dict()
1✔
2626
        d2 = other.as_dict()
1✔
2627
        for k, v in d1.items():
1✔
2628
            if v is not None and v != d2[k]:
1✔
2629
                return False
1✔
2630
        return True
1✔
2631

2632
    @classmethod
1✔
2633
    def from_string(cls, string):
1✔
2634
        """Get a cp2k formatted string representation"""
2635
        string = string.upper()
1✔
2636
        data = {}
1✔
2637
        if "NLCC" in string:
1✔
2638
            data["nlcc"] = True
1✔
2639
        if "GTH" in string:
1✔
2640
            data["potential_type"] = "GTH"
1✔
2641
        for i, s in enumerate(string):
1✔
2642
            if s == "Q" and string[i + 1].isnumeric():
1✔
2643
                data["electrons"] = int("".join(_ for _ in string[i + 1 :] if _.isnumeric()))
1✔
2644

2645
        for x in ("LDA", "PADA", "MGGA", "GGA", "HF", "PBE0", "PBE", "BP", "BLYP", "B3LYP", "SCAN"):
1✔
2646
            if x in string:
1✔
2647
                data["xc"] = x
1✔
2648
                break
1✔
2649

2650
        return cls(**data)
1✔
2651

2652

2653
@dataclass
1✔
2654
class GthPotential(AtomicMetadata):
1✔
2655
    """
2656
    Representation of GTH-type (pseudo)potential
2657

2658
    Attributes:
2659
        info: Info about this potential
2660
        n_elecs: Number of electrons for each quantum number
2661
        r_loc: Radius of local projectors
2662
        nexp_ppl: Number of the local pseudopotential functions
2663
        c_exp_ppl: Sequence = field(None, description="Coefficients of the local pseudopotential functions
2664
        radii: Radius of the nonlocal part for angular momentum quantum number l defined by the Gaussian
2665
            function exponents alpha_prj_ppnl
2666
        nprj: Number of projectors
2667
        nprj_ppnl: Number of the non-local projectors for the angular momentum quantum number
2668
        hprj_ppnl: Coefficients of the non-local projector functions. Coeff ij for ang momentum l
2669
        )
2670
    """
2671

2672
    info: PotentialInfo
1✔
2673
    n_elecs: dict[int, int] | None = None
1✔
2674
    r_loc: float | None = None
1✔
2675
    nexp_ppl: int | None = None
1✔
2676
    c_exp_ppl: Sequence | None = None
1✔
2677
    radii: dict[int, float] | None = None
1✔
2678
    nprj: int | None = None
1✔
2679
    nprj_ppnl: dict[int, int] | None = None
1✔
2680
    hprj_ppnl: dict[int, dict[int, dict[int, float]]] | None = None
1✔
2681

2682
    def __post_init__(self) -> None:
1✔
2683
        if self.potential == "All Electron" and self.element:
1✔
2684
            self.info.electrons = self.element.Z
1✔
2685
        if self.name == "ALLELECTRON":
1✔
2686
            self.name = "ALL"  # cp2k won't parse ALLELECTRON for some reason
1✔
2687

2688
        def cast(d):
1✔
2689
            new = {}
1✔
2690
            for k, v in d.items():
1✔
2691
                if isinstance(v, dict):
1✔
2692
                    v = cast(v)
1✔
2693
                new[int(k)] = v
1✔
2694
            return new
1✔
2695

2696
        if self.n_elecs:
1✔
2697
            self.n_elecs = cast(self.n_elecs)
1✔
2698
        if self.radii:
1✔
2699
            self.radii = cast(self.radii)
1✔
2700
        if self.nprj_ppnl:
1✔
2701
            self.nprj_ppnl = cast(self.nprj_ppnl)
1✔
2702
        if self.hprj_ppnl:
1✔
2703
            self.hprj_ppnl = cast(self.hprj_ppnl)
1✔
2704

2705
    def get_keyword(self) -> Keyword:
1✔
2706
        """Get keyword object for the potential"""
2707
        if self.name is None:
1✔
2708
            raise ValueError("Cannot get keyword without name attribute")
×
2709

2710
        return Keyword("POTENTIAL", self.name)
1✔
2711

2712
    def get_section(self) -> Section:
1✔
2713
        """
2714
        Convert model to a GTH-formatted section object for input files
2715
        """
2716
        if self.name is None:
×
2717
            raise ValueError("Cannot get section without name attribute")
×
2718

2719
        keywords = {"POTENTIAL": Keyword("", self.get_string())}
×
2720
        return Section(
×
2721
            name=self.name,
2722
            section_parameters=None,
2723
            subsections=None,
2724
            description="Manual definition of GTH Potential",
2725
            keywords=keywords,
2726
        )
2727

2728
    @classmethod
1✔
2729
    def from_section(cls, section: Section) -> GthPotential:
1✔
2730
        """
2731
        Extract GTH-formatted string from a section and convert it to model
2732
        """
2733
        sec = copy.deepcopy(section)
×
2734
        sec.verbosity(False)
×
2735
        s = sec.get_string()
×
2736
        s = [_ for _ in s.split("\n") if not _.startswith("&")]
×
2737
        s = "\n".join(s)
×
2738
        return cls.from_string(s)
×
2739

2740
    def get_string(self):
1✔
2741
        """
2742
        Convert model to a GTH-formatted string
2743
        """
2744
        if (
×
2745
            self.info is None
2746
            or self.n_elecs is None
2747
            or self.r_loc is None
2748
            or self.nexp_ppl is None
2749
            or self.c_exp_ppl is None
2750
            or self.radii is None
2751
            or self.nprj is None
2752
            or self.nprj_ppnl is None
2753
            or self.hprj_ppnl is None
2754
        ):
2755
            raise ValueError("Must initialize all attributes in order to get string")
×
2756

2757
        s = f"{str(self.element)} {self.name} {' '.join(self.alias_names)}\n"
×
2758
        s += f"{' '.join(str(self.n_elecs[i]) for i in range(len(self.n_elecs)))}\n"
×
2759
        s += f"{self.r_loc: .14f} {str(self.nexp_ppl)} "
×
2760
        for i in range(self.nexp_ppl):
×
2761
            s += f"{self.c_exp_ppl[i]: .14f} "
×
2762
        s += "\n"
×
2763
        s += f"{self.nprj} \n"
×
2764
        for l in range(self.nprj):
×
2765
            total_fill = self.nprj_ppnl[l] * 20 + 24
×
2766
            tmp = f"{self.radii[l]: .14f} {self.nprj_ppnl[l]: d}"
×
2767
            s += f"{tmp:>{''}{24}}"
×
2768
            for i in range(self.nprj_ppnl[l]):
×
2769
                k = total_fill - 24 if i == 0 else total_fill
×
2770
                tmp = " ".join(f"{v: .14f}" for v in self.hprj_ppnl[l][i].values())
×
2771
                s += f"{tmp:>{''}{k}}"
×
2772
                s += "\n"
×
2773
        return s
×
2774

2775
    @classmethod
1✔
2776
    def from_string(cls, string):
1✔
2777
        """
2778
        Initialize model from a GTH formatted string
2779
        """
2780
        lines = [line for line in string.split("\n") if line]
1✔
2781
        firstline = lines[0].split()
1✔
2782
        element, name, aliases = firstline[0], firstline[1], firstline[2:]
1✔
2783
        info = PotentialInfo.from_string(name).as_dict()
1✔
2784
        for alias in aliases:
1✔
2785
            for k, v in PotentialInfo.from_string(alias).as_dict().items():
1✔
2786
                if info[k] is None:
1✔
2787
                    info[k] = v
1✔
2788
        info = PotentialInfo.from_dict(info)
1✔
2789
        potential: Literal["All Electron", "Pseudopotential"]
2790
        if any("ALL" in x for x in [name] + aliases):
1✔
2791
            potential = "All Electron"
1✔
2792
            info.electrons = Element(element).Z
1✔
2793
        else:
2794
            potential = "Pseudopotential"
1✔
2795
        nelecs = {i: int(n) for i, n in enumerate(lines[1].split())}
1✔
2796
        info.electrons = sum(nelecs.values())  # override, more reliable than name
1✔
2797
        thirdline = lines[2].split()
1✔
2798
        r_loc, nexp_ppl, c_exp_ppl = (
1✔
2799
            float(thirdline[0]),
2800
            int(thirdline[1]),
2801
            list(map(float, thirdline[2:])),
2802
        )
2803
        nprj = int(lines[3].split()[0]) if len(lines) > 3 else 0
1✔
2804

2805
        radii = {}
1✔
2806
        nprj_ppnl = {}
1✔
2807
        hprj_ppnl = {}
1✔
2808
        lines = lines[4:]
1✔
2809
        i = 0
1✔
2810
        l = 0
1✔
2811
        L = 0
1✔
2812

2813
        while l < nprj:
1✔
2814
            line = lines[i].split()
1✔
2815
            radii[l] = float(line[0])
1✔
2816
            nprj_ppnl[l] = int(line[1])
1✔
2817
            hprj_ppnl[l] = {x: {} for x in range(nprj_ppnl[l])}
1✔
2818
            line = list(map(float, line[2:]))
1✔
2819
            hprj_ppnl[l][0] = {j: float(ln) for j, ln in enumerate(line)}
1✔
2820

2821
            L = 1
1✔
2822
            i += 1
1✔
2823
            while L < nprj_ppnl[l]:
1✔
2824
                line2 = list(map(float, lines[i].split()))
1✔
2825
                hprj_ppnl[l][L] = {j: float(ln) for j, ln in enumerate(line2)}
1✔
2826
                i += 1
1✔
2827
                L += 1
1✔
2828
            l += 1
1✔
2829

2830
        return cls(
1✔
2831
            element=Element(element),
2832
            name=name,
2833
            alias_names=aliases,
2834
            potential=potential,
2835
            n_elecs=nelecs,
2836
            r_loc=r_loc,
2837
            nexp_ppl=nexp_ppl,
2838
            c_exp_ppl=c_exp_ppl,
2839
            info=info,
2840
            radii=radii,
2841
            nprj=nprj,
2842
            nprj_ppnl=nprj_ppnl,
2843
            hprj_ppnl=hprj_ppnl,
2844
        )
2845

2846

2847
@dataclass
1✔
2848
class DataFile(MSONable):
1✔
2849
    """A data file for a cp2k calc."""
2850

2851
    objects: Sequence | None = None
1✔
2852

2853
    @classmethod
1✔
2854
    def from_file(cls, fn):
1✔
2855
        """Load from a file"""
2856
        with open(fn) as f:
×
2857
            data = cls.from_string(f.read())
×
2858
            for obj in data.objects:
×
2859
                obj.filename = fn
×
2860
            return data
×
2861

2862
    @classmethod
1✔
2863
    def from_string(cls):
1✔
2864
        """Initialize from a string"""
2865
        raise NotImplementedError
×
2866

2867
    def write_file(self, fn):
1✔
2868
        """Write to a file"""
2869
        with open(fn, "w") as f:
×
2870
            f.write(self.get_string())
×
2871

2872
    def get_string(self):
1✔
2873
        """Get string representation"""
2874
        return "\n".join(b.get_string() for b in self.objects)
×
2875

2876
    def __str__(self):
1✔
2877
        return self.get_string()
×
2878

2879

2880
@dataclass
1✔
2881
class BasisFile(DataFile):
1✔
2882
    """Data file for basis sets only"""
2883

2884
    @classmethod
1✔
2885
    def from_string(cls, string):
1✔
2886
        """Initialize from a string representation"""
2887
        basis_sets = [GaussianTypeOrbitalBasisSet.from_string(c) for c in chunk(string)]
1✔
2888
        return cls(objects=basis_sets)
1✔
2889

2890

2891
@dataclass
1✔
2892
class PotentialFile(DataFile):
1✔
2893
    """Data file for potentials only"""
2894

2895
    @classmethod
1✔
2896
    def from_string(cls, string):
1✔
2897
        """Initialize from a string representation"""
2898
        basis_sets = [GthPotential.from_string(c) for c in chunk(string)]
1✔
2899
        return cls(objects=basis_sets)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc