httk-atomistic 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- httk/atomistic/__init__.py +98 -0
- httk/atomistic/cell.py +79 -0
- httk/atomistic/cell_api.py +21 -0
- httk/atomistic/cell_backend.py +20 -0
- httk/atomistic/cell_class.py +37 -0
- httk/atomistic/cell_class_view.py +40 -0
- httk/atomistic/cell_like.py +16 -0
- httk/atomistic/cell_params.py +94 -0
- httk/atomistic/cell_params_view.py +75 -0
- httk/atomistic/cell_primitive.py +56 -0
- httk/atomistic/cell_primitive_view.py +35 -0
- httk/atomistic/cell_view.py +21 -0
- httk/atomistic/elements.py +156 -0
- httk/atomistic/py.typed +0 -0
- httk/atomistic/sites.py +49 -0
- httk/atomistic/sites_api.py +21 -0
- httk/atomistic/sites_backend.py +20 -0
- httk/atomistic/sites_class.py +37 -0
- httk/atomistic/sites_class_view.py +40 -0
- httk/atomistic/sites_like.py +9 -0
- httk/atomistic/sites_primitive.py +56 -0
- httk/atomistic/sites_primitive_view.py +35 -0
- httk/atomistic/sites_view.py +21 -0
- httk/atomistic/species.py +87 -0
- httk/atomistic/species_api.py +51 -0
- httk/atomistic/species_backend.py +20 -0
- httk/atomistic/species_class.py +60 -0
- httk/atomistic/species_class_view.py +51 -0
- httk/atomistic/species_like.py +9 -0
- httk/atomistic/species_primitive.py +85 -0
- httk/atomistic/species_primitive_view.py +55 -0
- httk/atomistic/species_view.py +21 -0
- httk/atomistic/structure.py +102 -0
- httk/atomistic/structure_api.py +40 -0
- httk/atomistic/structure_backend.py +20 -0
- httk/atomistic/structure_like.py +15 -0
- httk/atomistic/structure_primitive.py +115 -0
- httk/atomistic/structure_primitive_view.py +50 -0
- httk/atomistic/structure_simple.py +52 -0
- httk/atomistic/structure_simple_view.py +40 -0
- httk/atomistic/structure_view.py +21 -0
- httk_atomistic-0.1.0.dist-info/METADATA +46 -0
- httk_atomistic-0.1.0.dist-info/RECORD +46 -0
- httk_atomistic-0.1.0.dist-info/WHEEL +5 -0
- httk_atomistic-0.1.0.dist-info/licenses/LICENSE +661 -0
- httk_atomistic-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal periodic table for httk-atomistic.
|
|
3
|
+
|
|
4
|
+
Provides the IUPAC element symbols in atomic-number order and helpers to convert
|
|
5
|
+
between a chemical symbol and its atomic number. The pseudo-symbols ``"X"``
|
|
6
|
+
(unknown element) and ``"vacancy"`` are deliberately not elements here; they are
|
|
7
|
+
handled at the ``Species`` level.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
SYMBOLS: tuple[str, ...] = (
|
|
11
|
+
"H",
|
|
12
|
+
"He",
|
|
13
|
+
"Li",
|
|
14
|
+
"Be",
|
|
15
|
+
"B",
|
|
16
|
+
"C",
|
|
17
|
+
"N",
|
|
18
|
+
"O",
|
|
19
|
+
"F",
|
|
20
|
+
"Ne",
|
|
21
|
+
"Na",
|
|
22
|
+
"Mg",
|
|
23
|
+
"Al",
|
|
24
|
+
"Si",
|
|
25
|
+
"P",
|
|
26
|
+
"S",
|
|
27
|
+
"Cl",
|
|
28
|
+
"Ar",
|
|
29
|
+
"K",
|
|
30
|
+
"Ca",
|
|
31
|
+
"Sc",
|
|
32
|
+
"Ti",
|
|
33
|
+
"V",
|
|
34
|
+
"Cr",
|
|
35
|
+
"Mn",
|
|
36
|
+
"Fe",
|
|
37
|
+
"Co",
|
|
38
|
+
"Ni",
|
|
39
|
+
"Cu",
|
|
40
|
+
"Zn",
|
|
41
|
+
"Ga",
|
|
42
|
+
"Ge",
|
|
43
|
+
"As",
|
|
44
|
+
"Se",
|
|
45
|
+
"Br",
|
|
46
|
+
"Kr",
|
|
47
|
+
"Rb",
|
|
48
|
+
"Sr",
|
|
49
|
+
"Y",
|
|
50
|
+
"Zr",
|
|
51
|
+
"Nb",
|
|
52
|
+
"Mo",
|
|
53
|
+
"Tc",
|
|
54
|
+
"Ru",
|
|
55
|
+
"Rh",
|
|
56
|
+
"Pd",
|
|
57
|
+
"Ag",
|
|
58
|
+
"Cd",
|
|
59
|
+
"In",
|
|
60
|
+
"Sn",
|
|
61
|
+
"Sb",
|
|
62
|
+
"Te",
|
|
63
|
+
"I",
|
|
64
|
+
"Xe",
|
|
65
|
+
"Cs",
|
|
66
|
+
"Ba",
|
|
67
|
+
"La",
|
|
68
|
+
"Ce",
|
|
69
|
+
"Pr",
|
|
70
|
+
"Nd",
|
|
71
|
+
"Pm",
|
|
72
|
+
"Sm",
|
|
73
|
+
"Eu",
|
|
74
|
+
"Gd",
|
|
75
|
+
"Tb",
|
|
76
|
+
"Dy",
|
|
77
|
+
"Ho",
|
|
78
|
+
"Er",
|
|
79
|
+
"Tm",
|
|
80
|
+
"Yb",
|
|
81
|
+
"Lu",
|
|
82
|
+
"Hf",
|
|
83
|
+
"Ta",
|
|
84
|
+
"W",
|
|
85
|
+
"Re",
|
|
86
|
+
"Os",
|
|
87
|
+
"Ir",
|
|
88
|
+
"Pt",
|
|
89
|
+
"Au",
|
|
90
|
+
"Hg",
|
|
91
|
+
"Tl",
|
|
92
|
+
"Pb",
|
|
93
|
+
"Bi",
|
|
94
|
+
"Po",
|
|
95
|
+
"At",
|
|
96
|
+
"Rn",
|
|
97
|
+
"Fr",
|
|
98
|
+
"Ra",
|
|
99
|
+
"Ac",
|
|
100
|
+
"Th",
|
|
101
|
+
"Pa",
|
|
102
|
+
"U",
|
|
103
|
+
"Np",
|
|
104
|
+
"Pu",
|
|
105
|
+
"Am",
|
|
106
|
+
"Cm",
|
|
107
|
+
"Bk",
|
|
108
|
+
"Cf",
|
|
109
|
+
"Es",
|
|
110
|
+
"Fm",
|
|
111
|
+
"Md",
|
|
112
|
+
"No",
|
|
113
|
+
"Lr",
|
|
114
|
+
"Rf",
|
|
115
|
+
"Db",
|
|
116
|
+
"Sg",
|
|
117
|
+
"Bh",
|
|
118
|
+
"Hs",
|
|
119
|
+
"Mt",
|
|
120
|
+
"Ds",
|
|
121
|
+
"Rg",
|
|
122
|
+
"Cn",
|
|
123
|
+
"Nh",
|
|
124
|
+
"Fl",
|
|
125
|
+
"Mc",
|
|
126
|
+
"Lv",
|
|
127
|
+
"Ts",
|
|
128
|
+
"Og",
|
|
129
|
+
)
|
|
130
|
+
"""The 118 IUPAC element symbols in atomic-number order (``SYMBOLS[0]`` is hydrogen)."""
|
|
131
|
+
|
|
132
|
+
_NUMBER_OF: dict[str, int] = {symbol: z for z, symbol in enumerate(SYMBOLS, start=1)}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def atomic_number(symbol: str) -> int:
|
|
136
|
+
"""
|
|
137
|
+
Return the atomic number (1-118) of an element symbol.
|
|
138
|
+
|
|
139
|
+
Raises ValueError for anything that is not one of the 118 element symbols
|
|
140
|
+
(in particular for the ``"X"`` and ``"vacancy"`` pseudo-symbols).
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
return _NUMBER_OF[symbol]
|
|
144
|
+
except KeyError:
|
|
145
|
+
raise ValueError(f"Unknown element symbol: {symbol!r}") from None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def symbol_of(z: int) -> str:
|
|
149
|
+
"""
|
|
150
|
+
Return the element symbol for the atomic number z (1-118).
|
|
151
|
+
|
|
152
|
+
Raises ValueError for atomic numbers outside the 1-118 range.
|
|
153
|
+
"""
|
|
154
|
+
if not 1 <= z <= len(SYMBOLS):
|
|
155
|
+
raise ValueError(f"Unknown atomic number: {z!r}")
|
|
156
|
+
return SYMBOLS[z - 1]
|
httk/atomistic/py.typed
ADDED
|
File without changes
|
httk/atomistic/sites.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Sites class for httk-atomistic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Sites:
|
|
9
|
+
"""
|
|
10
|
+
The sites of a crystal structure: the Nx3 matrix of reduced coordinates.
|
|
11
|
+
|
|
12
|
+
A Sites object holds N sites as the rows of ``reduced_coords`` and is iterable and
|
|
13
|
+
indexable over those length-3 coordinate rows (with ``len`` giving the number of
|
|
14
|
+
sites).
|
|
15
|
+
|
|
16
|
+
Note: the numeric values are stored as interim nested tuples of floats. They are
|
|
17
|
+
intended to be replaced by the httk exact vector representation fairly soon; keep
|
|
18
|
+
numeric access behind the ``reduced_coords`` accessor so that change stays contained.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_reduced_coords: tuple[tuple[float, ...], ...]
|
|
22
|
+
|
|
23
|
+
def __init__(self, reduced_coords: Sequence[Sequence[float]]) -> None:
|
|
24
|
+
norm = tuple(tuple(float(x) for x in row) for row in reduced_coords)
|
|
25
|
+
if any(len(row) != 3 for row in norm):
|
|
26
|
+
raise ValueError("Sites reduced_coords must be a sequence of length-3 coordinates")
|
|
27
|
+
self._reduced_coords = norm
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def reduced_coords(self) -> tuple[tuple[float, ...], ...]:
|
|
31
|
+
"""The Nx3 reduced site coordinates as nested float tuples (one site per row)."""
|
|
32
|
+
return self._reduced_coords
|
|
33
|
+
|
|
34
|
+
def __len__(self) -> int:
|
|
35
|
+
return len(self._reduced_coords)
|
|
36
|
+
|
|
37
|
+
def __iter__(self) -> Iterator[tuple[float, ...]]:
|
|
38
|
+
return iter(self._reduced_coords)
|
|
39
|
+
|
|
40
|
+
def __getitem__(self, index: int) -> tuple[float, ...]:
|
|
41
|
+
return self._reduced_coords[index]
|
|
42
|
+
|
|
43
|
+
def __eq__(self, other: object) -> bool:
|
|
44
|
+
if not isinstance(other, Sites):
|
|
45
|
+
return NotImplemented
|
|
46
|
+
return self._reduced_coords == other._reduced_coords
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"Sites(reduced_coords={self._reduced_coords!r})"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The minimal canonical sites interface for httk-atomistic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SitesAPI(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for the canonical sites interface.
|
|
11
|
+
|
|
12
|
+
It declares the single ``reduced_coords`` accessor (the Nx3 reduced coordinates)
|
|
13
|
+
that every sites backend produces from its own native representation and every sites
|
|
14
|
+
view builds its presentation from. This is the single interchange format; there is
|
|
15
|
+
no pairwise conversion between backends.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def reduced_coords(self) -> tuple[tuple[float, ...], ...]:
|
|
21
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The abstract base class for all sites backends in httk-atomistic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
|
+
|
|
7
|
+
from httk.core import Backend
|
|
8
|
+
|
|
9
|
+
from .sites_api import SitesAPI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SitesBackend(Backend["SitesBackend"], SitesAPI):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for all backends of sites data.
|
|
15
|
+
|
|
16
|
+
Concrete backends carry a native representation and produce the canonical Nx3
|
|
17
|
+
``reduced_coords`` declared by ``SitesAPI`` from it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
backend_classes: ClassVar[list[type[Backend[Any]]]]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend wrapping a Sites object in the class representation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .sites import Sites
|
|
8
|
+
from .sites_backend import SitesBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SitesClass(SitesBackend):
|
|
12
|
+
"""
|
|
13
|
+
Backend for sites backed by an actual ``Sites`` object.
|
|
14
|
+
|
|
15
|
+
Its ``reduced_coords`` accessor delegates to the wrapped Sites, and ``unwrap``
|
|
16
|
+
returns that Sites.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_sites: Sites
|
|
20
|
+
|
|
21
|
+
# Cannot type annotate __new__ as `Self | None` for some reason
|
|
22
|
+
def __new__(cls, obj: Any, **hints: Any) -> Any:
|
|
23
|
+
if not isinstance(obj, Sites):
|
|
24
|
+
return None
|
|
25
|
+
if hints and hints.get("kind", "class") != "class":
|
|
26
|
+
return None
|
|
27
|
+
return super().__new__(cls)
|
|
28
|
+
|
|
29
|
+
def __init__(self, obj: Sites, **hints: Any) -> None:
|
|
30
|
+
self._sites = obj
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def reduced_coords(self) -> tuple[tuple[float, ...], ...]:
|
|
34
|
+
return self._sites.reduced_coords
|
|
35
|
+
|
|
36
|
+
def unwrap(self) -> Any:
|
|
37
|
+
return self._sites
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A view presenting any sites backend as a Sites object (the class representation).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from httk.core import unwrap
|
|
8
|
+
|
|
9
|
+
from .sites import Sites
|
|
10
|
+
from .sites_backend import SitesBackend
|
|
11
|
+
from .sites_like import SitesLike
|
|
12
|
+
from .sites_view import SitesView
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SitesClassView(SitesView, Sites):
|
|
16
|
+
"""
|
|
17
|
+
A view presenting an underlying sites backend as a ``Sites`` object.
|
|
18
|
+
|
|
19
|
+
This view is a genuine ``Sites``, so it can be passed anywhere a Sites is accepted.
|
|
20
|
+
Its coordinates are built eagerly from the backend on construction.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_backend: SitesBackend
|
|
24
|
+
|
|
25
|
+
def __new__(cls, obj: SitesLike, **hints: Any) -> Self:
|
|
26
|
+
if isinstance(obj, cls):
|
|
27
|
+
return obj
|
|
28
|
+
backend = cls._prepare_backend(obj, hints)
|
|
29
|
+
instance = super().__new__(cls)
|
|
30
|
+
# Sites is mutable, so its state is initialized here in __new__ (keeping __init__ a no-op),
|
|
31
|
+
# so that rewrapping an existing view via cls(view) does not re-initialize it.
|
|
32
|
+
Sites.__init__(instance, backend.reduced_coords)
|
|
33
|
+
instance._backend = backend
|
|
34
|
+
return instance
|
|
35
|
+
|
|
36
|
+
def __init__(self, obj: SitesLike, **hints: Any) -> None:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def unwrap(self) -> Any:
|
|
40
|
+
return unwrap(self._backend)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The accepted-input union for sites functions in httk-atomistic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from . import sites, sites_backend, sites_view
|
|
8
|
+
|
|
9
|
+
type SitesLike = (sites_backend.SitesBackend | sites_view.SitesView | sites.Sites | tuple[Any, ...] | list[Any])
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend wrapping a raw Nx3 matrix of reduced coordinates.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .sites_backend import SitesBackend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_number(value: Any) -> bool:
|
|
11
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_nx3(matrix: Any) -> bool:
|
|
15
|
+
if not isinstance(matrix, (list, tuple)):
|
|
16
|
+
return False
|
|
17
|
+
for row in matrix:
|
|
18
|
+
if not isinstance(row, (list, tuple)) or len(row) != 3:
|
|
19
|
+
return False
|
|
20
|
+
if not all(_is_number(x) for x in row):
|
|
21
|
+
return False
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SitesPrimitive(SitesBackend):
|
|
26
|
+
"""
|
|
27
|
+
Backend for sites backed by a raw Nx3 list or tuple of numbers.
|
|
28
|
+
|
|
29
|
+
The native representation is an Nx3 nested list or tuple of reduced coordinates (one
|
|
30
|
+
site per row). The ``reduced_coords`` are derived lazily and cached, and ``unwrap``
|
|
31
|
+
returns the original raw object.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
_raw: Any
|
|
35
|
+
_reduced_coords_cache: tuple[tuple[float, ...], ...] | None
|
|
36
|
+
|
|
37
|
+
# Cannot type annotate __new__ as `Self | None` for some reason
|
|
38
|
+
def __new__(cls, obj: Any, **hints: Any) -> Any:
|
|
39
|
+
if hints and hints.get("kind", "primitive") != "primitive":
|
|
40
|
+
return None
|
|
41
|
+
if not _is_nx3(obj):
|
|
42
|
+
return None
|
|
43
|
+
return super().__new__(cls)
|
|
44
|
+
|
|
45
|
+
def __init__(self, obj: Any, **hints: Any) -> None:
|
|
46
|
+
self._raw = obj
|
|
47
|
+
self._reduced_coords_cache = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def reduced_coords(self) -> tuple[tuple[float, ...], ...]:
|
|
51
|
+
if self._reduced_coords_cache is None:
|
|
52
|
+
self._reduced_coords_cache = tuple(tuple(float(x) for x in row) for row in self._raw)
|
|
53
|
+
return self._reduced_coords_cache
|
|
54
|
+
|
|
55
|
+
def unwrap(self) -> Any:
|
|
56
|
+
return self._raw
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A view presenting any sites backend as a raw Nx3 tuple of reduced coordinates.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from httk.core import unwrap
|
|
8
|
+
|
|
9
|
+
from .sites_backend import SitesBackend
|
|
10
|
+
from .sites_like import SitesLike
|
|
11
|
+
from .sites_view import SitesView
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SitesPrimitiveView(SitesView, tuple):
|
|
15
|
+
"""
|
|
16
|
+
A view presenting an underlying sites backend as a raw Nx3 matrix.
|
|
17
|
+
|
|
18
|
+
This view is a genuine tuple of reduced-coordinate rows, built eagerly and immutable.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_backend: SitesBackend
|
|
22
|
+
|
|
23
|
+
def __new__(cls, obj: SitesLike, **hints: Any) -> Self:
|
|
24
|
+
if isinstance(obj, cls):
|
|
25
|
+
return obj
|
|
26
|
+
backend = cls._prepare_backend(obj, hints)
|
|
27
|
+
instance = super().__new__(cls, backend.reduced_coords)
|
|
28
|
+
instance._backend = backend
|
|
29
|
+
return instance
|
|
30
|
+
|
|
31
|
+
def __init__(self, obj: SitesLike, **hints: Any) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
def unwrap(self) -> Any:
|
|
35
|
+
return unwrap(self._backend)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The abstract base class for all sites views in httk-atomistic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar, Self
|
|
6
|
+
|
|
7
|
+
from httk.core import View
|
|
8
|
+
|
|
9
|
+
from .sites_backend import SitesBackend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SitesView(View[SitesBackend]):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for all views of sites data.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_backend_base_cls: ClassVar[type[SitesBackend]] = SitesBackend # type: ignore[type-abstract]
|
|
18
|
+
_view_base_cls: ClassVar[type[Self]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
SitesView._view_base_cls = SitesView
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Species definition for httk-atomistic, mirroring the OPTIMADE ``species`` entry.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .elements import SYMBOLS
|
|
9
|
+
|
|
10
|
+
_ELEMENTS: frozenset[str] = frozenset(SYMBOLS)
|
|
11
|
+
_SPECIAL_SYMBOLS: frozenset[str] = frozenset({"X", "vacancy"})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Species:
|
|
16
|
+
"""
|
|
17
|
+
A chemical species occupying one or more sites, mirroring the OPTIMADE ``species`` object.
|
|
18
|
+
|
|
19
|
+
A species has a ``name`` (unique within a structure; it need not be a chemical
|
|
20
|
+
symbol), a list of ``chemical_symbols`` composing it, and a matching list of
|
|
21
|
+
``concentration`` values. Each chemical symbol is an element symbol, or one of
|
|
22
|
+
the pseudo-symbols ``"X"`` (unknown) or ``"vacancy"``. The optional ``mass``,
|
|
23
|
+
``attached``, ``nattached``, and ``original_name`` fields carry the remaining
|
|
24
|
+
OPTIMADE species information; ``attached`` and ``nattached`` must be given
|
|
25
|
+
together and share their length.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
chemical_symbols: tuple[str, ...]
|
|
30
|
+
concentration: tuple[float, ...]
|
|
31
|
+
mass: tuple[float, ...] | None = None
|
|
32
|
+
original_name: str | None = None
|
|
33
|
+
attached: tuple[str, ...] | None = None
|
|
34
|
+
nattached: tuple[int, ...] | None = None
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
object.__setattr__(self, "chemical_symbols", tuple(self.chemical_symbols))
|
|
38
|
+
object.__setattr__(self, "concentration", tuple(float(c) for c in self.concentration))
|
|
39
|
+
if self.mass is not None:
|
|
40
|
+
object.__setattr__(self, "mass", tuple(float(m) for m in self.mass))
|
|
41
|
+
if self.attached is not None:
|
|
42
|
+
object.__setattr__(self, "attached", tuple(self.attached))
|
|
43
|
+
if self.nattached is not None:
|
|
44
|
+
object.__setattr__(self, "nattached", tuple(int(n) for n in self.nattached))
|
|
45
|
+
|
|
46
|
+
if len(self.concentration) != len(self.chemical_symbols):
|
|
47
|
+
raise ValueError("Species concentration must have the same length as chemical_symbols")
|
|
48
|
+
for symbol in self.chemical_symbols:
|
|
49
|
+
if symbol not in _ELEMENTS and symbol not in _SPECIAL_SYMBOLS:
|
|
50
|
+
raise ValueError(f"Species chemical symbol is not an element, 'X', or 'vacancy': {symbol!r}")
|
|
51
|
+
if self.mass is not None and len(self.mass) != len(self.chemical_symbols):
|
|
52
|
+
raise ValueError("Species mass must have the same length as chemical_symbols")
|
|
53
|
+
if (self.attached is None) != (self.nattached is None):
|
|
54
|
+
raise ValueError("Species attached and nattached must be given together or not at all")
|
|
55
|
+
if self.attached is not None and self.nattached is not None and len(self.attached) != len(self.nattached):
|
|
56
|
+
raise ValueError("Species attached and nattached must have the same length")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_single_element(self) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Whether this species is a single, unattached, real chemical element.
|
|
62
|
+
|
|
63
|
+
True only for a species composed of exactly one element symbol (not ``"X"``
|
|
64
|
+
or ``"vacancy"``) with no attached particles. Such species are the ones that
|
|
65
|
+
can be represented as a bare atomic number in the primitive representation.
|
|
66
|
+
"""
|
|
67
|
+
return len(self.chemical_symbols) == 1 and self.chemical_symbols[0] in _ELEMENTS and self.attached is None
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def create(cls, obj: "Species | dict[str, Any]") -> "Species":
|
|
71
|
+
"""
|
|
72
|
+
Return a Species from either an existing Species (returned unchanged) or an OPTIMADE species dict.
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(obj, Species):
|
|
75
|
+
return obj
|
|
76
|
+
attached = obj.get("attached")
|
|
77
|
+
nattached = obj.get("nattached")
|
|
78
|
+
mass = obj.get("mass")
|
|
79
|
+
return cls(
|
|
80
|
+
name=obj["name"],
|
|
81
|
+
chemical_symbols=tuple(obj["chemical_symbols"]),
|
|
82
|
+
concentration=tuple(obj["concentration"]),
|
|
83
|
+
mass=None if mass is None else tuple(mass),
|
|
84
|
+
original_name=obj.get("original_name"),
|
|
85
|
+
attached=None if attached is None else tuple(attached),
|
|
86
|
+
nattached=None if nattached is None else tuple(nattached),
|
|
87
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The minimal canonical species interface for httk-atomistic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SpeciesAPI(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for the canonical single-species interface.
|
|
11
|
+
|
|
12
|
+
It declares the accessors mirroring the OPTIMADE ``species`` fields that every
|
|
13
|
+
species backend produces from its own native representation and every species view
|
|
14
|
+
builds its presentation from: ``name``, ``chemical_symbols``, ``concentration``,
|
|
15
|
+
and the optional ``mass``, ``attached``, ``nattached``, and ``original_name``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def chemical_symbols(self) -> tuple[str, ...]:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def concentration(self) -> tuple[float, ...]:
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def mass(self) -> tuple[float, ...] | None:
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def attached(self) -> tuple[str, ...] | None:
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def nattached(self) -> tuple[int, ...] | None:
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def original_name(self) -> str | None:
|
|
51
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The abstract base class for all species backends in httk-atomistic.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
|
+
|
|
7
|
+
from httk.core import Backend
|
|
8
|
+
|
|
9
|
+
from .species_api import SpeciesAPI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpeciesBackend(Backend["SpeciesBackend"], SpeciesAPI):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for all backends of single-species data.
|
|
15
|
+
|
|
16
|
+
Concrete backends carry a native representation and produce the canonical OPTIMADE
|
|
17
|
+
species accessors declared by ``SpeciesAPI`` from it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
backend_classes: ClassVar[list[type[Backend[Any]]]]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend wrapping a Species in the class representation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .species import Species
|
|
8
|
+
from .species_backend import SpeciesBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpeciesClass(SpeciesBackend):
|
|
12
|
+
"""
|
|
13
|
+
Backend for a species backed by an actual ``Species`` object.
|
|
14
|
+
|
|
15
|
+
Its accessors delegate to the wrapped Species, and ``unwrap`` returns that Species.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_species: Species
|
|
19
|
+
|
|
20
|
+
# Cannot type annotate __new__ as `Self | None` for some reason
|
|
21
|
+
def __new__(cls, obj: Any, **hints: Any) -> Any:
|
|
22
|
+
if not isinstance(obj, Species):
|
|
23
|
+
return None
|
|
24
|
+
if hints and hints.get("kind", "class") != "class":
|
|
25
|
+
return None
|
|
26
|
+
return super().__new__(cls)
|
|
27
|
+
|
|
28
|
+
def __init__(self, obj: Species, **hints: Any) -> None:
|
|
29
|
+
self._species = obj
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
return self._species.name
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def chemical_symbols(self) -> tuple[str, ...]:
|
|
37
|
+
return self._species.chemical_symbols
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def concentration(self) -> tuple[float, ...]:
|
|
41
|
+
return self._species.concentration
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def mass(self) -> tuple[float, ...] | None:
|
|
45
|
+
return self._species.mass
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def attached(self) -> tuple[str, ...] | None:
|
|
49
|
+
return self._species.attached
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def nattached(self) -> tuple[int, ...] | None:
|
|
53
|
+
return self._species.nattached
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def original_name(self) -> str | None:
|
|
57
|
+
return self._species.original_name
|
|
58
|
+
|
|
59
|
+
def unwrap(self) -> Any:
|
|
60
|
+
return self._species
|