Coverage for icet/core/matrix_of_equivalent_positions.py: 94%

71 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-08-02 10:14 +0000

1""" 

2This module provides a Python interface to the MatrixOfEquivalentPositions 

3class with supplementary functions. 

4""" 

5 

6import numpy as np 

7import spglib 

8 

9from ase import Atoms 

10from icet.core.lattice_site import LatticeSite 

11from icet.core.neighbor_list import get_neighbor_lists 

12from icet.core.structure import Structure 

13from icet.input_output.logging_tools import logger 

14from icet.tools.geometry import (ase_atoms_to_spglib_cell, 

15 get_fractional_positions_from_neighbor_list, 

16 get_primitive_structure) 

17 

18logger = logger.getChild('matrix_of_equivalent_positions') 

19 

20 

21class MatrixOfEquivalentPositions: 

22 """ 

23 This class handles a matrix of equivalent positions given the symmetry 

24 elements of an atomic structure. 

25 

26 Note 

27 ---- 

28 As a user you will usually not interact directly with objects of this type. 

29 

30 Parameters 

31 ---------- 

32 translations 

33 Translational symmetry elements. 

34 rotations 

35 Rotational symmetry elements. 

36 """ 

37 

38 def __init__(self, translations: np.ndarray, rotations: np.ndarray): 

39 if len(translations) != len(rotations): 

40 raise ValueError(f'The number of translations ({len(translations)})' 

41 f' must equal the number of rotations ({len(rotations)}).') 

42 self.n_symmetries = len(rotations) 

43 self.translations = np.array(translations) 

44 self.rotations = np.array(rotations) 

45 

46 def build(self, fractional_positions: np.ndarray) -> None: 

47 """ 

48 Builds a matrix of symmetry equivalent positions given a set of input 

49 coordinates using the rotational and translational symmetries provided upon 

50 initialization of the object. 

51 

52 Parameters 

53 ---------- 

54 fractional_positions 

55 Atomic positions in fractional coordinates. 

56 Dimensions: (number of atoms, 3 fractional coordinates). 

57 """ 

58 positions = np.dot(self.rotations, fractional_positions.transpose()) 

59 positions = np.moveaxis(positions, 2, 0) 

60 translations = self.translations[np.newaxis, :].repeat(len(fractional_positions), axis=0) 

61 positions += translations 

62 self.positions = positions 

63 

64 def get_equivalent_positions(self) -> np.ndarray: 

65 """ 

66 Returns the matrix of equivalent positions. Each row corresponds 

67 to a set of symmetry equivalent positions. The entry in the 

68 first column is commonly treated as the representative position. 

69 Dimensions: (number of atoms, number of symmetries, 3 fractional coordinates) 

70 """ 

71 return self.positions 

72 

73 

74def matrix_of_equivalent_positions_from_structure(structure: Atoms, 

75 cutoff: float, 

76 position_tolerance: float, 

77 symprec: float, 

78 find_primitive: bool = True) \ 

79 -> tuple[np.ndarray, Structure, list]: 

80 """Sets up a matrix of equivalent positions from an :class:`Atoms <ase.Atoms>` object. 

81 

82 Parameters 

83 ---------- 

84 structure 

85 Input structure. 

86 cutoff 

87 Cutoff radius. 

88 find_primitive 

89 If ``True`` the symmetries of the primitive structure will be employed. 

90 symprec 

91 Tolerance imposed when analyzing the symmetry using spglib. 

92 position_tolerance 

93 Tolerance applied when comparing positions in Cartesian coordinates. 

94 

95 Returns 

96 ------- 

97 The tuple that is returned comprises the matrix of equivalent positions, 

98 the primitive structure, and the neighbor list. 

99 """ 

100 

101 structure = structure.copy() 

102 structure_prim = structure 

103 if find_primitive: 

104 structure_prim = get_primitive_structure(structure, symprec=symprec) 

105 logger.debug(f'Size of primitive structure: {len(structure_prim)}') 

106 

107 # get symmetry information 

108 structure_as_tuple = ase_atoms_to_spglib_cell(structure_prim) 

109 symmetry = spglib.get_symmetry(structure_as_tuple, symprec=symprec) 

110 translations = symmetry['translations'] 

111 rotations = symmetry['rotations'] 

112 

113 # set up a MatrixOfEquivalentPositions object 

114 matrix_of_equivalent_positions = MatrixOfEquivalentPositions(translations, rotations) 

115 

116 # create neighbor lists 

117 prim_icet_structure = Structure.from_atoms(structure_prim) 

118 

