Coverage for calorine/calculators/_dftd3.py: 100%
27 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-08 12:52 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-08 12:52 +0000
1import math
2import numbers
3from typing import Any
5# The fields of the DFT-D3 correction, in the order GPUMD writes them on the `dftd3` line.
6DFTD3_KEYS = ('functional', 'potential_cutoff', 'coordination_number_cutoff')
9def parse_dftd3(dftd3: dict[str, Any] | None) -> tuple[str, float, float] | None:
10 """Validate DFT-D3 settings and return the values in the order GPUMD writes them.
12 The functional is returned in lower case, the form in which the extension reads it.
14 Parameters
15 ----------
16 dftd3
17 Settings with the keys ``functional``, ``potential_cutoff`` and
18 ``coordination_number_cutoff``, or None.
20 Returns
21 -------
22 The three values in the order they take on the ``dftd3`` line, or None.
24 Example
25 -------
26 >>> parse_dftd3(dict(functional='pbe', potential_cutoff=12, coordination_number_cutoff=6))
27 ('pbe', 12.0, 6.0)
28 """
29 if dftd3 is None:
30 return None
31 if not isinstance(dftd3, dict):
32 raise ValueError(f'dftd3 must be a dict, not {type(dftd3).__name__}.')
33 missing = [key for key in DFTD3_KEYS if key not in dftd3]
34 unknown = [key for key in dftd3 if key not in DFTD3_KEYS]
35 if missing or unknown:
36 raise ValueError(f'dftd3 takes the keys {", ".join(DFTD3_KEYS)}; '
37 f'missing: {missing}, unknown: {unknown}.')
39 functional = dftd3['functional']
40 # CPUNEP checks the name against the parameter table of the extension, and GPUNEP writes it
41 # to run.in for GPUMD to check against a table with the same names, all printable ASCII.
42 # The extension reports an error as a C string, which ends at a NUL, and pybind11 refuses a
43 # lone surrogate.
44 if not (isinstance(functional, str) and functional.split() == [functional]
45 and functional.isascii() and functional.isprintable()):
46 raise ValueError('The functional must be a name of printable ASCII characters without '
47 f'whitespace, not {functional!r}.')
49 cutoffs = []
50 for key in DFTD3_KEYS[1:]:
51 cutoff = dftd3[key]
52 if isinstance(cutoff, bool) or not isinstance(cutoff, numbers.Real):
53 raise ValueError(f'{key} must be a number, not {cutoff!r}.')
54 # A comparison against zero accepts nan and infinity.
55 if not math.isfinite(cutoff):
56 raise ValueError(f'{key} must be finite, not {cutoff}.')
57 if cutoff <= 0:
58 raise ValueError(f'{key} must be positive, not {cutoff}.')
59 # A Real such as a Fraction writes as 25/2 on the dftd3 line, which GPUMD reads as 25.
60 cutoffs.append(float(cutoff))
61 return (functional.lower(), *cutoffs)