Statistiques
| Révision :

root / ase / lattice / bravais.py @ 19

Historique | Voir | Annoter | Télécharger (18,3 ko)

1
"""Bravais.py - class for generating Bravais lattices etc.
2

3
This is a base class for numerous classes setting up pieces of crystal.
4
"""
5

    
6
import math
7
import numpy as np
8
from ase.atoms import Atoms
9
import ase.data
10

    
11
class Bravais:
12
    """Bravais lattice factory.
13

14
    This is a base class for the objects producing various lattices
15
    (SC, FCC, ...).
16
    """
17

    
18
    # The following methods are NOT defined here, but must be defined
19
    # in classes inhering from Bravais:
20
    #   get_lattice_constant
21
    #   make_crystal_basis
22
    # The following class attributes are NOT defined here, but must be defined
23
    # in classes inhering from Bravais:
24
    #   int_basis
25
    #   inverse_basis
26

    
27
    other = {0:(1,2), 1:(2,0), 2:(0,1)}
28

    
29
    # For Bravais lattices with a basis, set the basis here.  Leave as
30
    # None if no basis is present.
31
    bravais_basis = None
32

    
33
    # If more than one type of element appear in the crystal, give the
34
    # order here.  For example, if two elements appear in a 3:1 ratio,
35
    # bravais_basis could contain four vectors, and element_basis
36
    # could be (0,0,1,0) - the third atom in the basis is different
37
    # from the other three.  Leave as None if all atoms are of the
38
    # same type.
39
    element_basis = None
40

    
41
    # How small numbers should be considered zero in the unit cell?
42
    chop_tolerance = 1e-10
43

    
44
    def __call__(self, symbol,
45
                 directions=(None,None,None), miller=(None,None,None),
46
                 size=(1,1,1), latticeconstant=None,
47
                 pbc=True, align=True, debug=0):
48
        "Create a lattice."
49
        self.size = size
50
        self.pbc = pbc
51
        self.debug = debug
52
        self.process_element(symbol)
53
        self.find_directions(directions, miller)
54
        if self.debug:
55
            self.print_directions_and_miller()
56
        self.convert_to_natural_basis()
57
        if self.debug >= 2:
58
            self.print_directions_and_miller(" (natural basis)")
59
        if latticeconstant is None:
60
            if self.element_basis is None:
61
                self.latticeconstant = self.get_lattice_constant()
62
            else:
63
                raise ValueError,\
64
                      "A lattice constant must be specified for a compound"
65
        else:
66
            self.latticeconstant = latticeconstant
67
        if self.debug:
68
            print "Expected number of atoms in unit cell:", self.calc_num_atoms()
69
        if self.debug >= 2:
70
            print "Bravais lattice basis:", self.bravais_basis
71
            if self.bravais_basis is not None:
72
                print " ... in natural basis:", self.natural_bravais_basis
73
        self.make_crystal_basis()
74
        self.make_unit_cell()
75
        if align:
76
            self.align()
77
        return self.make_list_of_atoms()
78

    
79
    def align(self):
80
        "Align the first axis along x-axis and the second in the x-y plane."
81
        degree = 180/np.pi
82
        if self.debug >= 2:
83
            print "Basis before alignment:"
84
            print self.basis
85
        if self.basis[0][0]**2 + self.basis[0][2]**2 < 0.01 * self.basis[0][1]**2:
86
            # First basis vector along y axis - rotate 90 deg along z
87
            t = np.array([[0, -1, 0],
88
                          [1, 0, 0],
89
                          [0, 0, 1]], np.float)
90
            self.basis = np.dot(self.basis, t)
91
            transf = t
92
            if self.debug >= 2:
93
                print "Rotating -90 degrees around z axis for numerical stability."
94
                print self.basis
95
        else:
96
            transf = np.identity(3, np.float)
97
        assert abs(np.linalg.det(transf) - 1) < 1e-6
98
        # Rotate first basis vector into xy plane
99
        theta = math.atan2(self.basis[0,2], self.basis[0,0])
100
        t = np.array([[np.cos(theta), 0, -np.sin(theta)],
101
                      [         0, 1, 0          ],
102
                      [np.sin(theta), 0, np.cos(theta) ]])
103
        self.basis = np.dot(self.basis, t)
104
        transf = np.dot(transf, t)
105
        if self.debug >= 2:
106
            print "Rotating %f degrees around y axis." % (-theta*degree,)
107
            print self.basis
108
        assert abs(np.linalg.det(transf) - 1) < 1e-6
109
        # Rotate first basis vector to point along x axis
110
        theta = math.atan2(self.basis[0,1], self.basis[0,0])
111
        t = np.array([[np.cos(theta), -np.sin(theta), 0],
112
                      [np.sin(theta),  np.cos(theta), 0],
113
                      [         0,           0, 1]])
