Statistiques
| Révision :

root / ase / utils / geometry.py @ 20

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

1
# Copyright (C) 2010, Jesper Friis
2
# (see accompanying license files for details).
3

    
4
"""Utility tools for convenient creation of slabs and interfaces of
5
different orientations."""
6

    
7
import numpy as np
8

    
9

    
10

    
11
def gcd(seq):
12
    """Returns greatest common divisor of integers in *seq*."""
13
    def _gcd(m, n):
14
        while n:
15
            m, n = n, m%n
16
        return m       
17
    return reduce(_gcd, seq)
18

    
19

    
20

    
21

    
22
def get_layers(atoms, miller, tolerance=0.001):
23
    """Returns two arrays describing which layer each atom belongs
24
    to and the distance between the layers and origo. 
25

26
    Parameters:
27

28
    miller: 3 integers
29
        The Miller indices of the planes. Actually, any direction
30
        in reciprocal space works, so if a and b are two float
31
        vectors spanning an atomic plane, you can get all layers
32
        parallel to this with miller=np.cross(a,b).
33
    tolerance: float
34
        The maximum distance in Angstrom along the plane normal for
35
        counting two atoms as belonging to the same plane.
36

37
    Returns:
38

39
    tags: array of integres
40
        Array of layer indices for each atom.
41
    levels: array of floats
42
        Array of distances in Angstrom from each layer to origo.
43

44
    Example:
45

46
    >>> import numpy as np
47
    >>> from ase.lattice.spacegroup import crystal
48
    >>> atoms = crystal('Al', [(0,0,0)], spacegroup=225, cellpar=4.05)
49
    >>> np.round(atoms.positions, decimals=5)
50
    array([[ 0.   ,  0.   ,  0.   ],
51
           [ 0.   ,  2.025,  2.025],
52
           [ 2.025,  0.   ,  2.025],
53
           [ 2.025,  2.025,  0.   ]])
54
    >>> get_layers(atoms, (0,0,1))
55
    (array([0, 1, 1, 0]), array([ 0.   ,  2.025]))
56
    """
57
    miller = np.asarray(miller)
58

    
59
    metric = np.dot(atoms.cell, atoms.cell.T)
60
    c = np.linalg.solve(metric.T, miller.T).T
61
    miller_norm = np.sqrt(np.dot(c, miller))
62
    d = np.dot(atoms.get_scaled_positions(), miller)/miller_norm
63

    
64
    keys = np.argsort(d)
65
    ikeys = np.argsort(keys)
66
    mask = np.concatenate(([True], np.diff(d[keys]) > tolerance))
67
    tags = np.cumsum(mask)[ikeys]
68
    if tags.min() == 1:
69
        tags -= 1
70

    
71
    levels = d[keys][mask]
72
    return tags, levels
73

    
74

    
75

    
76

    
77
def cut(atoms, a=(1,0,0), b=(0,1,0), c=(0,0,1), origo=(0,0,0), 
78
        nlayers=None, extend=1.0, tolerance=0.001):
