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

72 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-12-26 04:12 +0000

1""" 

2This module provides a Python interface to the MatrixOfEquivalentPositions 

3class with supplementary functions. 

4""" 

5 

6from typing import List, Tuple 

7 

8import numpy as np 

9import spglib 

10 

11from ase import Atoms 

12from icet.core.lattice_site import LatticeSite 

13from icet.core.neighbor_list import get_neighbor_lists 

14from icet.core.structure import Structure 

15from icet.input_output.logging_tools import logger 

16from icet.tools.geometry import (ase_atoms_to_spglib_cell, 

17 get_fractional_positions_from_neighbor_list, 

18 get_primitive_structure) 

19 

20logger = logger.getChild('matrix_of_equivalent_positions') 

21 

22 

23class MatrixOfEquivalentPositions: 

24 """ 

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

26 elements of an atomic structure. 

27 

28 Note 

29 ---- 

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

31 

32 Parameters 

33 ---------- 

34 translations 

35 Translational symmetry elements. 

36 rotations 

37 Rotational symmetry elements. 

38 """ 

39 

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

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

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

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

44 self.n_symmetries = len(rotations) 

45 self.translations = np.array(translations) 

46 self.rotations = np.array(rotations) 

47 

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

49 """ 

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

51 coordinates using the rotational and translational symmetries provided upon 

52 initialization of the object. 

53 

54 Parameters 

55 ---------- 

56 fractional_positions 

57 Atomic positions in fractional coordinates. 

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

59 """ 

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

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

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

63 positions += translations 

64 self.positions = positions 

65 

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

67 """ 

68 Returns the matrix of equivalent positions. Each row corresponds 

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

70 first column is commonly treated as the representative position. 

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

72 """ 

73 return self.positions 

74 

75 

76def matrix_of_equivalent_positions_from_structure(structure: Atoms, 

77 cutoff: float, 

78 position_tolerance: float, 

79 symprec: float, 

80 find_primitive: bool = True) \ 

81 -> Tuple[np.ndarray, Structure, List]: 

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

83 

84 Parameters 

85 ---------- 

86 structure 

87 Input structure. 

88 cutoff 

89 Cutoff radius. 

90 find_primitive 

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

92 symprec 

93 Tolerance imposed when analyzing the symmetry using spglib. 

94 position_tolerance 

95 Tolerance applied when comparing positions in Cartesian coordinates. 

96 

97 Returns 

98 ------- 

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

100 the primitive structure, and the neighbor list. 

101 """ 

102 

103 structure = structure.copy() 

104 structure_prim = structure 

105 if find_primitive: 

106 structure_prim = get_primitive_structure(structure, symprec=symprec) 

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

108 

109 # get symmetry information 

110 structure_as_tuple = ase_atoms_to_spglib_cell(structure_prim) 

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

112 translations = symmetry['translations'] 

113 rotations = symmetry['rotations'] 

114 

115 # set up a MatrixOfEquivalentPositions object 

116 matrix_of_equivalent_positions = MatrixOfEquivalentPositions(translations, rotations) 

117 

118 # create neighbor lists 

119 prim_icet_structure = Structure.from_atoms(structure_prim) 

120 

121 neighbor_list = get_neighbor_lists(prim_icet_structure, 

122 [cutoff], 

123 position_tolerance=position_tolerance)[0] 

124 

125 # get fractional positions for neighbor_list 

126 frac_positions = get_fractional_positions_from_neighbor_list( 

127 prim_icet_structure, neighbor_list) 

128 

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

130 if frac_positions is not None: 130 ↛ 133line 130 didn't jump to line 133, because the condition on line 130 was never false

131 matrix_of_equivalent_positions.build(frac_positions) 

132 

133 return matrix_of_equivalent_positions, prim_icet_structure, neighbor_list 

134 

135 

136def _get_lattice_site_matrix_of_equivalent_positions( 

137 structure: Structure, 

138 matrix_of_equivalent_positions: MatrixOfEquivalentPositions, 

139 fractional_position_tolerance: float, 

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

141 """ 

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

143 entries instead of fractional coordinates. 

144 

145 Parameters 

146 ---------- 

147 structure 

148 Primitive structure. 

149 matrix_of_equivalent_positions 

150 Matrix of equivalent positions with fractional coordinates format entries. 

151 fractional_position_tolerance 

152 Tolerance applied when evaluating distances in fractional coordinates. 

153 prune 

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

155 

156 Returns 

157 ------- 

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

159 """ 

160 eqpos_frac = matrix_of_equivalent_positions.get_equivalent_positions() 

161 

162 eqpos_lattice_sites = [] 

163 for row in eqpos_frac: 

164 positions = _fractional_to_cartesian(row, structure.cell) 

165 lattice_sites = [] 

166 if np.all(structure.pbc): 166 ↛ 170line 166 didn't jump to line 170, because the condition on line 166 was never false

167 lattice_sites = structure.find_lattice_sites_by_positions( 

168 positions=positions, fractional_position_tolerance=fractional_position_tolerance) 

169 else: 

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

171 if lattice_sites is not None: 171 ↛ 174line 171 didn't jump to line 174, because the condition on line 171 was never false

172 eqpos_lattice_sites.append(lattice_sites) 

173 else: 

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

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

176 if prune: 176 ↛ 185line 176 didn't jump to line 185, because the condition on line 176 was never false

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

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

179 

180 eqpos_lattice_sites = _prune_matrix_of_equivalent_positions(eqpos_lattice_sites) 

181 

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

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

184 

185 return eqpos_lattice_sites 

186 

187 

188def _prune_matrix_of_equivalent_positions(matrix_of_equivalent_positions: List[List[LatticeSite]]): 

189 """ 

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

191 

192 Parameters 

193 ---------- 

194 matrix_of_equivalent_positions 

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

196 """ 

197 

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

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

200 if j <= i: 

201 continue 

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

203 matrix_of_equivalent_positions.pop(j) 

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

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

206 return matrix_of_equivalent_positions 

207 

208 

209def _fractional_to_cartesian(fractional_coordinates: List[List[float]], 

210 cell: np.ndarray) -> List[float]: 

211 """ 

212 Converts cell metrics from fractional to Cartesian coordinates. 

213 

214 Parameters 

215 ---------- 

216 fractional_coordinates 

217 List of fractional coordinates. 

218 cell 

219 Cell metric. 

220 """ 

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

222 for frac in fractional_coordinates] 

223 return cartesian_coordinates