Coverage for /builds/kinetik161/ase/ase/geometry/bravais_type_engine.py: 89.19%

74 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1import itertools 

2 

3import numpy as np 

4 

5from ase.cell import Cell 

6from ase.lattice import UnconventionalLattice, bravais_lattices, bravais_names 

7 

8"""This module implements a crude method to recognize most Bravais lattices. 

9 

10Suppose we use the ase.lattice module to generate many 

11lattices of some particular type, say, BCT(a, c), and then we 

12Niggli-reduce all of them. The Niggli-reduced forms are not 

13immediately recognizable, but we know the mapping from each reduced 

14form back to the original form. As it turns out, there are apparently 

155 such mappings (the proof is left as an exercise for the reader). 

16 

17Hence, presumably, every BCT lattice (whether generated by BCT(a, c) 

18or in some other form) Niggli-reduces to a form which, through the 

19inverse of one of those five operations, is mapped back to the 

20recognizable one. Knowing all five operations (or equivalence 

21classes), we can characterize any BCT lattice. Same goes for the 

22other lattices of sufficiently low dimension. 

23 

24For MCL, MCLC, and TRI, we may not recognize all forms correctly, 

25but we aspire that this will work for all common inputs.""" 

26 

27 

28niggli_op_table = { # Generated by generate_niggli_op_table() 

29 'LINE': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

30 'SQR': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

31 'RECT': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

32 'CRECT': [(-1, 1, 0, 1, 0, 0, 0, 0, -1), 

33 (1, 0, 0, 0, -1, 0, 0, 0, -1)], 

34 'HEX2D': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

35 'OBL': [(-1, 1, 0, 1, 0, 0, 0, 0, -1), 

36 (1, -1, 0, 0, 1, 0, 0, 0, 1), 

37 (1, 0, 0, 0, -1, 0, 0, 0, -1), 

38 (-1, -1, 0, 1, 0, 0, 0, 0, 1), 

39 (1, 1, 0, 0, -1, 0, 0, 0, -1)], 

40 'BCC': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

41 'BCT': [(1, 0, 0, 0, 1, 0, 0, 0, 1), 

42 (0, 1, 0, 0, 0, 1, 1, 0, 0), 

43 (0, 1, 0, 1, 0, 0, 1, 1, -1), 

44 (-1, 0, 1, 0, 1, 0, -1, 1, 0), 

45 (1, 1, 0, 1, 0, 0, 0, 0, -1)], 

46 'CUB': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

47 'FCC': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

48 'HEX': [(1, 0, 0, 0, 1, 0, 0, 0, 1), (0, 1, 0, 0, 0, 1, 1, 0, 0)], 

49 'ORC': [(1, 0, 0, 0, 1, 0, 0, 0, 1)], 

50 'ORCC': [(1, 0, 0, 0, 1, 0, 0, 0, 1), 

51 (1, 0, -1, 1, 0, 0, 0, -1, 0), 

52 (-1, 1, 0, -1, 0, 0, 0, 0, 1), 

53 (0, 1, 0, 0, 0, 1, 1, 0, 0), 

54 (0, -1, 1, 0, -1, 0, 1, 0, 0)], 

55 'ORCF': [(0, -1, 0, 0, 1, -1, 1, 0, 0), (-1, 0, 0, 1, 0, 1, 1, 1, 0)], 

56 'ORCI': [(0, 0, -1, 0, -1, 0, -1, 0, 0), 

57 (0, 0, 1, -1, 0, 0, -1, -1, 0), 

58 (0, 1, 0, 1, 0, 0, 1, 1, -1), 

59 (0, -1, 0, 1, 0, -1, 1, -1, 0)], 

60 'RHL': [(0, -1, 0, 1, 1, 1, -1, 0, 0), 

61 (1, 0, 0, 0, 1, 0, 0, 0, 1), 

62 (1, -1, 0, 1, 0, -1, 1, 0, 0)], 

63 'TET': [(1, 0, 0, 0, 1, 0, 0, 0, 1), (0, 1, 0, 0, 0, 1, 1, 0, 0)], 

64 'MCL': [(0, 0, 1, -1, -1, 0, 1, 0, 0), 

65 (-1, 0, 0, 0, 1, 0, 0, 0, -1), 

66 (0, 0, -1, 1, 1, 0, 0, -1, 0), 

67 (0, -1, 0, 1, 0, 1, -1, 0, 0), 

68 (0, 1, 0, -1, 0, -1, 0, 0, 1), 

69 (-1, 0, 0, 0, 1, 1, 0, 0, -1), 

70 (0, 1, 0, 1, 0, -1, -1, 0, 0), 

71 (0, 0, 1, 1, -1, 0, 0, 1, 0), 

72 (0, 1, 0, -1, 0, 0, 0, 0, 1), 

73 (0, 0, -1, -1, 1, 0, 1, 0, 0), 

74 (1, 0, 0, 0, 1, -1, 0, 0, 1), 

75 (0, -1, 0, -1, 0, 1, 0, 0, -1), 

76 (-1, 0, 0, 0, -1, 1, 0, 1, 0), 

77 (1, 0, 0, 0, -1, -1, 0, 1, 0), 

78 (0, 0, -1, 1, 0, 0, 0, -1, 0)], 

79 'MCLC': [(1, 1, 1, 1, 0, 1, 0, 0, -1), 

80 (1, 1, 1, 1, 1, 0, -1, 0, 0), 

81 (1, -1, 1, -1, 0, 1, 0, 0, -1), 

82 (-1, 1, 0, 1, 0, 0, 0, 0, -1), 

83 (1, 0, 0, 0, 1, 0, 0, 0, 1), 

84 (-1, 0, -1, 1, -1, -1, 0, 0, 1), 

85 (1, -1, -1, 1, -1, 0, -1, 0, 0), 

86 (-1, -1, 0, -1, 0, -1, 1, 0, 0), 

87 (1, 0, 1, 1, 0, 0, 0, 1, 0), 

88 (-1, 1, 0, -1, 0, 1, 1, 0, 0), 

89 (0, -1, 1, -1, 0, 1, 0, 0, -1), 

90 (-1, -1, 0, -1, 0, 0, 0, 0, -1), 

91 (-1, -1, 1, -1, 0, 1, 0, 0, -1), 

92 (1, 0, 0, 0, -1, 1, 0, 0, -1), 

93 (-1, 0, -1, 0, -1, -1, 0, 0, 1), 

94 (1, 0, -1, -1, 1, -1, 0, 0, 1), 

95 (1, -1, 1, 1, -1, 0, 0, 1, 0), 

96 (0, -1, 0, 1, 0, -1, 0, 0, 1), 

97 (-1, 0, 0, 1, 1, 1, 0, 0, -1), 

98 (1, 0, -1, 0, 1, -1, 0, 0, 1), 

99 (-1, 1, 0, 1, 1, -1, 0, -1, 0), 

100 (1, 1, -1, 1, -1, 0, -1, 0, 0), 

101 (-1, -1, -1, -1, -1, 0, 0, 1, 0), 

102 (-1, 1, 1, 1, 0, 1, 0, 0, -1), 

103 (-1, 0, 0, 0, -1, 0, 0, 0, 1), 

104 (-1, -1, 1, 1, -1, 0, 0, 1, 0), 

105 (1, 1, 0, -1, 0, -1, 0, 0, 1)], 

106 'TRI': [(0, -1, 0, -1, 0, 0, 0, 0, -1), 

107 (0, 1, 0, 0, 0, 1, 1, 0, 0), 

108 (0, 0, -1, 0, -1, 0, -1, 1, 0), 

109 (0, 0, 1, 0, 1, 0, -1, 0, 0), 

110 (0, -1, 0, 0, 0, -1, 1, 1, 1), 

111 (0, 1, 0, 0, 0, 1, 1, -1, 0), 

112 (0, 0, -1, 0, -1, 0, -1, 0, 0), 

113 (-1, 1, 0, 0, 0, -1, 0, -1, 0), 

114 (0, 0, 1, 1, -1, 0, 0, 1, 0), 

115 (0, 0, -1, 1, 1, 1, 0, -1, 0), 

116 (-1, 0, 0, 0, 1, 0, 0, -1, -1), 

117 (0, 0, 1, 1, 0, 0, 0, 1, 0), 

118 (0, 0, 1, 0, 1, 0, -1, -1, -1), 

119 (-1, 0, 0, 0, 0, -1, 0, -1, 0), 

120 (0, -1, 0, 0, 0, -1, 1, 0, 0), 

121 (1, 0, 0, 0, 1, 0, 0, 0, 1), 

122 (0, 0, -1, -1, 0, 0, 1, 1, 1), 

123 (0, 0, -1, -1, 0, 0, 0, 1, 0), 

124 (-1, -1, -1, 0, 0, 1, 0, 1, 0)], 

125} 