79
    """Cuts out a cell defined by *a*, *b*, *c* and *origo* from a
80
    sufficiently repeated copy of *atoms*.
81

82
    Typically, this function is used to create slabs of different
83
    sizes and orientations. The vectors *a*, *b* and *c* are in scaled
84
    coordinates and defines the returned cell and should normally be
85
    integer-valued in order to end up with a periodic
86
    structure. However, for systems with sub-translations, like fcc,
87
    integer multiples of 1/2 or 1/3 might also make sence for some
88
    directions (and will be treated correctly).
89

90
    Parameters:
91
    
92
    atoms: Atoms instance
93
        This should correspond to a repeatable unit cell.
94
    a: int | 3 floats
95
        The a-vector in scaled coordinates of the cell to cut out. If
96
        integer, the a-vector will be the scaled vector from *origo* to the
97
        atom with index *a*.
98
    b: int | 3 floats
99
        The b-vector in scaled coordinates of the cell to cut out. If
100
        integer, the b-vector will be the scaled vector from *origo* to the
101
        atom with index *b*.
102
    c: int | 3 floats
103
        The c-vector in scaled coordinates of the cell to cut out. If
104
        integer, the c-vector will be the scaled vector from *origo* to the
105
        atom with index *c*. Not used if *nlayers* is given.
106
    origo: int | 3 floats
107
        Position of origo of the new cell in scaled coordinates. If
108
        integer, the position of the atom with index *origo* is used.
109
    nlayers: int
110
        If *nlayers* is not *None*, the returned cell will have
111
        *nlayers* atomic layers in the c-direction. The direction of
112
        the c-vector will be along cross(a, b) converted to real
113
        space, i.e. normal to the plane spanned by a and b in
114
        orthorombic systems.
115
    extend: 1 or 3 floats
116
        The *extend* argument scales the effective cell in which atoms
117
        will be included. It must either be three floats or a single
118
        float scaling all 3 directions.  By setting to a value just
119
        above one, e.g. 1.05, it is possible to all the corner and
120
        edge atoms in the returned cell.  This will of cause make the
121
        returned cell non-repeatable, but is very usefull for
122
        visualisation.
123
    tolerance: float
124
        Determines what is defined as a plane.  All atoms within
125
        *tolerance* Angstroms from a given plane will be considered to
126
        belong to that plane.
127

128
    Example:
129

130
    >>> import ase
131
    >>> from ase.lattice.spacegroup import crystal
132
    >>>
133
    # Create an aluminium (111) slab with three layers
134
    #
135
    # First an unit cell of Al
136
    >>> a = 4.05
137
    >>> aluminium = crystal('Al', [(0,0,0)], spacegroup=225,
138
    ...                     cellpar=[a, a, a, 90, 90, 90])
139
    >>>
140
    # Then cut out the slab
141
    >>> al111 = cut(aluminium, (1,-1,0), (0,1,-1), nlayers=3)
142
    >>>
143
    # Visualisation of the skutterudite unit cell 
144
    #
145
    # Again, create a skutterudite unit cell
146
    >>> a = 9.04
147
    >>> skutterudite = crystal(
148
    ...     ('Co', 'Sb'), 
149
    ...     basis=[(0.25,0.25,0.25), (0.0, 0.335, 0.158)], 
150
    ...     spacegroup=204, 
151
    ...     cellpar=[a, a, a, 90, 90, 90])
152
    >>>
153
    # Then use *origo* to put 'Co' at the corners and *extend* to
154
    # include all corner and edge atoms.
155
    >>> s = cut(skutterudite, origo=(0.25, 0.25, 0.25), extend=1.01)
156
    >>> ase.view(s)  # doctest: +SKIP
157
    """
158
    atoms = atoms.copy()
159
    cell = atoms.cell
160

    
161
    if isinstance(origo, int):
162
        origo = atoms.get_scaled_positions()[origo]
163
    scaled = (atoms.get_scaled_positions() - origo)%1.0
164
    scaled %= 1.0 # needed to ensure that all numbers are *less* than one
165
    atoms.set_scaled_positions(scaled)
166

    
167
    if isinstance(a, int):
168
        a = scaled[a] - origo
169
    if isinstance(b, int):
170
        b = scaled[b] - origo
171
    if isinstance(c, int):
172
        c = scaled[c] - origo
173

    
174
    a = np.array(a, dtype=float)
175
    b = np.array(b, dtype=float)
176
    origo = np.array(origo, dtype=float)
177

    
178
    if nlayers:
179
        miller = np.cross(a, b) # surface normal
180
        # The factor 36 = 2*2*3*3 is because the elements of a and b
181
        # might be multiples of 1/2 or 1/3 because of lattice
182
        # subtranslations
183
        if np.all(36*miller - np.rint(36*miller)) < 1e-5:
184
            miller = np.rint(36*miller)
185
            miller /= gcd(miller)
186
        tags, layers = get_layers(atoms, miller, tolerance)
