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.
Files changed (46) hide show
  1. httk/atomistic/__init__.py +98 -0
  2. httk/atomistic/cell.py +79 -0
  3. httk/atomistic/cell_api.py +21 -0
  4. httk/atomistic/cell_backend.py +20 -0
  5. httk/atomistic/cell_class.py +37 -0
  6. httk/atomistic/cell_class_view.py +40 -0
  7. httk/atomistic/cell_like.py +16 -0
  8. httk/atomistic/cell_params.py +94 -0
  9. httk/atomistic/cell_params_view.py +75 -0
  10. httk/atomistic/cell_primitive.py +56 -0
  11. httk/atomistic/cell_primitive_view.py +35 -0
  12. httk/atomistic/cell_view.py +21 -0
  13. httk/atomistic/elements.py +156 -0
  14. httk/atomistic/py.typed +0 -0
  15. httk/atomistic/sites.py +49 -0
  16. httk/atomistic/sites_api.py +21 -0
  17. httk/atomistic/sites_backend.py +20 -0
  18. httk/atomistic/sites_class.py +37 -0
  19. httk/atomistic/sites_class_view.py +40 -0
  20. httk/atomistic/sites_like.py +9 -0
  21. httk/atomistic/sites_primitive.py +56 -0
  22. httk/atomistic/sites_primitive_view.py +35 -0
  23. httk/atomistic/sites_view.py +21 -0
  24. httk/atomistic/species.py +87 -0
  25. httk/atomistic/species_api.py +51 -0
  26. httk/atomistic/species_backend.py +20 -0
  27. httk/atomistic/species_class.py +60 -0
  28. httk/atomistic/species_class_view.py +51 -0
  29. httk/atomistic/species_like.py +9 -0
  30. httk/atomistic/species_primitive.py +85 -0
  31. httk/atomistic/species_primitive_view.py +55 -0
  32. httk/atomistic/species_view.py +21 -0
  33. httk/atomistic/structure.py +102 -0
  34. httk/atomistic/structure_api.py +40 -0
  35. httk/atomistic/structure_backend.py +20 -0
  36. httk/atomistic/structure_like.py +15 -0
  37. httk/atomistic/structure_primitive.py +115 -0
  38. httk/atomistic/structure_primitive_view.py +50 -0
  39. httk/atomistic/structure_simple.py +52 -0
  40. httk/atomistic/structure_simple_view.py +40 -0
  41. httk/atomistic/structure_view.py +21 -0
  42. httk_atomistic-0.1.0.dist-info/METADATA +46 -0
  43. httk_atomistic-0.1.0.dist-info/RECORD +46 -0
  44. httk_atomistic-0.1.0.dist-info/WHEEL +5 -0
  45. httk_atomistic-0.1.0.dist-info/licenses/LICENSE +661 -0
  46. 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]
File without changes
@@ -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