114
        self.basis = np.dot(self.basis, t)
115
        transf = np.dot(transf, t)
116
        if self.debug >= 2:
117
            print "Rotating %f degrees around z axis." % (-theta*degree,)
118
            print self.basis
119
        assert abs(np.linalg.det(transf) - 1) < 1e-6
120
        # Rotate second basis vector into xy plane
121
        theta = math.atan2(self.basis[1,2], self.basis[1,1])
122
        t = np.array([[1, 0, 0],
123
                      [0, np.cos(theta), -np.sin(theta)],
124
                      [0, np.sin(theta),  np.cos(theta)]])
125
        self.basis = np.dot(self.basis, t)
126
        transf = np.dot(transf, t)
127
        if self.debug >= 2:
128
            print "Rotating %f degrees around x axis." % (-theta*degree,)
129
            print self.basis
130
        assert abs(np.linalg.det(transf) - 1) < 1e-6
131
        # Now we better rotate the atoms as well
132
        self.atoms = np.dot(self.atoms, transf)
133
        # ... and rotate miller_basis
134
        self.miller_basis = np.dot(self.miller_basis, transf)
135

    
136
    def make_list_of_atoms(self):
137
        "Repeat the unit cell."
138
        nrep = self.size[0] * self.size[1] * self.size[2]
139
        if nrep <= 0:
140
            raise ValueError, "Cannot create a non-positive number of unit cells"
141
        # Now the unit cells must be merged.
142
        a2 = []
143
        e2 = []
144
        for i in xrange(self.size[0]):
145
            offset = self.basis[0] * i
146
            a2.append(self.atoms + offset[np.newaxis,:])
147
            e2.append(self.elements)
148
        atoms = np.concatenate(a2)
149
        elements = np.concatenate(e2)
150
        a2 = []
151
        e2 = []
152
        for j in xrange(self.size[1]):
153
            offset = self.basis[1] * j
154
            a2.append(atoms + offset[np.newaxis,:])
155
            e2.append(elements)
156
        atoms = np.concatenate(a2)
157
        elements = np.concatenate(e2)
158
        a2 = []
159
        e2 = []
160
        for k in xrange(self.size[2]):
161
            offset = self.basis[2] * k
162
            a2.append(atoms + offset[np.newaxis,:])
163
            e2.append(elements)
164
        atoms = np.concatenate(a2)
165
        elements = np.concatenate(e2)
166
        del a2, e2
167
        assert len(atoms) == nrep * len(self.atoms)
168
        basis = np.array([[self.size[0],0,0],
169
                          [0,self.size[1],0],
170
                          [0,0,self.size[2]]])
171
        basis = np.dot(basis, self.basis)
172

    
173
        # Tiny elements should be replaced by zero.  The cutoff is
174
        # determined by chop_tolerance which is a class attribute.
175
        basis = np.where(np.abs(basis) < self.chop_tolerance,
176
                         0.0, basis)
177

    
178
        # None should be replaced, and memory should be freed.
179
        lattice = Lattice(positions=atoms, cell=basis, numbers=elements,
180
                          pbc=self.pbc)
181
        lattice.millerbasis = self.miller_basis
182
        # Add info for lattice.surface.AddAdsorbate
183
        lattice._addsorbate_info_size = np.array(self.size[:2])
184
        return lattice
185

    
186
    def process_element(self, element):
187
        "Extract atomic number from element"
188
        # The types that can be elements: integers and strings
189
        if self.element_basis is None:
190
            if isinstance(element, type("string")):
191
                self.atomicnumber = ase.data.atomic_numbers[element]
192
            elif isinstance(element, int):
193
                self.atomicnumber = element
194
            else:
195
                raise TypeError("The symbol argument must be a string or an atomic number.")
196
        else:
197
            atomicnumber = []
198
            try:
199
                if len(element) != max(self.element_basis) + 1:
200
                    oops = True
201
                else:
202
                    oops = False
203
            except TypeError:
204
                oops = True
205
            if oops:
206
                raise TypeError(
207
                    ("The symbol argument must be a sequence of length %d"
208
                         +" (one for each kind of lattice position")
209
                        % (max(self.element_basis)+1,))
210
            for e in element:
211
                if isinstance(e, type("string")):
212
                    atomicnumber.append(ase.data.atomic_numbers[e])
213
                elif isinstance(element, int):
214
                    atomicnumber.append(e)
215
                else:
216
                    raise TypeError("The symbols argument must be a sequence of strings or atomic numbers.")
217
            self.atomicnumber = [atomicnumber[i] for i in self.element_basis]
218
            assert len(self.atomicnumber) == len(self.bravais_basis)
219

    
220
    def convert_to_natural_basis(self):
