Statistiques
| Révision :

root / ase / lattice / spacegroup / spacegroup.py @ 1

Historique | Voir | Annoter | Télécharger (14,86 ko)

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

    
4
"""Definition of the Spacegroup class.
5

6
This module only depends on NumPy and the space group database.
7
"""
8

    
9
import os
10
import warnings
11

    
12
import numpy as np
13

    
14
__all__ = ['Spacegroup']
15

    
16

    
17
class SpacegroupError(Exception):
18
    """Base exception for the spacegroup module."""
19
    pass
20

    
21
class SpacegroupNotFoundError(SpacegroupError):
22
    """Raised when given space group cannot be found in data base."""
23
    pass
24

    
25
class SpacegroupValueError(SpacegroupError):
26
    """Raised when arguments have invalid value."""
27
    pass
28

    
29

    
30
class Spacegroup(object):
31
    """A space group class. 
32
    
33
    The instances of Spacegroup describes the symmetry operations for
34
    the given space group. 
35

36
    Example:
37
    
38
    >>> from spacegroup import Spacegroup
39
    >>> 
40
    >>> sg = Spacegroup(225)
41
    >>> print 'Space group', sg.no, sg.symbol
42
    Space group 225 F m -3 m
43
    >>> sg.scaled_primitive_cell
44
    array([[ 0. ,  0.5,  0.5],
45
           [ 0.5,  0. ,  0.5],
46
           [ 0.5,  0.5,  0. ]])
47
    >>> sites, kinds = sg.equivalent_sites([[0,0,0]])
48
    >>> sites
49
    array([[ 0. ,  0. ,  0. ],
50
           [ 0. ,  0.5,  0.5],
51
           [ 0.5,  0. ,  0.5],
52
           [ 0.5,  0.5,  0. ]])
53
    """
54
    no = property(
55
        lambda self: self._no,
56
        doc='Space group number in International Tables of Crystallography.')
57
    symbol = property(
58
        lambda self: self._symbol, 
59
        doc='Hermann-Mauguin (or international) symbol for the space group.')
60
    setting = property(
61
        lambda self: self._setting, 
62
        doc='Space group setting. Either one or two.')
63
    lattice = property(
64
        lambda self: self._symbol[0], 
65
        doc="""Lattice type: 
66
            P      primitive
67
            I      body centering, h+k+l=2n
68
            F      face centering, h,k,l all odd or even
69
            A,B,C  single face centering, k+l=2n, h+l=2n, h+k=2n
70
            R      rhombohedral centering, -h+k+l=3n (obverse); h-k+l=3n (reverse)
71
            """)
72
    centrosymmetric = property(
73
        lambda self: self._centrosymmetric, 
74
        doc='Whether a center of symmetry exists.')
75
    scaled_primitive_cell = property(
76
        lambda self: self._scaled_primitive_cell, 
77
        doc='Primitive cell in scaled coordinates as a matrix with the '
78
        'primitive vectors along the rows.')
79
    reciprocal_cell = property(
80
        lambda self: self._reciprocal_cell, 
81
        doc='Tree Miller indices that span all kinematically non-forbidden '
82
        'reflections as a matrix with the Miller indices along the rows.')
83
    nsubtrans = property(
84
        lambda self: len(self._subtrans), 
85
        doc='Number of cell-subtranslation vectors.')
86
    def _get_nsymop(self):
87
        if self.centrosymmetric:
88
            return 2 * len(self._rotations) * len(self._subtrans)
89
        else:
90
            return len(self._rotations) * len(self._subtrans)
91
    nsymop = property(_get_nsymop, doc='Total number of symmetry operations.')
92
    subtrans = property(
93
        lambda self: self._subtrans, 
94
        doc='Translations vectors belonging to cell-sub-translations.')
95
    rotations = property(
96
        lambda self: self._rotations, 
97
        doc='Symmetry rotation matrices. The invertions are not included '
98
        'for centrosymmetrical crystals.')
99
    translations = property(
100
        lambda self: self._translations, 
101
        doc='Symmetry translations. The invertions are not included '
102
        'for centrosymmetrical crystals.')