187
        while tags.max() < nlayers:
188
            atoms = atoms.repeat(2)
189
            tags, layers = get_layers(atoms, miller, tolerance)
190
        # Convert surface normal in reciprocal space to direction in
191
        # real space
192
        metric = np.dot(cell, cell.T)
193
        c = np.linalg.solve(metric.T, miller.T).T
194
        c *= layers[nlayers]/np.sqrt(np.dot(c, miller))
195
        if np.linalg.det(np.dot(np.array([a, b, c]), cell)) < 0:
196
            c *= -1.0
197

    
198
    newcell = np.dot(np.array([a, b, c]), cell)
199
    
200
    # Create a new atoms object, repeated and translated such that
201
    # it completely covers the new cell
202
    scorners_newcell = np.array([[0., 0., 0.], [0., 0., 1.], 
203
                                 [0., 1., 0.], [0., 1., 1.], 
204
                                 [1., 0., 0.], [1., 0., 1.], 
205
                                 [1., 1., 0.], [1., 1., 1.]])
206
    corners = np.dot(scorners_newcell, newcell*extend)
207
    scorners = np.linalg.solve(cell.T, corners.T).T
208
    rep = np.ceil(scorners.ptp(axis=0)).astype('int') + 1
209
    trans = np.dot(np.floor(scorners.min(axis=0)), cell)
210
    atoms = atoms.repeat(rep)
211
    atoms.translate(trans)
212
    atoms.set_cell(newcell)
213

    
214
    # Mask out atoms outside new cell
215
    stol = tolerance  # scaled tolerance, XXX
216
    maskcell = atoms.cell*extend
217
    sp = np.linalg.solve(maskcell.T, (atoms.positions).T).T
218
    mask = np.all(np.logical_and(-stol <= sp, sp < 1-stol), axis=1)
219
    atoms = atoms[mask]
220
    
221
    return atoms
222

    
223

    
224

    
225

    
226
def stack(atoms1, atoms2, axis=2, cell=None, fix=0.5,  
227
          maxstrain=0.5, distance=None):
228
    """Return a new Atoms instance with *atoms2* added to atoms1
229
    along the given axis. Periodicity in all directions is
230
    ensured.
231

232
    The size of the final cell is determined by *cell*, except
233
    that the length alongh *axis* will be the sum of
234
    *atoms1.cell[axis]* and *atoms2.cell[axis]*. If *cell* is None,
235
    it will be interpolated between *atoms1* and *atoms2*, where
236
    *fix* determines their relative weight. Hence, if *fix* equals
237
    zero, the final cell will be determined purely from *atoms1* and
238
    if *fix* equals one, it will be determined purely from
239
    *atoms2*.
240

241
    An ValueError exception will be raised if the far corner of
242
    the unit cell of either *atoms1* or *atoms2* is displaced more
243
    than *maxstrain*. Setting *maxstrain* to None, disable this
244
    check.
245

246
    If *distance* is provided, the atomic positions in *atoms1* and
247
    *atoms2* as well as the cell lengths along *axis* will be
248
    adjusted such that the distance between the distance between
249
    the closest atoms in *atoms1* and *atoms2* will equal *distance*.
250
    This option uses scipy.optimize.fmin() and hence require scipy
251
    to be installed.
252

253
    Example:
254

255
    >>> import ase
256
    >>> from ase.lattice.spacegroup import crystal
257
    >>>
258
    # Create an Ag(110)-Si(110) interface with three atomic layers
259
    # on each side. 
260
    >>> a_ag = 4.09
261
    >>> ag = crystal(['Ag'], basis=[(0,0,0)], spacegroup=225, 
262
    ...              cellpar=[a_ag, a_ag, a_ag, 90., 90., 90.])
263
    >>> ag110 = cut(ag, (0, 0, 3), (-1.5, 1.5, 0), nlayers=3)
264
    >>>
265
    >>> a_si = 5.43
266
    >>> si = crystal(['Si'], basis=[(0,0,0)], spacegroup=227, 
267
    ...              cellpar=[a_si, a_si, a_si, 90., 90., 90.])
268
    >>> si110 = cut(si, (0, 0, 2), (-1, 1, 0), nlayers=3)
269
    >>>
270
    >>> interface = stack(ag110, si110, maxstrain=1)
271
    >>> ase.view(interface)  # doctest: +SKIP
272
    >>>
273
    # Once more, this time adjusted such that the distance between
274
    # the closest Ag and Si atoms will be 2.3 Angstrom.
275
    >>> interface2 = stack(ag110, si110, 
276
    ...                    maxstrain=1, distance=2.3)   # doctest:+ELLIPSIS
277
    Optimization terminated successfully.
278
        ...
279
    >>> ase.view(interface2)  # doctest: +SKIP
280
    """
