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

1import numpy as np 

2from ase import Atoms 

3from numpy.typing import NDArray 

4from .tools import inv 

5from ..units import Dalton_to_dmu 

6 

7 

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 

15 

16 @property 

17 def pos(self) -> NDArray[float]: 

18 """Cartesian positions.""" 

19 return self._atoms.positions.copy() 

20 

21 @property 

22 def positions(self) -> NDArray[float]: 

23 """Cartesian positions.""" 

24 return self.pos 

25 

26 @property 

27 def spos(self) -> NDArray[float]: 

28 """Reduced (or scaled) positions of atoms.""" 

29 return self._atoms.get_scaled_positions() 

30 

31 @property 

32 def scaled_positions(self) -> NDArray[float]: 

33 """Reduced (or scaled) positions of atoms.""" 

34 return self.spos 

35 

36 @property 

37 def cell(self) -> NDArray[float]: 

38 """Cell of atoms with cell vectors as rows.""" 

39 return self._atoms.cell.array.copy() 

40 

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 

45 

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

50 

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²/Ų 

55 

56 @property 

57 def volume(self) -> float: 

58 """Volume of cell.""" 

59 return self._atoms.get_volume() 

60 

61 @property 

62 def n_atoms(self) -> int: 

63 """Number of atoms.""" 

64 return len(self._atoms) 

65 

66 @property 

67 def symbols(self) -> list[str]: 

68 """List of chemical symbol for each element.""" 

69 return list(self._atoms.symbols) 

70 

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

75 

76 def __str__(self) -> str: 

77 return f'{self.__class__.__name__} with {self.n_atoms} atoms' 

78 

79 def __repr__(self) -> str: 

80 return str(self) 

81 

82 

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

103 

104 strings = strings + atom_s 

105 

106 string = '\n'.join(strings) 

107 

108 return string 

109 

110 

111class Supercell(DynasorAtoms): 

112 """The supercell takes care of some mappings between the primitive and repeated structure. 

113 

114 In particular the P-matrix connecting the cells as well as the offset-index of each atom is 

115 calculated. 

116 

117 Note that the positions cannot be recovered as `offset x cell + basis` since the atoms get 

118 wrapped. 

119 

120 Parameters 

121 ---------- 

122 supercell 

123 Some ideal repetition of the primitive structure and possible wrapping. 

124 prim 

125 Primitive structure. 

126 """ 

127 

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

134 

135 self.prim = Prim(prim.copy()) 

136 super().__init__(supercell) 

137 

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) 

142 

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) 

146 

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

151 

152 @property 

153 def P_inv(self) -> NDArray[float]: 

154 """Inverse of `P`.""" 

155 return self._P_inv.copy() 

156 

157 @property 

158 def offsets(self) -> NDArray[float]: 

159 """The offset of each atom.""" 

160 return self._offsets.copy() 

161 

162 @property 

163 def indices(self) -> NDArray[int]: 

164 """The basis index of each atom""" 

165 return self._indices.copy() 

166 

167 @property 

168 def n_cells(self) -> int: 

169 """Number of unit cells""" 

170 return self.n_atoms // self.prim.n_atoms 

171 

172 def __str__(self): 

173 

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