103

    
104
    def __init__(self, spacegroup, setting=1, datafile=None):
105
        """Returns a new Spacegroup instance. 
106

107
        Parameters:
108

109
        spacegroup : int | string | Spacegroup instance
110
            The space group number in International Tables of
111
            Crystallography or its Hermann-Mauguin symbol. E.g. 
112
            spacegroup=225 and spacegroup='F m -3 m' are equivalent.
113
        setting : 1 | 2
114
            Some space groups have more than one setting. `setting`
115
            determines Which of these should be used.
116
        datafile : None | string
117
            Path to database file. If `None`, the the default database 
118
            will be used.
119
        """
120
        if isinstance(spacegroup, Spacegroup):
121
            for k, v in spacegroup.__dict__.iteritems():
122
                setattr(self, k, v)
123
            return 
124
        if not datafile:
125
            datafile = get_datafile()
126
        f = open(datafile, 'r')
127
        try:
128
            _read_datafile(self, spacegroup, setting, f)
129
        finally:
130
            f.close()
131

    
132
    def __repr__(self):
133
        return 'Spacegroup(%d, setting=%d)'%(self.no, self.setting)
134
    
135
    def __str__(self):
136
        """Return a string representation of the space group data in
137
        the same format as found the database."""
138
        retval = []
139
        # no, symbol
140
        retval.append('%-3d   %s\n' % (self.no, self.symbol))
141
        # setting
142
        retval.append('  setting %d\n' % (self.setting))
143
        # centrosymmetric
144
        retval.append('  centrosymmetric %d\n' % (self.centrosymmetric))
145
        # primitive vectors
146
        retval.append('  primitive vectors\n')
147
        for i in range(3):
148
            retval.append('   ')
149
            for j in range(3):
150
                retval.append(' %13.10f' % (self.scaled_primitive_cell[i, j]))
151
            retval.append('\n')
152
        # primitive reciprocal vectors
153
        retval.append('  reciprocal vectors\n')
154
        for i in range(3):
155
            retval.append('   ')
156
            for j in range(3):
157
                retval.append(' %3d' % (self.reciprocal_cell[i, j]))
158
            retval.append('\n')
159
        # sublattice
160
        retval.append('  %d subtranslations\n' % self.nsubtrans)
161
        for i in range(self.nsubtrans):
162
            retval.append('   ')
163
            for j in range(3):
164
                retval.append(' %13.10f' % (self.subtrans[i, j]))
165
            retval.append('\n')
166
        # symmetry operations
167
        nrot = len(self.rotations)
168
        retval.append('  %d symmetry operations (rot+trans)\n' % nrot)
169
        for i in range(nrot):
170
            retval.append(' ')
171
            for j in range(3):
172
                retval.append(' ')
173
                for k in range(3):
174
                    retval.append(' %2d' % (self.rotations[i, j, k]))
175
                retval.append('  ')
176
            for j in range(3):
177
                retval.append(' %13.10f' % self.translations[i, j])
178
            retval.append('\n')
179
        retval.append('\n')
180
        return ''.join(retval)
181

    
182
    def __eq__(self, other):
183
        """Chech whether *self* and *other* refer to the same
184
        spacegroup number and setting."""
185
        if isinstance(other, int):
186
            return self.no == other
187
        elif isinstance(other, str):
188
            return self.symbol == other
189
        else:
190
            return self.no == other.no and self.setting == other.setting
191

    
192
    def __index__(self):
193
        return self.no
194

    
195
    def get_symop(self):
196
        """Returns all symmetry operations (including inversions and
197
        subtranslations) as a sequence of (rotation, translation)
198
        tuples."""
199
        symop = []
200
        parities = [1]
201
        if self.centrosymmetric:
202
            parities.append(-1)
203
        for parity in parities:
204
            for subtrans in self.subtrans:
205
                for rot,trans in zip(self.rotations, self.translations):
206
                    newtrans = np.mod(trans + subtrans, 1)
207
                    symop.append((parity*rot, newtrans))