221
        "Convert directions and miller indices to the natural basis."
222
        self.directions = np.dot(self.directions, self.inverse_basis)
223
        if self.bravais_basis is not None:
224
            self.natural_bravais_basis = np.dot(self.bravais_basis,
225
                                                        self.inverse_basis)
226
        for i in (0,1,2):
227
            self.directions[i] = reduceindex(self.directions[i])
228
        for i in (0,1,2):
229
            (j,k) = self.other[i]
230
            self.miller[i] = reduceindex(self.handedness *
231
                                         cross(self.directions[j],
232
                                                self.directions[k]))
233

    
234
    def calc_num_atoms(self):
235
        v = int(round(abs(np.linalg.det(self.directions))))
236
        if self.bravais_basis is None:
237
            return v
238
        else:
239
            return v * len(self.bravais_basis)
240

    
241
    def make_unit_cell(self):
242
        "Make the unit cell."
243
        # Make three loops, and find the positions in the integral
244
        # lattice.  Each time a position is found, the atom is placed
245
        # in the real unit cell by put_atom().
246
        self.natoms = self.calc_num_atoms()
247
        self.nput = 0
248
        self.atoms = np.zeros((self.natoms,3), np.float)
249
        self.elements = np.zeros(self.natoms, np.int)
250
        self.farpoint = farpoint = sum(self.directions)
251
        #printprogress = self.debug and (len(self.atoms) > 250)
252
        percent = 0
253
        # Find the radius of the sphere containing the whole system
254
        sqrad = 0
255
        for i in (0,1):
256
            for j in (0,1):
257
                for k in (0,1):
258
                    vect = (i * self.directions[0] +
259
                            j * self.directions[1] +
260
                            k * self.directions[2])
261
                    if np.dot(vect,vect) > sqrad:
262
                        sqrad = np.dot(vect,vect)
263
        del i,j,k
264
        # Loop along first crystal axis (i)
265
        for (istart, istep) in ((0,1), (-1,-1)):
266
            i = istart
267
            icont = True
268
            while icont:
269
                nj = 0
270
                for (jstart, jstep) in ((0,1), (-1,-1)):
271
                    j = jstart
272
                    jcont = True
273
                    while jcont:
274
                        nk = 0
275
                        for (kstart, kstep) in ((0,1), (-1,-1)):
276
                            k = kstart
277
                            #print "Starting line i=%d, j=%d, k=%d, step=(%d,%d,%d)" % (i,j,k,istep,jstep,kstep)
278
                            kcont = True
279
                            while kcont:
280
                                # Now (i,j,k) loops over Z^3, except that
281
                                # the loops can be cut off when we get outside
282
                                # the unit cell.
283
                                point = np.array((i,j,k))
284
                                if self.inside(point):
285
                                    self.put_atom(point)
286
                                    nk += 1
287
                                    nj += 1
288
                                # Is k too high?
289
                                if np.dot(point,point) > sqrad:
290
                                    assert not self.inside(point)
291
                                    kcont = False
292
                                k += kstep
293
                        # Is j too high?
294
                        if i*i+j*j > sqrad:
295
                            jcont = False
296
                        j += jstep
297
                # Is i too high?
298
                if i*i > sqrad:
299
                    icont = False
300
                i += istep
301
                #if printprogress:
302
                #    perce = int(100*self.nput / len(self.atoms))
303
                #    if perce > percent + 10:
304
                #        print ("%d%%" % perce),
305
                #        percent = perce
306
        assert(self.nput == self.natoms)
307

    
308
    def inside(self, point):
309
        "Is a point inside the unit cell?"
310
        return (np.dot(self.miller[0], point) >= 0 and
311
                np.dot(self.miller[0], point - self.farpoint) < 0 and
312
                np.dot(self.miller[1], point) >= 0 and
313
                np.dot(self.miller[1], point - self.farpoint) < 0 and
314
                np.dot(self.miller[2], point) >= 0 and
315
                np.dot(self.miller[2], point - self.farpoint) < 0)
316

    
317
    def put_atom(self, point):
318
        "Place an atom given its integer coordinates."
319
        if self.bravais_basis is None:
320
            # No basis - just place a single atom
321
            pos = np.dot(point, self.crystal_basis)
322
            if self.debug >= 2:
323
                print ("Placing an atom at (%d,%d,%d) ~ (%.3f, %.3f, %.3f)."
324
                       % (tuple(point) + tuple(pos)))
325
            self.atoms[self.nput] = pos
326
            self.elements[self.nput] = self.atomicnumber
327
            self.nput += 1
328
        else:
329
            for i, offset in enumerate(self.natural_bravais_basis):
330
                pos = np.dot(point + offset, self.crystal_basis)
331
                if self.debug >= 2:
