Ewald 1D and 2D in 3D notes formulae linewraps
[qpms.git] / qpms / lattices2d.py
blobeb5f68bf10233566b567e8ddfb9e34f11c0ef10c
1 import numpy as np
2 from enum import Enum
3 from math import floor
5 nx = None
7 class LatticeType(Enum):
8 """
9 All the five Bravais lattices in 2D
10 """
11 OBLIQUE=1
12 RECTANGULAR=2
13 SQUARE=4
14 RHOMBIC=5
15 EQUILATERAL_TRIANGULAR=3
16 RIGHT_ISOSCELES=SQUARE
17 PARALLELOGRAMMIC=OBLIQUE
18 CENTERED_RHOMBIC=RECTANGULAR
19 RIGHT_TRIANGULAR=RECTANGULAR
20 CENTERED_RECTANGULAR=RHOMBIC
21 ISOSCELE_TRIANGULAR=RHOMBIC
22 RIGHT_ISOSCELE_TRIANGULAR=SQUARE
23 HEXAGONAL=EQUILATERAL_TRIANGULAR
25 def reduceBasisSingle(b1, b2):
26 """
27 Lagrange-Gauss reduction of a 2D basis.
28 cf. https://www.math.auckland.ac.nz/~sgal018/crypto-book/ch17.pdf
29 inputs and outputs are (2,)-shaped numpy arrays
30 The output shall satisfy |b1| <= |b2| <= |b2 - b1|
31 TODO doc
33 TODO perhaps have the (on-demand?) guarantee of obtuse angle between b1, b2?
34 TODO possibility of returning the (in-order, no-obtuse angles) b as well?
35 """
36 b1 = np.array(b1)
37 b2 = np.array(b2)
38 if b1.shape != (2,) or b2.shape != (2,):
39 raise ValueError('Shape of b1 and b2 must be (2,)')
40 B1 = np.sum(b1 * b1, axis=-1, keepdims=True)
41 mu = np.sum(b1 * b2, axis=-1, keepdims=True) / B1
42 b2 = b2 - np.rint(mu) * b1
43 B2 = np.sum(b2 * b2, axis=-1, keepdims=True)
44 while(np.any(B2 < B1)):
45 b2t = b1
46 b1 = b2
47 b2 = b2t
48 B1 = B2
49 mu = np.sum(b1 * b2, axis=-1, keepdims=True) / B1
50 b2 = b2 - np.rint(mu) * b1
51 B2 = np.sum(b2*b2, axis=-1, keepdims=True)
52 return np.array((b1,b2))
54 def shortestBase3(b1, b2):
55 '''
56 returns the "ordered shortest triple" of base vectors (each pair from
57 the triple is a base) and there may not be obtuse angle between b1, b2
58 and between b2, b3
59 '''
60 b1, b2 = reduceBasisSingle(b1,b2)
61 if is_obtuse(b1, b2, tolerance=0):
62 b3 = b2
63 b2 = b2 + b1
64 else:
65 b3 = b2 - b1
66 return (b1, b2, b3)
68 def shortestBase46(b1, b2, tolerance=1e-13):
69 b1, b2 = reduceBasisSingle(b1,b2)
70 b1s = np.sum(b1 ** 2)
71 b2s = np.sum(b2 ** 2)
72 b3 = b2 - b1
73 b3s = np.sum(b3 ** 2)
74 eps = tolerance * (b2s + b1s)
75 if abs(b3s - b2s - b1s) < eps:
76 return(b1, b2, -b1, -b2)
77 else:
78 if b3s - b2s - b1s > eps: #obtuse
79 b3 = b2
80 b2 = b2 + b1
81 return (b1, b2, b3, -b1, -b2, -b3)
84 def is_obtuse(b1, b2, tolerance=1e-13):
85 b1s = np.sum(b1 ** 2)
86 b2s = np.sum(b2 ** 2)
87 b3 = b2 - b1
88 b3s = np.sum(b3 ** 2)
89 eps = tolerance * (b2s + b1s)
90 return (b3s - b2s - b1s > eps)
92 def classifyLatticeSingle(b1, b2, tolerance=1e-13):
93 """
94 Given two basis vectors, returns 2D Bravais lattice type.
95 Tolerance is relative.
96 TODO doc
97 """
98 b1, b2 = reduceBasisSingle(b1, b2)
99 b1s = np.sum(b1 ** 2)
100 b2s = np.sum(b2 ** 2)
101 b3 = b2 - b1
102 b3s = np.sum(b3 ** 2)
103 eps = tolerance * (b2s + b1s)
104 # Avoid obtuse angle between b1 and b2. TODO This should be yet thoroughly tested.
105 # TODO use is_obtuse here?
106 if b3s - b2s - b1s > eps:
107 b3 = b2
108 b2 = b2 + b1
109 # N. B. now the assumption |b3| >= |b2| is no longer valid
110 #b3 = b2 - b1
111 b2s = np.sum(b2 ** 2)
112 b3s = np.sum(b3 ** 2)
113 if abs(b2s - b1s) < eps or abs(b2s - b3s) < eps: # isoscele
114 if abs(b3s - b1s) < eps:
115 return LatticeType.EQUILATERAL_TRIANGULAR
116 elif abs(b3s - 2 * b1s) < eps:
117 return LatticeType.SQUARE
118 else:
119 return LatticeType.RHOMBIC
120 elif abs(b3s - b2s - b1s) < eps:
121 return LatticeType.RECTANGULAR
122 else:
123 return LatticeType.OBLIQUE
125 def range2D(maxN, mini=1, minj=0, minN = 0):
127 "Triangle indices"
128 Generates pairs of non-negative integer indices (i, j) such that
129 minN ≤ i + j ≤ maxN, i ≥ mini, j ≥ minj.
130 TODO doc and possibly different orderings
132 for maxn in range(min(mini, minj, minN), floor(maxN+1)): # i + j == maxn
133 for i in range(mini, maxn + 1):
134 yield (i, maxn - i)
137 def generateLattice(b1, b2, maxlayer=5, include_origin=False, order='leaves'):
138 bvs = shortestBase46(b1, b2)
139 cc = len(bvs) # "corner count"
141 if order == 'leaves':
142 indices = np.array(list(range2D(maxlayer)))
143 ia = indices[:,0]
144 ib = indices[:,1]
145 cc = len(bvs) # 4 for square/rec,
146 leaves = list()
147 if include_origin: leaves.append(np.array([[0,0]]))
148 for c in range(cc):
149 ba = bvs[c]
150 bb = bvs[(c+1)%cc]
151 leaves.append(ia[:,nx]*ba + ib[:,nx]*bb)
152 return np.concatenate(leaves)
153 else:
154 raise ValueError('Lattice point order not implemented: ', order)
156 def generateLatticeDisk(b1, b2, r, include_origin=False, order='leaves'):
157 b1, b2 = reduceBasisSingle(b1,b2)
158 blen = np.linalg.norm(b1, ord=2)
159 maxlayer = 2*r/blen # FIXME kanon na vrabce? Nestačí odmocnina ze 2?
160 points = generateLattice(b1,b2, maxlayer=maxlayer, include_origin=include_origin, order=order)
161 mask = (np.linalg.norm(points, axis=-1, ord=2) <= r)
162 return points[mask]
164 def cellCornersWS(b1, b2,):
166 Given basis vectors, returns the corners of the Wigner-Seitz unit cell
167 (w1, w2, -w1, w2) for rectangular and square lattice or
168 (w1, w2, w3, -w1, -w2, -w3) otherwise
170 def solveWS(v1, v2):
171 v1x = v1[0]
172 v1y = v1[1]
173 v2x = v2[0]
174 v2y = v2[1]
175 lsm = ((-v1y, v2y), (v1x, -v2x))
176 rs = ((v1x-v2x)/2, (v1y - v2y)/2)
177 t = np.linalg.solve(lsm, rs)
178 return np.array(v1)/2 + t[0]*np.array((v1y, -v1x))
179 b1, b2 = reduceBasisSingle(b1, b2)
180 latticeType = classifyLatticeSingle(b1, b2)
181 if latticeType is LatticeType.RECTANGULAR or latticeType is LatticeType.SQUARE:
182 return np.array( (
183 (+b1+b2),
184 (+b2-b1),
185 (-b1-b2),
186 (-b2+b1),
187 )) / 2
188 else:
189 bvs = shortestBase46(b1,b2,tolerance=0)
190 return np.array([solveWS(bvs[i], bvs[(i+1)%6]) for i in range(6)])
192 def cutWS(points, b1, b2, scale=1., tolerance=1e-13):
193 """
194 From given points, return only those that are inside (or on the edge of)
195 the Wigner-Seitz cell of a (scale*b1, scale*b2)-based lattice.
197 # TODO check input dimensions?
198 bvs = shortestBase46(b1, b2)
199 points = np.array(points)
200 for b in bvs:
201 mask = (np.tensordot(points, b, axes=(-1, 0)) <= (scale * (1+tolerance) / 2) *np.linalg.norm(b, ord=2)**2 )
202 points = points[mask]
203 return points
205 def filledWS(b1, b2, density=10, scale=1.):
207 TODO doc
208 TODO more intelligent generation, anisotropy balancing etc.
210 points = generateLattice(b1,b2,maxlayer=density*scale, include_origin=True)
211 points = cutWS(points/density, np.array(b1)*scale, np.array(b2)*scale)
212 return points
215 def reciprocalBasis1(*pargs):
216 a = reduceBasisSingle(*pargs)
217 return np.linalg.inv(a).T
219 def reciprocalBasis2pi(*pargs):
220 return 2*np.pi*reciprocalBasis1(*pargs)
222 # TODO fill it with "points from reciprocal space" instead
223 def filledWS2(b1,b2, density=10, scale=1.):
224 b1, b2 = reduceBasisSingle(b1,b2)
225 b1r, b2r = reciprocalBasis2pi(b1,b2)
226 b1l = np.linalg.norm(b1, ord=2)
227 b2l = np.linalg.norm(b2, ord=2)
228 b1rl = np.linalg.norm(b1r, ord=2)
229 b2rl = np.linalg.norm(b2r, ord=2)
230 # Black magick. Think later.™ Really. FIXME
231 sicher_ratio = np.maximum(b1rl/b2rl, b2rl/b1rl) * np.maximum(b1l/b2l, b2l/b1l) # This really has to be adjusted
232 points = generateLattice(b1r,b2r,maxlayer=density*scale*sicher_ratio, include_origin=True)
233 points = cutWS(points*b1l/b1rl/density, b1*scale, b2*scale)
234 return points
237 def change_basis(srcbasis, destbasis, srccoords, srccoordsaxis=-1, lattice=True):
238 srcbasis = np.array(srcbasis)
239 destbasis = np.array(destbasis)
240 trmatrix = np.dot(np.linalg.inv(np.transpose(destbasis)), np.transpose(srcbasis))
241 if lattice: # if srcbasis and destbasis are two bases of the same lattice, its elements are ints
242 otrmatrix = trmatrix
243 trmatrix = np.round(trmatrix)
244 if not np.all(np.isclose(trmatrix, otrmatrix)):
245 raise ValueError("Given srcbasis and destbasis are not bases"
246 "of the same lattice", srcbasis, destbasis, trmatrix-otrmatrix)
247 destcoords = np.tensordot(srccoords, trmatrix, axes=(srccoordsaxis, -1))
248 return destcoords
251 TODO
252 ====
254 - DOC!!!!!
255 - (nehoří) výhledově pořešit problém „hodně anisotropních“ mřížek (tj. kompensovat
256 rozdílné délky základních vektorů).