208
        return symop
209

    
210
    def get_rotations(self):
211
        """Return all rotations, including inversions for
212
        centrosymmetric crystals."""
213
        if self.centrosymmetric:
214
            return np.vstack((self.rotations, -self.rotations))
215
        else:
216
            return self.rotations
217

    
218
    def equivalent_reflections(self, hkl):
219
        """Return all equivalent reflections to the list of Miller indices
220
        in hkl.
221

222
        Example:
223

224
        >>> from spacegroup import Spacegroup
225
        >>> sg = Spacegroup(225)  # fcc
226
        >>> sg.equivalent_reflections([[0,0,2]])
227
        array([[ 0,  0, -2],
228
               [ 0, -2,  0],
229
               [-2,  0,  0],
230
               [ 2,  0,  0],
231
               [ 0,  2,  0],
232
               [ 0,  0,  2]])
233
        """
234
        hkl = np.array(hkl, dtype='int')
235
        rot = self.get_rotations()
236
        n,nrot = len(hkl), len(rot)
237
        R = rot.transpose(0, 2, 1).reshape((3*nrot, 3)).T
238
        refl = np.dot(hkl, R).reshape((n*nrot, 3))
239
        ind = np.lexsort(refl.T)
240
        refl = refl[ind]
241
        diff = np.diff(refl, axis=0)
242
        mask = np.any(diff, axis=1)
243
        return np.vstack((refl[mask], refl[-1,:]))
244
            
245
    def equivalent_sites(self, scaled_positions, ondublicates='error', 
246
                         symprec=1e-3):
247
        """Returns the scaled positions and all their equivalent sites.
248

249
        Parameters:
250

251
        scaled_positions: list | array
252
            List of non-equivalent sites given in unit cell coordinates.
253
        ondublicates : 'keep' | 'replace' | 'warn' | 'error'
254
            Action if `scaled_positions` contain symmetry-equivalent
255
            positions:
256
            
257
            'keep'
258
               ignore additional symmetry-equivalent positions
259
            'replace'
260
                replace
261
            'warn'
262
                like 'keep', but issue an UserWarning
263
            'error'
264
                raises a SpacegroupValueError
265
                    
266
        symprec: float
267
            Minimum "distance" betweed two sites in scaled coordinates
268
            before they are counted as the same site.
269

270
        Returns:
271

272
        sites: array
273
            A NumPy array of equivalent sites.
274
        kinds: list
275
            A list of integer indices specifying which input site is 
276
            equivalent to the corresponding returned site.
277
        """
278
        kinds = []
279
        sites = []
280
        symprec2 = symprec**2
281
        for kind, pos in enumerate(scaled_positions):
282
            for rot, trans in self.get_symop():
283
                site = np.mod(np.dot(rot, pos) + trans, 1.)
284
                if not sites:
285
                    sites.append(site)
286
                    kinds.append(kind)
287
                    continue
288
                t = site - sites
289
                mask = np.sum(t*t, 1) < symprec2
290
                if np.any(mask):
291
                    ind = np.argwhere(mask)[0][0]
292
                    if kinds[ind] == kind:
293
                        pass
294
                    elif ondublicates == 'keep':
295
                        pass
296
                    elif ondublicates == 'replace':
297
                        kinds[ind] = kind
298
                    elif ondublicates == 'warn':
299
                        warnings.warn('scaled_positions %d and %d '
300
                                      'are equivalent'%(kinds[ind], kind))
301
                    elif ondublicates == 'error':
302
                        raise SpacegroupValueError(
303
                            'scaled_positions %d and %d are equivalent'%(
304
                                kinds[ind], kind))
305
                    else:
306
                        raise SpacegroupValueError(
307
                            'Argument "ondublicates" must be one of: '
308
                            '"keep", "replace", "warn" or "error".')
309
                else:
310
                    sites.append(site)
311
                    kinds.append(kind)
312
        return np.array(sites), kinds
313

    
314

    
315

    
316

    
317
def get_datafile():
318
    """Return default path to datafile."""