332
                    print ("Placing an atom at (%d+%f, %d+%f, %d+%f) ~ (%.3f, %.3f, %.3f)."
333
                           % (point[0], offset[0], point[1], offset[1],
334
                              point[2], offset[2], pos[0], pos[1], pos[2]))
335
                self.atoms[self.nput] = pos
336
                if self.element_basis is None:
337
                    self.elements[self.nput] = self.atomicnumber
338
                else:
339
                    self.elements[self.nput] = self.atomicnumber[i]
340
                self.nput += 1
341

    
342
    def find_directions(self, directions, miller):
343
        "Find missing directions and miller indices from the specified ones."
344
        directions = list(directions)
345
        miller = list(miller)
346
        # If no directions etc are specified, use a sensible default.
347
        if directions == [None, None, None] and miller == [None, None, None]:
348
            directions = [[1,0,0], [0,1,0], [0,0,1]]
349
        # Now fill in missing directions and miller indices.  This is an
350
        # iterative process.
351
        change = 1
352
        while change:
353
            change = False
354
            missing = 0
355
            for i in (0,1,2):
356
                (j,k) = self.other[i]
357
                if directions[i] is None:
358
                    missing += 1
359
                    if miller[j] is not None and miller[k] is not None:
360
                        directions[i] = reduceindex(cross(miller[j],
361
                                                          miller[k]))
362
                        change = True
363
                        if self.debug >= 2:
364
                            print "Calculating directions[%d] from miller indices" % i
365
                if miller[i] is None:
366
                    missing += 1
367
                    if directions[j] is not None and directions[k] is not None:
368
                        miller[i] = reduceindex(cross(directions[j],
369
                                                      directions[k]))
370
                        change = True
371
                        if self.debug >= 2:
372
                            print "Calculating miller[%d] from directions" % i
373
        if missing:
374
            raise ValueError, "Specification of directions and miller indices is incomplete."
375
        # Make sure that everything is Numeric arrays
376
        self.directions = np.array(directions)
377
        self.miller = np.array(miller)
378
        # Check for left-handed coordinate system
379
        if np.linalg.det(self.directions) < 0:
380
            print "WARNING: Creating a left-handed coordinate system!"
381
            self.miller = -self.miller
382
            self.handedness = -1
383
        else:
384
            self.handedness = 1
385
        # Now check for consistency
386
        for i in (0,1,2):
387
            (j,k) = self.other[i]
388
            m = reduceindex(self.handedness *
389
                            cross(self.directions[j], self.directions[k]))
390
            if sum(np.not_equal(m, self.miller[i])):
391
                print "ERROR: Miller index %s is inconsisten with directions %d and %d" % (i,j,k)
392
                print "Miller indices:"
393
                print str(self.miller)
394
                print "Directions:"
395
                print str(self.directions)
396
                raise ValueError, "Inconsistent specification of miller indices and directions."
397

    
398
    def print_directions_and_miller(self, txt=""):
399
        "Print direction vectors and Miller indices."
400
        print "Direction vectors of unit cell%s:" % (txt,)
401
        for i in (0,1,2):
402
            print "   ", self.directions[i]
403
        print "Miller indices of surfaces%s:" % (txt,)
404
        for i in (0,1,2):
405
            print "   ", self.miller[i]
406

    
407
class MillerInfo:
408
    """Mixin class to provide information about Miller indices."""
409
    def miller_to_direction(self, miller):
410
        """Returns the direction corresponding to a given Miller index."""
411
        return np.dot(miller, self.millerbasis)
412

    
413
class Lattice(Atoms, MillerInfo):
414
    """List of atoms initially containing a regular lattice of atoms.
415

416
    A part from the usual list of atoms methods this list of atoms type
417
    also has a method, `miller_to_direction`, used to convert from Miller
418
    indices to directions in the coordinate system of the lattice.
419
    """
420
    pass
421

    
422
# Helper functions
423
def cross(a, b):
424
    """The cross product of two vectors."""
425
    return np.array((a[1]*b[2] - b[1]*a[2],
426
                     a[2]*b[0] - b[2]*a[0],
427
                     a[0]*b[1] - b[0]*a[1]))
428

    
429
def gcd(a,b):
430
    """Greatest Common Divisor of a and b."""
431
    while a != 0:
432
        a,b = b%a,a
433
    return b
434

    
435
def reduceindex(M):
436
    "Reduce Miller index to the lowest equivalent integers."
437
    oldM = M
438
    g = gcd(M[0], M[1])
439
    h = gcd(g, M[2])
440
    while h != 1:
441
        M = M/h
442
        g = gcd(M[0], M[1])
443
        h = gcd(g, M[2])
444
    if np.dot(oldM, M) > 0:
445
        return M
446
    else:
447
        return -M
448