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

74 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3import itertools 

4 

5import numpy as np 

6 

7from ase.cell import Cell 

8from ase.lattice import UnconventionalLattice, bravais_lattices, bravais_names 

9 

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

11 

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

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

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

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

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

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

18 

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

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

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

22recognizable one. Knowing all five operations (or equivalence 

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

24other lattices of sufficiently low dimension. 

25 

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

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

28 

29 

30niggli_op_table = { # Generated by generate_niggli_op_table() 

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

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

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

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

35 (1, 0, 0, 0, -1, 0, 0, 0, -1)], 

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

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

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

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

40 (-1, -1, 0, 1, 0, 0, 0, 0, 1), 

41 (1, 1, 0, 0, -1, 0, 0, 0, -1)], 

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

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

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

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

46 (-1, 0, 1, 0, 1, 0, -1, 1, 0), 

47 (1, 1, 0, 1, 0, 0, 0, 0, -1)], 

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

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

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

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

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

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

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

55 (0, 1, 0, 0, 0, 1, 1, 0, 0), 

56 (0, -1, 1, 0, -1, 0, 1, 0, 0)], 

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

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

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

60 (0, 1, 0, 1, 0, 0, 1, 1, -1), 

61 (0, -1, 0, 1, 0, -1, 1, -1, 0)], 

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

63 (1, 0, 0, 0, 1, 0, 0, 0, 1), 

64 (1, -1, 0, 1, 0, -1, 1, 0, 0)], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

79 (1, 0, 0, 0, -1, -1, 0, 1, 0), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

106 (-1, -1, 1, 1, -1, 0, 0, 1, 0), 

107 (1, 1, 0, -1, 0, -1, 0, 0, 1)], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

125 (0, 0, -1, -1, 0, 0, 0, 1, 0), 

126 (-1, -1, -1, 0, 0, 1, 0, 1, 0)], 

127} 

128 

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

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

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

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

133# 

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

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

136 

137 

138def lattice_loop(latcls, length_grid, angle_grid): 

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

140 param_grids = [] 

141 for varname in latcls.parameters: 

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

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

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

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

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

147 # thing in order not to worry. 

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

149 special_var = 'c' 

150 else: 

151 special_var = 'a' 

152 if varname == special_var: 

153 values = np.ones(1) 

154 elif varname in 'abc': 

155 values = length_grid 

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

157 values = angle_grid 

158 else: 

159 raise ValueError(varname) 

160 param_grids.append(values) 

161 

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

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

164 try: 

165 lat = latcls(**kwargs) 

166 except (UnconventionalLattice, AssertionError): 

167 # XXX assertion error can happen because cellpar_to_cell 

168 # makes certain assumptions. Should be investigated. 

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

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

171 pass 

172 else: 

173 yield lat 

174 

175 

176def find_niggli_ops(latcls, length_grid, angle_grid): 

177 niggli_ops = {} 

178 

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

180 cell = lat.tocell() 

181 

182 try: 

183 rcell, op = cell.niggli_reduce() 

184 except RuntimeError: 

185 print('Niggli reduce did not converge') 

186 continue 

187 assert op.dtype == int 

188 op_key = tuple(op.ravel()) 

189 

190 if op_key in niggli_ops: 

191 niggli_ops[op_key] += 1 

192 else: 

193 niggli_ops[op_key] = 1 

194 

195 rcell_test = Cell(op.T @ cell) 

196 rcellpar_test = rcell_test.cellpar() 

197 rcellpar = rcell.cellpar() 

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

199 assert err < 1e-7, err 

200 

201 return niggli_ops 

202 

203 

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

205 all_niggli_ops = {} 

206 if lattices is None: 

207 lattices = [name for name in bravais_names 

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

209 

210 for latname in lattices: 

211 latcls = bravais_lattices[latname] 

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

213 niggli_ops = find_niggli_ops(latcls, length_grid, angle_grid) 

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

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

216 print(f' {np.array(key)!s:>40}: {count}') 

217 print() 

218 all_niggli_ops[latname] = niggli_ops 

219 return all_niggli_ops 

220 

221 

222def generate_niggli_op_table(lattices=None, 

223 length_grid=None, 

224 angle_grid=None): 

225 

226 if length_grid is None: 

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

228 if angle_grid is None: 

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

230 all_niggli_ops_and_counts = find_all_niggli_ops(length_grid, angle_grid, 

231 lattices=lattices) 

232 

233 niggli_op_table = {} 

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

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

236 niggli_op_table[latname] = ops 

237 

238 import pprint 

239 print(pprint.pformat(niggli_op_table)) 

240 return niggli_op_table 

241 

242 

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

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

245# found also in 6e2b1c6cae0ae6ee04638a9887821e7b1a1f2f3f .