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

1import math 

2import numbers 

3from typing import Any 

4 

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') 

7 

8 

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. 

11 

12 The functional is returned in lower case, the form in which the extension reads it. 

13 

14 Parameters 

15 ---------- 

16 dftd3 

17 Settings with the keys ``functional``, ``potential_cutoff`` and 

18 ``coordination_number_cutoff``, or None. 

19 

20 Returns 

21 ------- 

22 The three values in the order they take on the ``dftd3`` line, or None. 

23 

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}.') 

38 

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}.') 

48 

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)