Coverage for /builds/kinetik161/ase/ase/outputs.py: 98.02%
101 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1from abc import ABC, abstractmethod
2from collections.abc import Mapping
3from typing import Sequence, Union
5import numpy as np
8class Properties(Mapping):
9 def __init__(self, dct):
10 self._dct = {}
11 for name, value in dct.items():
12 self._setvalue(name, value)
14 def __len__(self):
15 return len(self._dct)
17 def __iter__(self):
18 return iter(self._dct)
20 def __getitem__(self, name):
21 return self._dct[name]
23 def _setvalue(self, name, value):
24 if name in self._dct:
25 # Which error should we raise for already existing property?
26 raise ValueError(f'{name} already set')
28 prop = all_outputs[name]
29 value = prop.normalize_type(value)
30 shape = np.shape(value)
32 if not self.shape_is_consistent(prop, value):
33 raise ValueError(f'{name} has bad shape: {shape}')
35 for i, spec in enumerate(prop.shapespec):
36 if not isinstance(spec, str) or spec in self._dct:
37 continue
38 self._setvalue(spec, shape[i])
40 self._dct[name] = value
42 def shape_is_consistent(self, prop, value) -> bool:
43 """Return whether shape of values is consistent with properties.
45 For example, forces of shape (7, 3) are consistent
46 unless properties already have "natoms" with non-7 value.
47 """
48 shapespec = prop.shapespec
49 shape = np.shape(value)
50 if len(shapespec) != len(shape):
51 return False
52 for dimspec, dim in zip(shapespec, shape):
53 if isinstance(dimspec, str):
54 dimspec = self._dct.get(dimspec, dim)
55 if dimspec != dim:
56 return False
57 return True
59 def __repr__(self):
60 clsname = type(self).__name__
61 return f'({clsname}({self._dct})'
64all_outputs = {}
67class Property(ABC):
68 def __init__(self, name, dtype, shapespec):
69 self.name = name
70 assert dtype in [float, int] # Others?
71 self.dtype = dtype
72 self.shapespec = shapespec
74 @abstractmethod
75 def normalize_type(self, value):
76 ...
78 def __repr__(self) -> str:
79 typename = self.dtype.__name__ # Extend to other than float/int?
80 shape = ', '.join(str(dim) for dim in self.shapespec)
81 return f'Property({self.name!r}, dtype={typename}, shape=[{shape}])'
84class ScalarProperty(Property):
85 def __init__(self, name, dtype):
86 super().__init__(name, dtype, ())
88 def normalize_type(self, value):
89 if not np.isscalar(value):
90 raise TypeError('Expected scalar')
91 return self.dtype(value)
94class ArrayProperty(Property):
95 def normalize_type(self, value):
96 if np.isscalar(value):
97 raise TypeError('Expected array, got scalar')
98 return np.asarray(value, dtype=self.dtype)
101ShapeSpec = Union[str, int]
104def _defineprop(
105 name: str,
106 dtype: type = float,
107 shape: Union[ShapeSpec, Sequence[ShapeSpec]] = ()
108) -> Property:
109 """Create, register, and return a property."""
111 if isinstance(shape, (int, str)):
112 shape = (shape,)
114 shape = tuple(shape)
115 prop: Property
116 if len(shape) == 0:
117 prop = ScalarProperty(name, dtype)
118 else:
119 prop = ArrayProperty(name, dtype, shape)
121 assert name not in all_outputs, name
122 all_outputs[name] = prop
123 return prop
126# Atoms, energy, forces, stress:
127_defineprop('natoms', int)
128_defineprop('energy', float)
129_defineprop('energies', float, shape='natoms')
130_defineprop('free_energy', float)
131_defineprop('forces', float, shape=('natoms', 3))
132_defineprop('stress', float, shape=6)
133_defineprop('stresses', float, shape=('natoms', 6))
135# Electronic structure:
136_defineprop('nbands', int)
137_defineprop('nkpts', int)
138_defineprop('nspins', int)
139_defineprop('fermi_level', float)
140_defineprop('kpoint_weights', float, shape='nkpts')
141_defineprop('ibz_kpoints', float, shape=('nkpts', 3))
142_defineprop('eigenvalues', float, shape=('nspins', 'nkpts', 'nbands'))
143_defineprop('occupations', float, shape=('nspins', 'nkpts', 'nbands'))
145# Miscellaneous:
146_defineprop('dipole', float, shape=3)
147_defineprop('charges', float, shape='natoms')
148_defineprop('magmom', float)
149_defineprop('magmoms', float, shape='natoms') # XXX spinors?
150_defineprop('polarization', float, shape=3)
151_defineprop('dielectric_tensor', float, shape=(3, 3))
152_defineprop('born_effective_charges', float, shape=('natoms', 3, 3))
154# We might want to allow properties that are part of Atoms, such as
155# positions, numbers, pbc, cell. It would be reasonable for those
156# concepts to have a formalization outside the Atoms class.
159# def to_singlepoint(self, atoms):
160# from ase.calculators.singlepoint import SinglePointDFTCalculator
161# return SinglePointDFTCalculator(atoms,
162# efermi=self.fermi_level,
164# We can also retrieve (P)DOS and band structure. However:
165#
166# * Band structure may require bandpath, which is an input, and
167# may not necessarily be easy or possible to reconstruct from
168# the outputs.
169#
170# * Some calculators may produce the whole BandStructure object in
171# one go (e.g. while parsing)
172#
173# * What about HOMO/LUMO? Can be obtained from
174# eigenvalues/occupations, but some codes provide real data. We
175# probably need to distinguish between HOMO/LUMO inferred by us
176# versus values provided within the output.
177#
178# * HOMO is sometimes used as alternative reference energy for
179# band structure.
180#
181# * What about spin-dependent (double) Fermi level?
182#
183# * What about 3D arrays? We will almost certainly want to be
184# connected to an object that can load dynamically from a file.