119 neighbor_list = get_neighbor_lists(prim_icet_structure, 

120 [cutoff], 

121 position_tolerance=position_tolerance)[0] 

122 

123 # get fractional positions for neighbor_list 

124 frac_positions = get_fractional_positions_from_neighbor_list( 

125 prim_icet_structure, neighbor_list) 

126 

127 logger.debug(f'Number of fractional positions: {len(frac_positions)}') 

128 if frac_positions is not None: 128 ↛ 131line 128 didn't jump to line 131 because the condition on line 128 was always true

129 matrix_of_equivalent_positions.build(frac_positions) 

130 

131 return matrix_of_equivalent_positions, prim_icet_structure, neighbor_list 

132 

133 

134def _get_lattice_site_matrix_of_equivalent_positions( 

135 structure: Structure, 

136 matrix_of_equivalent_positions: MatrixOfEquivalentPositions, 

137 fractional_position_tolerance: float, 

138 prune: bool = True) -> np.ndarray: 

139 """ 

140 Returns a transformed matrix of equivalent positions with lattice sites as 

141 entries instead of fractional coordinates. 

142 

143 Parameters 

144 ---------- 

145 structure 

146 Primitive structure. 

147 matrix_of_equivalent_positions 

148 Matrix of equivalent positions with fractional coordinates format entries. 

149 fractional_position_tolerance 

150 Tolerance applied when evaluating distances in fractional coordinates. 

151 prune 

152 If ``True`` the matrix of equivalent positions will be pruned. 

153 

154 Returns 

155 ------- 

156 Matrix of equivalent positions in row major order with entries in lattice site format. 

157 """ 

158 eqpos_frac = matrix_of_equivalent_positions.get_equivalent_positions() 

159 

160 eqpos_lattice_sites = [] 

161 for row in eqpos_frac: 

162 positions = _fractional_to_cartesian(row, structure.cell) 

163 lattice_sites = [] 

164 if np.all(structure.pbc): 164 ↛ 168line 164 didn't jump to line 168 because the condition on line 164 was always true

165 lattice_sites = structure.find_lattice_sites_by_positions( 

166 positions=positions, fractional_position_tolerance=fractional_position_tolerance) 

167 else: 

168 raise ValueError('Input structure must have periodic boundary conditions.') 

169 if lattice_sites is not None: 169 ↛ 172line 169 didn't jump to line 172 because the condition on line 169 was always true

170 eqpos_lattice_sites.append(lattice_sites) 

171 else: 

172 logger.warning('Unable to transform any element in a column of the' 

173 ' fractional matrix of equivalent positions to lattice site') 

174 if prune: 174 ↛ 183line 174 didn't jump to line 183 because the condition on line 174 was always true

175 logger.debug('Size of columns of the matrix of equivalent positions before' 

176 ' pruning {}'.format(len(eqpos_lattice_sites))) 

177 

178 eqpos_lattice_sites = _prune_matrix_of_equivalent_positions(eqpos_lattice_sites) 

179 

180 logger.debug('Size of columns of the matrix of equivalent positions after' 

181 ' pruning {}'.format(len(eqpos_lattice_sites))) 

182 

183 return eqpos_lattice_sites 

184 

185 

186def _prune_matrix_of_equivalent_positions(matrix_of_equivalent_positions: list[list[LatticeSite]]): 

187 """ 

188 Prunes the matrix so that the first column only contains unique elements. 

189 

190 Parameters 

191 ---------- 

192 matrix_of_equivalent_positions 

193 Permutation matrix with :class:`LatticeSite` type entries. 

194 """ 

195 

196 for i in range(len(matrix_of_equivalent_positions)): 

197 for j in reversed(range(len(matrix_of_equivalent_positions))): 

198 if j <= i: 

199 continue 

200 if matrix_of_equivalent_positions[i][0] == matrix_of_equivalent_positions[j][0]: 

201 matrix_of_equivalent_positions.pop(j) 

202 logger.debug('Removing duplicate in matrix of equivalent positions' 

203 'i: {} j: {}'.format(i, j)) 

204 return matrix_of_equivalent_positions 

205 

206 

207def _fractional_to_cartesian(fractional_coordinates: list[list[float]], 

208 cell: np.ndarray) -> list[float]: 

209 """ 

210 Converts cell metrics from fractional to Cartesian coordinates. 

211 

212 Parameters 

213 ---------- 

214 fractional_coordinates 

215 List of fractional coordinates. 

216 cell 

217 Cell metric. 

218 """ 

219 cartesian_coordinates = [np.dot(frac, cell) 

220 for frac in fractional_coordinates] 

221 return cartesian_coordinates