126 

127# XXX The TRI list was generated by looping over all TRI structures in 

128# the COD (Crystallography Open Database) and seeing what operations 

129# were necessary to map all those to standard form. Hence if the 

130# data does not cover all possible inputs, we could miss something. 

131# 

132# Looping over all possible TRI lattices in general would generate 

133# 100+ operations, we don't want to tabulate that. 

134 

135 

136def lattice_loop(latcls, length_grid, angle_grid): 

137 """Yield all lattices defined by the length and angle grids.""" 

138 param_grids = [] 

139 for varname in latcls.parameters: 

140 # Actually we could choose one parameter, a, to always be 1, 

141 # reducing the dimension of the problem by 1. The lattice 

142 # recognition code should do something like that as well, but 

143 # it doesn't. This could affect the impact of the eps value 

144 # on lattice determination, so we just loop over the whole 

145 # thing in order not to worry. 

146 if latcls.name in ['MCL', 'MCLC']: 

147 special_var = 'c' 

148 else: 

149 special_var = 'a' 

150 if varname == special_var: 

151 values = np.ones(1) 

152 elif varname in 'abc': 

153 values = length_grid 

154 elif varname in ['alpha', 'beta', 'gamma']: 

155 values = angle_grid 

156 else: 

157 raise ValueError(varname) 

