Source code for pylada.dftcrystal.crystal

__docformat__ = "restructuredtext en"
__all__ = ['Crystal']
from .molecule import Molecule
[docs]class Crystal(Molecule): """ CRYSTAL-wise structure, e.g. functional approach. CRYSTAL_ proposes a functional_ approach to crystals, as opposed to the imperative_ style used by Pylada. In practice, this means that CRYSTAL_ declares a chain of functions which acts upon an initial data and transform it. The data is generally a space-group with a set of atomic sites. The functions can be affine transformations on these sites, additions and removals of sites, transformation upon the space-group, and even strain relaxation. In practice, both approaches can "do the same thing", and indeed both CRYSTAL_ and Pylada provide similar functionalities, e.g. creating a supercell from a unit-cell. However, there are clear benefits from allowing users to keep working the CRYSTAL_ way when working with CRYSTAL_. This class provides a python wrapper around CRYSTAL_'s approach. It is only a wrapper, in that it merely contains the data necessary to create a CRYSTAL_ input file. It does not re-implement any of CRYSTAL_'s algorithms. However, it does provides the ability to call CRYSTAL_ and extract the results as a Pylada :py:class:`~pylada.crystal.cppwrappers.Structure` instance. In this way, both approaches can be mixed, allowing for a complete integration of CRYSTAL_ with python. At present, both the initial data and chain of functions are represented and stored within the same :py:class:`Crystal` instance. This is a bit sub-optimal. A better implementation would make clear the separation bewteen initial data and functions. However, it is less wordy. The initial data upon which CRYSTAL_ applies its chain of functions is defined using the :py:attr:`symmgroup`, :py:attr:`args`, and :py:attr:`atoms`. The first references the space-group of the crystal, the second defines the lattice parameters, and the last holds the (initial) atomic sites. :py:class:`Crystal` is also a list where each item is an operation upon the initial crystal. In practice, the initial data is declared as follows: >>> from pylada.dftcrystal import Crystal >>> crystal = Crystal(227, 5.43) \\ ... .add_atom(0.125, 0.125, 0.125, "Si") This declares the diamond silicon structure. The first argument is the space-group, and the second the only required lattice parameter. If other parameters were needed they would be listed directly ``Crystal(?, a, b, c, alpha, beta, gamma)``. Only the parameters required for that particular space-group should be listed. The second line adds an atomic site to the initial structure. :py:meth:`add_atom` returns an the :py:class:`Crystal` instance itself, so that atomic site declarations can be chained: >>> crystal = Crystal(...) \\ ... .add_atom(...) \\ ... .add_atom(...) \\ ... .add_atom(...) Only those atoms which are strictly inequivalent by symmetry operations should be listed. :py:class:`Crystal` instances function as lists of transformations which are to be applied to the initial structure. The simplest way to add an operation is to use the traditional CRYSTAL_ format: >>> crystal = Crystal(227, 5.43) \\ ... .add_atom(0.125, 0.125, 0.125, "Si") \\ ... .append('supercel', '2 0 0 0 1 0 0 0 1') Here we have created a supercell of diamond with two unit cell in the (100) direction. The first string in :py:meth:`append` is the keyword and the second string its input. Much as :py:meth:`add_atom`, :py:meth:`append` returns the calling instance of :py:class:`Crystal` so that calls can be chained into a single declarative expression. A number of operations are implemented in a more pythonic manner. These can be added to the chain of functions by calling :py:meth:`append` with an operation instance as a the only argument. >>> from pylada.dftcrystal import Slabcut >>> crystal.append( Slabcut(hkl=(1, 0, 0), isup=1, nl=3) ) :py:class:`~pylada.dftcrystal.input.Slabcut` is an operation to create a thin-film from a 3d bulk material. Finally, the whole "data+functions" object can be evaluated with :py:meth:`eval`. This will return a :py:class:`~pylada.crystal.cppwrappers.Structure` instance which can be used with other Pylada functionalities. Internally, :py:meth:`eval` makes a call to CRYSTAL_ and greps the output to construct the output structure. :param symmgroup: String or integer denoting the space-group. :param args: Lattice parameters :math:`a`, :math:`b`, :math:`c`, :math:`\\alpha`, :math:`\\beta`, :math:`\\gamma`. In practice, these parameters are listed as given in the third (or fourth) line of the input to CRYSTAL_. Hence it should conform to the same syntax and the same order. :param shift: Optional. Non-standard shift of the origin. Should be a sequence of three floats. .. seealso:: For operations, see :py:mod:`~pylada.dftcrystal.geometry`. .. _functional: http://en.wikipedia.org/wiki/Functional_programming .. _imperative: http://en.wikipedia.org/wiki/Imperative_programming """ keyword = 'crystal' """ CRYSTAL keyword. """ def __init__(self, symmgroup=1, *args, **kwargs): """ Creates a crystal following the CRYSTAL philosophy. """ shift = kwargs.pop('shift', 0) super(Crystal, self).__init__(symmgroup, **kwargs) self.params = list(args) """ Lattice parameters. """ self.shift = shift """ Non-standard shift of the origin. """ @property def shift(self): """ Shift of the origin of the cell, - if 0 or second, second setting given international tables (equivalent to IFSO == 0). - if 1 or first, second setting given international tables (equivalent to IFSO == 1). - if a sequence of three, then an actual shift in reciprocal space * 24. """ return self._shift @shift.setter
[docs] def shift(self, value): from numpy import array from ..error import ValueError if hasattr(value, '__iter__'): self._shift = array(value, dtype='float64') elif value == 0 or value == 'second': self._shift = 0 elif value == 1 or value == 'first': self._shift = 1 else: raise ValueError('Unknown input value for shift.')
def __repr_header__(self): """ Dumps representation to string. """ # prints construction part. length = len('{0.__class__.__name__}('.format(self)) + 1 args = [repr(self.symmgroup)] for o in self.params: args.append('{0!r}'.format(o)) indent = ' '.join('' for i in xrange(length)) for key, value in self.__dict__.iteritems(): if key in ['atoms', 'symmgroup', 'params'] : continue if key == '_shift': key = 'shift' args.append('\\\n{2}{0}={1!r}'.format(key, value, indent)) return '{0.__class__.__name__}({1})'.format(self, ', '.join(args)) @property def raw(self): """ Raw input for structure. """ from ..physics.spacegroup import space_group as sg from ..error import ValueError from ..periodic_table import find as find_specie if hasattr(self.shift, '__iter__'): hasshift = 2 else: hasshift = self.shift try: symmgroup = int(self.symmgroup) except: if self.symmgroup not in sg: raise ValueError('Unknown space group {0.symmgroup}'.format(self)) symmgroup = sg[self.symmgroup].index # control flags + symmgroup result = '0 {0} {1}\n{2}\n'.format( getattr(self, 'ifhr', 0), \ hasshift, symmgroup ) # shift if needed, watch out for weird ass input. if hasshift == 2: shift = self.shift * 24.0 + 0.1 shift = [int(u) for u in shift] result += '{0[0]} {0[1]} {0[2]}\n'.format(shift) # lattice parameters. for o in self.params: result += '{0} '.format(o) # number of atoms + atoms result += '\n{0}\n'.format(len(self.atoms)) for atom in self.atoms: try: n = find_specie(name=atom.type).atomic_number except: try: n = int(atom.type) except: raise ValueError( 'Could not make sense of atomic type {0.type}.' \ .format(atom) ) result += '{0} {1.pos[0]} {1.pos[1]} {1.pos[2]}\n'.format(n, atom) return result @raw.setter def raw(self, value): """ Reads crystal input. """ from ..periodic_table import find as find_specie from numpy import array if not hasattr(value, '__iter__'): value = value.split('\n') data = value.pop(0).split() self.symmgroup, self.ifhr, shift = (int(u) for u in data[:3]) data = value.pop(0) if self.symmgroup == 0: self.symmgroup = int(data.split()[0]) else: self.symmgroup = data if shift > 1: self.shift = array(value.pop(0).split(), dtype='float64') / 24.0 else: self.shift = shift self.params = [float(u) for u in value.pop(0).split()] n = int(value.pop(0).split()[0]) self.atoms = [] for line in value[:n]: line = line.split() type = int(line[0]) if type < 100: type = find_specie(atomic_number=type).symbol self.add_atom(pos=[float(u) for u in line[1:4]], type=type)