319
    return os.path.join(os.path.dirname(__file__), 'spacegroup.dat')
320

    
321

    
322

    
323

    
324
#-----------------------------------------------------------------
325
# Functions for parsing the database. They are moved outside the
326
# Spacegroup class in order to make it easier to later implement
327
# caching to avoid reading the database each time a new Spacegroup
328
# instance is created.
329
#-----------------------------------------------------------------
330

    
331
def _skip_to_blank(f, spacegroup, setting):
332
    """Read lines from f until a blank line is encountered."""
333
    while True:
334
        line = f.readline()
335
        if not line:
336
            raise SpacegroupNotFoundError(
337
                'invalid spacegroup %s, setting %i not found in data base' % 
338
                ( spacegroup, setting ) )
339
        if not line.strip():
340
            break
341

    
342

    
343
def _skip_to_nonblank(f, spacegroup, setting):
344
    """Read lines from f until a nonblank line not starting with a
345
    hash (#) is encountered and returns this and the next line."""
346
    while True:
347
        line1 = f.readline()
348
        if not line1:
349
            raise SpacegroupNotFoundError(
350
                'invalid spacegroup %s, setting %i not found in data base' %
351
                ( spacegroup, setting ) )
352
        line1.strip()
353
        if line1 and not line1.startswith('#'):
354
            line2 = f.readline()
355
            break
356
    return line1, line2
357

    
358

    
359
def _read_datafile_entry(spg, no, symbol, setting, f):
360
    """Read space group data from f to spg."""
361
    spg._no = no
362
    spg._symbol = symbol.strip()
363
    spg._setting = setting
364
    spg._centrosymmetric = bool(int(f.readline().split()[1]))
365
    # primitive vectors
366
    f.readline()
367
    spg._scaled_primitive_cell = np.array([map(float, f.readline().split()) 
368
                                       for i in range(3)], 
369
                                      dtype=np.float)
370
    # primitive reciprocal vectors
371
    f.readline()
372
    spg._reciprocal_cell = np.array([map(int, f.readline().split()) 
373
                                        for i in range(3)],
374
                                       dtype=np.int)
375
    # subtranslations
376
    spg._nsubtrans = int(f.readline().split()[0])
377
    spg._subtrans = np.array([map(float, f.readline().split()) 
378
                              for i in range(spg._nsubtrans)],
379
                             dtype=np.float)
380
    # symmetry operations
381
    nsym = int(f.readline().split()[0])
382
    symop = np.array([map(float, f.readline().split()) for i in range(nsym)],
383
                     dtype=np.float)
384
    spg._nsymop = nsym
385
    spg._rotations = np.array(symop[:,:9].reshape((nsym,3,3)), dtype=np.int)
386
    spg._translations = symop[:,9:]
387

    
388

    
389
def _read_datafile(spg, spacegroup, setting, f):
390
    if isinstance(spacegroup, int):
391
        pass
392
    elif isinstance(spacegroup, basestring):
393
        spacegroup = ' '.join(spacegroup.strip().split())
394
    else:
395
        raise SpacegroupValueError('`spacegroup` must be of type int or str')
396
    while True:
397
        line1, line2 = _skip_to_nonblank(f, spacegroup, setting)
398
        _no,_symbol = line1.strip().split(None, 1)
399
        _setting = int(line2.strip().split()[1])
400
        _no = int(_no)
401
        if ((isinstance(spacegroup, int) and _no == spacegroup) or
402
            (isinstance(spacegroup, basestring) and 
403
             _symbol == spacegroup)) and _setting == setting:
404
            _read_datafile_entry(spg, _no, _symbol, _setting, f)
405
            break
406
        else:
407
            _skip_to_blank(f, spacegroup, setting)
408
                
409

    
410

    
411
#-----------------------------------------------------------------
412
# Self test
413
if __name__ == '__main__':
414

    
415
    # Import spacegroup in order to ensure that __file__ is defined
416
    # such that the data base can be found.
417
    import spacegroup
418

    
419
    import doctest
420
    print 'doctest: ', doctest.testmod()