281
    atoms1 = atoms1.copy()
282
    atoms2 = atoms2.copy()
283

    
284
    c1 = np.linalg.norm(atoms1.cell[axis])
285
    c2 = np.linalg.norm(atoms2.cell[axis])
286
    if cell is None:
287
        cell1 = atoms1.cell.copy()
288
        cell2 = atoms2.cell.copy()
289
        cell1[axis] /= c1
290
        cell2[axis] /= c2
291
        cell = cell1 + fix*(cell2 - cell1)
292
    cell[axis] /= np.linalg.norm(cell[axis])
293
    cell1 = cell.copy()
294
    cell2 = cell.copy()
295
    cell1[axis] *= c1
296
    cell2[axis] *= c2
297

    
298
    if (maxstrain and
299
        (((cell1 - atoms1.cell).sum(axis=0)**2).sum() > maxstrain**2 or
300
         ((cell2 - atoms2.cell).sum(axis=0)**2).sum() > maxstrain**2)):
301
        raise ValueError('Incompatible cells.')
302

    
303
    sp1 = np.linalg.solve(atoms1.cell.T, atoms1.positions.T).T
304
    sp2 = np.linalg.solve(atoms2.cell.T, atoms2.positions.T).T
305
    atoms1.set_cell(cell1)
306
    atoms2.set_cell(cell2)
307
    atoms1.set_scaled_positions(sp1)
308
    atoms2.set_scaled_positions(sp2)
309

    
310
    if distance is not None:
311
        from scipy.optimize import fmin
312
        def mindist(pos1, pos2):
313
            n1 = len(pos1)
314
            n2 = len(pos2)
315
            idx1 = np.arange(n1).repeat(n2)
316
            idx2 = np.tile(np.arange(n2), n1)
317
            return np.sqrt(((pos1[idx1] - pos2[idx2])**2).sum(axis=1).min())
318
        def func(x):
319
            t1, t2, h1, h2 = x[0:3], x[3:6], x[6], x[7]
320
            pos1 = atoms1.positions + t1
321
            pos2 = atoms2.positions + t2
322
            d1 = mindist(pos1, pos2 + (h1 + 1.0)*atoms1.cell[axis])
323
            d2 = mindist(pos2, pos1 + (h2 + 1.0)*atoms2.cell[axis])
324
            return (d1 - distance)**2 + (d2 - distance)**2
325
        atoms1.center()
326
        atoms2.center()
327
        x0 = np.zeros((8,))
328
        x = fmin(func, x0)
329
        t1, t2, h1, h2 = x[0:3], x[3:6], x[6], x[7]
330
        atoms1.translate(t1)
331
        atoms2.translate(t2)
332
        atoms1.cell[axis] *= 1.0 + h1
333
        atoms2.cell[axis] *= 1.0 + h2
334

    
335
    atoms2.translate(atoms1.cell[axis])
336
    atoms1.cell[axis] += atoms2.cell[axis]
337
    atoms1.extend(atoms2)
338
    return atoms1
339

    
340

    
341
#-----------------------------------------------------------------
342
# Self test
343
if __name__ == '__main__':
344
    import doctest
345
    print 'doctest: ', doctest.testmod()