158 param_grids.append(values) 

159 

160 for latpars in itertools.product(*param_grids): 

161 kwargs = dict(zip(latcls.parameters, latpars)) 

162 try: 

163 lat = latcls(**kwargs) 

164 except (UnconventionalLattice, AssertionError): 

165 # XXX assertion error can happen because cellpar_to_cell 

166 # makes certain assumptions. Should be investigated. 

167 # {'b': 0.1, 'gamma': 60.0, 'c': 0.1, 'a': 1.0, 

168 # 'alpha': 30.0, 'beta': 30.0} <-- this won't work 

169 pass 

170 else: 

171 yield lat 

172 

173 

174def find_niggli_ops(latcls, length_grid, angle_grid): 

175 niggli_ops = {} 

176 

177 for lat in lattice_loop(latcls, length_grid, angle_grid): 

178 cell = lat.tocell() 

179 

180 try: 

181 rcell, op = cell.niggli_reduce() 

182 except RuntimeError: 

183 print('Niggli reduce did not converge') 

184 continue 

185 assert op.dtype == int 

186 op_key = tuple(op.ravel()) 

187 

188 if op_key in niggli_ops: 

189 niggli_ops[op_key] += 1 

190 else: 

191 niggli_ops[op_key] = 1 

192 

193 rcell_test = Cell(op.T @ cell) 

194 rcellpar_test = rcell_test.cellpar() 

195 rcellpar = rcell.cellpar() 

196 err = np.abs(rcellpar_test - rcellpar).max() 

197 assert err < 1e-7, err 

198 

199 return niggli_ops 

200 

201 

202def find_all_niggli_ops(length_grid, angle_grid, lattices=None): 

203 all_niggli_ops = {} 

204 if lattices is None: 

205 lattices = [name for name in bravais_names 

206 if name not in ['MCL', 'MCLC', 'TRI']] 

207 

208 for latname in lattices: 

209 latcls = bravais_lattices[latname] 

210 print(f'Working on {latname}...') 

211 niggli_ops = find_niggli_ops(latcls, length_grid, angle_grid) 

212 print(f'Found {len(niggli_ops)} ops for {latname}') 

213 for key, count in niggli_ops.items(): 

214 print(f' {str(np.array(key)):>40}: {count}') 

215 print() 

216 all_niggli_ops[latname] = niggli_ops 

217 return all_niggli_ops 

218 

219 

220def generate_niggli_op_table(lattices=None, 

221 length_grid=None, 

222 angle_grid=None): 

223 

224 if length_grid is None: 

225 length_grid = np.logspace(-0.5, 1.5, 50).round(3) 

226 if angle_grid is None: 

227 angle_grid = np.linspace(10, 179, 50).round() 

228 all_niggli_ops_and_counts = find_all_niggli_ops(length_grid, angle_grid, 

229 lattices=lattices) 

230 

231 niggli_op_table = {} 

232 for latname, ops in all_niggli_ops_and_counts.items(): 

233 ops = [op for op in ops if np.abs(op).max() < 2] 

234 niggli_op_table[latname] = ops 

235 

236 import pprint 

237 print(pprint.pformat(niggli_op_table)) 

238 return niggli_op_table 

239 

240 

241# For generation of the table, please see the test_bravais_type_engine 

242# unit test. In case there's any trouble, some legacy code can be 

243# found also in 6e2b1c6cae0ae6ee04638a9887821e7b1a1f2f3f .