Coverage for dynasor/modes/atoms.py: 100%
88 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-10 08:27 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-10 08:27 +0000
1import numpy as np
2from ase import Atoms
3from numpy.typing import NDArray
4from .tools import inv
5from ..units import Dalton_to_dmu
8class DynasorAtoms:
9 """Dynasor's representation of a structure."""
10 def __init__(self, atoms: Atoms):
11 """Initialized using an ASE Atoms object"""
12 if not isinstance(atoms, Atoms):
13 raise TypeError(f'atoms must be an ASE Atoms object, not {type(atoms).__name__}.')
14 self._atoms = atoms
16 @property
17 def pos(self) -> NDArray[float]:
18 """Cartesian positions."""
19 return self._atoms.positions.copy()
21 @property
22 def positions(self) -> NDArray[float]:
23 """Cartesian positions."""
24 return self.pos
26 @property
27 def spos(self) -> NDArray[float]:
28 """Reduced (or scaled) positions of atoms."""
29 return self._atoms.get_scaled_positions()
31 @property
32 def scaled_positions(self) -> NDArray[float]:
33 """Reduced (or scaled) positions of atoms."""
34 return self.spos
36 @property
37 def cell(self) -> NDArray[float]:
38 """Cell of atoms with cell vectors as rows."""
39 return self._atoms.cell.array.copy()
41 @property
42 def inv_cell(self) -> NDArray[float]:
43 """The inverse cell transpose so the inverse cell vectors are rows, no 2pi."""
44 return np.linalg.inv(self._atoms.cell.array).T
46 @property
47 def numbers(self) -> NDArray[int]:
48 """Chemical number for each atom, e.g., 1 for H, 2 for He etc."""
49 return self._atoms.numbers.copy()
51 @property
52 def masses(self) -> NDArray[float]:
53 """Masses of atoms in dmu."""
54 return self._atoms.get_masses() * Dalton_to_dmu # In eVfs²/Ų
56 @property
57 def volume(self) -> float:
58 """Volume of cell."""
59 return self._atoms.get_volume()
61 @property
62 def n_atoms(self) -> int:
63 """Number of atoms."""
64 return len(self._atoms)
66 @property
67 def symbols(self) -> list[str]:
68 """List of chemical symbol for each element."""
69 return list(self._atoms.symbols)
71 def to_ase(self) -> Atoms:
72 """Converts the internal Atoms to ASE :class:`Atoms`."""
73 return Atoms(cell=self.cell, numbers=self.numbers, positions=self.positions, pbc=True,
74 masses=self._atoms.get_masses())
76 def __str__(self) -> str:
77 return f'{self.__class__.__name__} with {self.n_atoms} atoms'
79 def __repr__(self) -> str:
80 return str(self)
83class Prim(DynasorAtoms):
84 def __str__(self):
85 strings = [f"""Primitive cell:
86Number of atoms: {self.n_atoms}
87Volume: {self.volume:.3f}
88Atomic species present: {set(self.symbols)}
89Atomic numbers present: {set([int(n) for n in self.numbers])}
90Cell:
91[[{self.cell[0, 0]:<20}, {self.cell[0, 1]:<20}, {self.cell[0, 2]:<20}],
92 [{self.cell[1, 0]:<20}, {self.cell[1, 1]:<20}, {self.cell[1, 2]:<20}],
93 [{self.cell[2, 0]:<20}, {self.cell[2, 1]:<20}, {self.cell[2, 2]:<20}]]
94"""]
95 strings.append(f"{'Ind':<5}{'Sym':<5}{'Num':<5}{'Mass (Da)':<10}{'x':<10}{'y':<10}{'z':<10}"
96 f"{'a':<10}{'b':<10}{'c':<10}")
97 atom_s = []
98 for i, p, sp, m, n, s in zip(
99 range(self.n_atoms), self.positions, self.spos, self.masses / Dalton_to_dmu,
100 self.numbers, [a.symbol for a in self.to_ase()]):
101 atom_s.append(f'{i:<5}{s:<5}{n:<5}{m:<10.2f}{p[0]:<10.3f}{p[1]:<10.3f}{p[2]:<10.3f}'
102 f'{sp[0]:<10.3f}{sp[1]:<10.3f}{sp[2]:<10.3f}')
104 strings = strings + atom_s
106 string = '\n'.join(strings)
108 return string
111class Supercell(DynasorAtoms):
112 """The supercell takes care of some mappings between the primitive and repeated structure.
114 In particular the P-matrix connecting the cells as well as the offset-index of each atom is
115 calculated.
117 Note that the positions cannot be recovered as `offset x cell + basis` since the atoms get
118 wrapped.
120 Parameters
121 ----------
122 supercell
123 Some ideal repetition of the primitive structure and possible wrapping.
124 prim
125 Primitive structure.
126 """
128 def __init__(self, supercell: Atoms, prim: Atoms):
129 if not isinstance(supercell, Atoms):
130 raise TypeError(
131 f'supercell must be an ASE Atoms object, not {type(supercell).__name__}.')
132 if not isinstance(prim, Atoms):
133 raise TypeError(f'prim must be an ASE Atoms object, not {type(prim).__name__}.')
135 self.prim = Prim(prim.copy())
136 super().__init__(supercell)
138 # determine P-matrix relating supercell to primitive cell
139 from dynasor.tools.structures import get_P_matrix
140 self._P = get_P_matrix(self.prim.cell, self.cell) # P C = S
141 self._P_inv = inv(self.P)
143 # find the index and offsets for supercell using primitive as base unit
144 from dynasor.tools.structures import get_offset_index
145 self._offsets, self._indices = get_offset_index(prim, supercell, wrap=True)
147 @property
148 def P(self) -> NDArray[float]:
149 """P-matrix is defined as dot(P, prim.cell) = supercell.cell"""
150 return self._P.copy()
152 @property
153 def P_inv(self) -> NDArray[float]:
154 """Inverse of `P`."""
155 return self._P_inv.copy()
157 @property
158 def offsets(self) -> NDArray[float]:
159 """The offset of each atom."""
160 return self._offsets.copy()
162 @property
163 def indices(self) -> NDArray[int]:
164 """The basis index of each atom"""
165 return self._indices.copy()
167 @property
168 def n_cells(self) -> int:
169 """Number of unit cells"""
170 return self.n_atoms // self.prim.n_atoms
172 def __str__(self):
174 string = f"""Supercell:
175Number of atoms: {self.n_atoms}
176Volume: {self.volume:.3f}
177Number of unit cells: {self.n_cells}
178Cell:
179[[{self.cell[0, 0]:<20}, {self.cell[0, 1]:<20}, {self.cell[0, 2]:<20}],
180 [{self.cell[1, 0]:<20}, {self.cell[1, 1]:<20}, {self.cell[1, 2]:<20}],
181 [{self.cell[2, 0]:<20}, {self.cell[2, 1]:<20}, {self.cell[2, 2]:<20}]]
182P-matrix:
183[[{self.P[0, 0]:<20}, {self.P[0, 1]:<20}, {self.P[0, 2]:<20}],
184 [{self.P[1, 0]:<20}, {self.P[1, 1]:<20}, {self.P[1, 2]:<20}],
185 [{self.P[2, 0]:<20}, {self.P[2, 1]:<20}, {self.P[2, 2]:<20}]]
186{self.prim}
187"""
188 return string