Esempio n. 1
0
class Atoms(_atoms.Atoms, ase.Atoms):
    __doc__ = update_doc_string(
        _atoms.Atoms.__doc__,
        """
    The :class:`Atoms` class is a Pythonic wrapper over the auto-generated
    :class:`quippy._atoms.Atoms` class. Atoms object are usually
    constructed either by reading from an input file in one of the
    :ref:`fileformats`, or by using the structure creation functions in
    the :mod:`quippy.structures` or :mod:`ase.lattice` modules.

    For example to read from an :ref:`extendedxyz` file, use::

       from quippy.atoms import Atoms
       atoms = Atoms('filename.xyz')

    Or, to create an 8-atom bulk diamond cubic cell of silicon::

       from quippy.structures import diamond
       si_bulk = diamond(5.44, 14)

    The :class:`Atoms` class is inherited from the
    :class:`ase.atoms.Atoms` so has all the ASE Atoms attributes and
    methods. This means that quippy and ASE Atoms objects are fully
    interoperable.""",
        signature=
        'Atoms([symbols, positions, numbers, tags, momenta, masses, magmoms, charges, scaled_positions, cell, pbc, constraint, calculator, info, n, lattice, properties, params, fixed_size, **read_args])'
    )

    _cmp_skip_fields = [
        'own_this', 'ref_count', 'domain', 'connect', 'hysteretic_connect',
        'source'
    ]

    name_map = {'positions': 'pos', 'numbers': 'Z', 'charges': 'charge'}

    rev_name_map = dict(zip(name_map.values(), name_map.keys()))

    def __init__(self,
                 symbols=None,
                 positions=None,
                 numbers=None,
                 tags=None,
                 momenta=None,
                 masses=None,
                 magmoms=None,
                 charges=None,
                 scaled_positions=None,
                 cell=None,
                 pbc=None,
                 constraint=None,
                 calculator=None,
                 info=None,
                 n=None,
                 lattice=None,
                 properties=None,
                 params=None,
                 fixed_size=None,
                 set_species=True,
                 fpointer=None,
                 finalise=True,
                 **readargs):

        # check for mutually exclusive options
        if cell is not None and lattice is not None:
            raise ValueError('only one of cell and lattice can be present')

        if n is None:
            n = 0
        if cell is not None:
            lattice = cell.T
        if lattice is None:
            lattice = np.eye(3)

        from quippy import Dictionary
        if properties is not None and not isinstance(properties, Dictionary):
            properties = Dictionary(properties)
        if params is not None and not isinstance(params, Dictionary):
            params = Dictionary(params)

        _atoms.Atoms.__init__(self,
                              n=n,
                              lattice=lattice,
                              properties=properties,
                              params=params,
                              fixed_size=fixed_size,
                              fpointer=fpointer,
                              finalise=finalise)

        self._ase_arrays = PropertiesWrapper(self)

        # If first argument is quippy.Atoms instance, copy data from it
        if isinstance(symbols, self.__class__):
            self.copy_from(symbols)
            symbols = None

        # Phonopy compatibility
        if 'phonopy' in available_modules:
            if symbols is not None and isinstance(symbols, PhonopyAtoms):
                atoms = symbols
                symbols = atoms.get_chemical_symbols()
                cell = atoms.get_cell()
                positions = atoms.get_positions()
                masses = atoms.get_masses()

        # Try to read from first argument, if it's not ase.Atoms
        if symbols is not None and not isinstance(symbols, ase.Atoms):
            self.read_from(symbols, **readargs)
            symbols = None

        ## ASE compatibility
        remove_properties = []

        if symbols is None and numbers is None:
            if self.has_property('z'):
                numbers = self.z.view(np.ndarray)
            else:
                numbers = [0] * len(self)
                remove_properties.append('Z')

        if symbols is None and positions is None:
            if self.has_property('pos'):
                positions = self.pos.view(np.ndarray).T
            else:
                remove_properties.append('pos')

        # Make sure argument to ase.Atoms constructor are consistent with
        # properties already present in this Atoms object
        if symbols is None and momenta is None and self.has_property(
                'momenta'):
            momenta = self.get_momenta()
        if symbols is None and masses is None and self.has_property('masses'):
            masses = self.get_masses()
        if symbols is None and cell is None:
            cell = self.lattice.T.view(np.ndarray)
        if symbols is None and pbc is None:
            pbc = self.get_pbc()
        if charges is None and self.has_property('charge'):
            charges = self.charge.view(np.ndarray)

        ase.Atoms.__init__(self, symbols, positions, numbers, tags, momenta,
                           masses, magmoms, charges, scaled_positions, cell,
                           pbc, constraint, calculator)

        # remove anything that ASE added that we don't want
        for p in remove_properties:
            self.remove_property(p)

        if isinstance(symbols, ase.Atoms):
            self.copy_from(symbols)

        ## end ASE compatibility

        if set_species and self.has_property('Z'):
            if not self.has_property('species'):
                self.add_property('species', ' ' * TABLE_STRING_LENGTH)
            if self.n != 0 and not (self.z == 0).all():
                self.set_atoms(self.z)  # initialise species from z

        if info is not None:
            self.params.update(info)

        self._initialised = True

        # synonyms for backwards compatibility
        self.neighbours = self.connect
        self.hysteretic_neighbours = self.hysteretic_connect

    def set_atomic_numbers(self, numbers, set_species=True):
        """Set atomic numbers and optionally also species property (default True)"""
        # override ase.Atoms.set_atomic_numbers() to keep QUIP Z and species in sync
        ase.Atoms.set_atomic_numbers(self, numbers)
        if set_species:
            if not self.has_property('species'):
                self.add_property('species', ' ' * TABLE_STRING_LENGTH)
            if self.n != 0 and not (self.z == 0).all():
                self.set_atoms(self.z)  # set species from Z

    def set_chemical_symbols(self, symbols, set_species=True):
        """Set chemical symbols - sets Z and optionally also species properties (default True)"""
        # override ase.Atoms.set_chemical_symbols() to keep QUIP Z and species in sync
        ase.Atoms.set_chemical_symbols(self, symbols)
        if set_species:
            if not self.has_property('species'):
                self.add_property('species', ' ' * TABLE_STRING_LENGTH)
            if self.n != 0 and not (self.z == 0).all():
                self.set_atoms(self.z)  # set species from Z

    def new_array(self, name, a, dtype=None, shape=None):
        # we overrride ase.Atoms.new_array() to allow "special" arrays
        # like "numbers", "positions" to be added more than once without
        # raising a RuntimeError
        if name in self.name_map and name in self.arrays:
            self.arrays[name] = a
            return
        ase.Atoms.new_array(self, name, a, dtype, shape)

    def set_lattice(self, lattice, scale_positions=False):
        """Change the lattice vectors, keeping the inverse lattice vectors
           up to date. Optionally map the existing atoms into the new cell
           and recalculate connectivity (by default scale_positions=False)."""
        _atoms.Atoms.set_lattice(self, lattice, scale_positions)

    def _get_cell(self):
        """Get ASE cell from QUIP lattice"""
        return self.lattice.view(np.ndarray).T

    def _set_cell(self, cell):
        """Set QUIP lattice from ASE cell"""
        self.set_lattice(cell.T, scale_positions=False)

    _cell = property(_get_cell, _set_cell)

    def _set_pbc(self, pbc):
        self.is_periodic = np.array(pbc).astype(int)

    def _get_pbc(self):
        return self.is_periodic.view(np.ndarray) == QUIPPY_TRUE

    _pbc = property(_get_pbc, _set_pbc)

    def _get_ase_arrays(self):
        """Provides access to ASE arrays, stored in QUIP properties dict"""

        return self._ase_arrays

    def _set_ase_arrays(self, value):
        """Set ASE arrays. Does not remove existing QUIP properties."""

        self._ase_arrays.update(value)

    arrays = property(_get_ase_arrays, _set_ase_arrays)

    def _get_info(self):
        """ASE info dictionary

        Entries are actually stored in QUIP params dictionary."""

        return self.params

    def _set_info(self, value):
        """Set ASE info dictionary.

        Entries are actually stored in QUIP params dictionary.
        Note that clearing Atoms.info doesn't empty params
        """

        self.params.update(value)

    info = property(_get_info, _set_info)

    def _indices(self):
        """Return array of atoms indices

        If global ``fortran_indexing`` is True, returns FortranArray containing
        numbers 1..self.n.  Otherwise, returns a standard numpuy array
        containing numbers in range 0..(self.n-1)."""

        if get_fortran_indexing():
            return farray(list(frange(len(self))))
        else:
            return np.array(list(range(len(self))))

    indices = property(_indices)

    def iteratoms(self):
        """Iterate over atoms, calling get_atom() for each one"""
        for i in self.indices:
            yield self.get_atom(i)

    def equivalent(self, other):
        """Test for equivalence of two Atoms objects.

        Equivalence is less strong than equality.  Equality (written
        `self == other`) requires all properties and parameters to be
        equal. Equivalence requires only that the number of atoms,
        positions, atomic numbers, unit cell and periodic boundary
        conditions match.

        .. note::

            The quippy expression a.equivalent(b) has the same
            definition as a == b in ASE. This means that a quippy.Atoms
            instance can be compared with an ase.Atoms instance using
            this method.
        """

        try:
            a = self.arrays
            b = other.arrays
            return (len(self) == len(other)
                    and (a['positions'] == b['positions']).all()
                    and (a['numbers'] == b['numbers']).all()
                    and (self._cell == other.cell).all()
                    and (self._pbc == other.pbc).all())
        except AttributeError:
            return False

    @classmethod
    def read(cls, source, format=None, **kwargs):
        """
        Class method to read Atoms object from file `source` according to `format`

        If `format` is None, filetype is inferred from filename.
        Returns a new Atoms instance; to read into an existing Atoms
        object, use the read_from() method.

        If `source` corresponds to a known format then it used
        to construct an appropriate iterator from the :attr:`AtomsReaders`
        dictionary. See :ref:`fileformats` for a list of supported
        file formats.

        If `source` corresponds to an unknown format then it is
        expected to be an iterator returning :class:`Atoms` objects.
        """

        if isinstance(source, basestring) and '@' in os.path.basename(source):
            source, frame = source.split('@')
            if source.endswith('.db'):
                source = source + '@' + frame
                format = 'db'
            else:
                frame = parse_slice(frame)
                if 'frame' in kwargs:
                    raise ValueError(
                        "Conflicting frame references given: kwarg frame=%r and @-reference %r"
                        % (kwargs['frame'], frame))
                if not isinstance(frame, int):
                    raise ValueError(
                        "Frame @-reference %r does not resolve to single frame"
                        % frame)
                kwargs['frame'] = frame

        from quippy.io import AtomsReaders
        filename, source, format = infer_format(source, format, AtomsReaders)

        opened = False
        if format in AtomsReaders:
            source = AtomsReaders[format](source, format=format, **kwargs)
            opened = True

        if isinstance(source, basestring):
            raise IOError("Don't know how to read from file '%s'" % source)
        if not hasattr(source, '__iter__'):
            raise IOError('Cannot read from %r - not an iterator' % source)

        at = iter(source).next()
        if not isinstance(at, cls):
            raise ValueError('Object %r read from  %r is not Atoms instance' %
                             (at, source))
        if opened and hasattr(source, 'close'):
            source.close()
        if filename is not None:
            at.filename = filename
        return at

    def write(self,
              dest=None,
              format=None,
              properties=None,
              prefix=None,
              **kwargs):
        """
        Write this :class:`Atoms` object to `dest`. If `format` is
        absent it is inferred from the file extension or type of `dest`,
        as described for the :meth:`read` method.  If `properties` is
        present, it should be a list of property names to include in the
        output file, e.g. `['species', 'pos']`.

        See :ref:`fileformats` for a list of supported file formats.
        """

        if dest is None:
            # if filename is missing, save back to file from
            # which we loaded configuration
            if hasattr(self, 'filename'):
                dest = self.filename
            else:
                raise ValueError("No 'dest' and Atoms has no stored filename")

        from quippy.io import AtomsWriters
        filename, dest, format = infer_format(dest, format, AtomsWriters)
        opened = filename is not None

        if format in AtomsWriters:
            dest = AtomsWriters[format](dest, **kwargs)

        if not hasattr(dest, 'write'):
            raise ValueError(
                'Don\'t know how to write to "%s" in format "%s"' %
                (dest, format))

        write_kwargs = {}
        if properties is not None:
            write_kwargs['properties'] = properties
        if prefix is not None:
            write_kwargs['prefix'] = prefix
        try:
            res = dest.write(self, **write_kwargs)
        except TypeError:
            raise ValueError('destination %r doesn\'t support arguments %r' %
                             (dest, write_kwargs))

        if opened and hasattr(dest, 'close'):
            dest.close()
        return res

    def select(self, mask=None, list=None, orig_index=True):
        """Return a new :class:`Atoms` containing a subset of the atoms in this Atoms object

        One of either `mask` or `list` should be present.  If `mask`
        is given it should be a rank one array of length `self.n`. In
        this case atoms corresponding to true values in `mask` will be
        included in the result.  If `list` is present it should be an
        arry of list containing atom indices to include in the result.

        If `orig_index` is True (default), the new object will contain
        an ``orig_index`` property mapping the indices of the new atoms
        back to the original larger Atoms object.
        """
        if mask is not None:
            mask = farray(mask)
            out = self.__class__(n=mask.sum(),
                                 lattice=self.lattice,
                                 properties={},
                                 params={})
            _atoms.Atoms.select(out, self, mask=mask, orig_index=orig_index)
        elif list is not None:
            list = farray(list)
            out = self.__class__(n=len(list), lattice=self.lattice)
            _atoms.Atoms.select(out, self, list=list, orig_index=orig_index)
        else:
            raise ValueError('Either mask or list must be present.')
        return out

    def copy(self):
        """
        Return a copy of this :class:`Atoms` object
        """

        other = self.__class__(n=self.n,
                               lattice=self.lattice,
                               properties=self.properties,
                               params=self.params)

        # copy any normal (not Fortran) attributes
        for k, v in self.__dict__.iteritems():
            if not k.startswith('_') and k not in other.__dict__:
                other.__dict__[k] = v

        # from _atoms.Atoms
        other.cutoff = self.cutoff
        other.cutoff_skin = self.cutoff_skin
        other.nneightol = self.nneightol

        # from ase.Atoms
        other.constraints = copy.deepcopy(self.constraints)
        other.adsorbate_info = copy.deepcopy(self.adsorbate_info)
        return other

    def copy_from(self, other):
        """Replace contents of this Atoms object with data from `other`."""

        self.__class__.__del__(self)
        if isinstance(other, _atoms.Atoms):
            _atoms.Atoms.__init__(self,
                                  n=other.n,
                                  lattice=other.lattice,
                                  properties=other.properties,
                                  params=other.params)

            self.cutoff = other.cutoff
            self.cutoff_skin = other.cutoff_skin
            self.nneightol = other.nneightol

        elif isinstance(other, ase.Atoms):
            _atoms.Atoms.__init__(self, n=0, lattice=np.eye(3))
            ase.Atoms.__init__(self, other)

            # copy params/info dicts
            if hasattr(other, 'params'):
                self.params.update(other.params)
            if hasattr(other, 'info'):
                self.params.update(other.info)
                if 'nneightol' in other.info:
                    self.nneightol = other.info['nneightol']
                if 'cutoff' in other.info:
                    self.set_cutoff(other.info['cutoff'],
                                    other.info.get('cutoff_break'))

            # create extra properties for any non-standard arrays
            standard_ase_arrays = [
                'positions', 'numbers', 'masses', 'charges', 'momenta', 'tags',
                'magmoms'
            ]

            for ase_name, value in other.arrays.iteritems():
                quippy_name = self.name_map.get(ase_name, ase_name)
                if ase_name not in standard_ase_arrays:
                    self.add_property(quippy_name, np.transpose(value))

            self.constraints = copy.deepcopy(other.constraints)
            self.adsorbate_info = copy.deepcopy(other.adsorbate_info)

        else:
            raise TypeError(
                'can only copy from instances of quippy.Atoms or ase.Atoms')

        # copy any normal (not Fortran) attributes
        for k, v in other.__dict__.iteritems():
            if not k.startswith('_') and k not in self.__dict__:
                self.__dict__[k] = v

    def read_from(self, source, **readargs):
        """Replace contents of this Atoms object with Atoms read from `source`"""
        try:
            self.copy_from(source)
        except TypeError:
            tmp = Atoms.read(source, **readargs)
            self.shallow_copy_from(tmp)
            # tmp goes out of scope here, but reference counting
            # prevents it from being free'd.

    def __getattr__(self, name):
        #print 'getattr', name
        #if name in self.properties:
        if name == '_fpointer':
            raise AttributeError('Atoms object not initialised!')
        try:
            return self.properties[name]
        except KeyError:
            try:
                return self.params[name]
            except KeyError:
                raise AttributeError('Unknown Atoms attribute %s' % name)

    def __setattr__(self, name, value):
        #print 'setattr', name, value
        if not '_initialised' in self.__dict__:
            object.__setattr__(self, name, value)
        elif self.properties._fpointer is not None and name in self.properties:
            self.properties[name][...] = value
        elif self.params._fpointer is not None and name in self.params:
            if self.params.is_array(name):
                self.params[name][...] = value
            else:
                self.params[name] = value
        else:
            object.__setattr__(self, name, value)

    def md5_hash(self, ndigits):
        """Hash an atoms object with a precision of ndigits decimal
        digits.  Atomic numbers, lattice and fractional positions are
        fed to MD5 to form the hash."""
        def rounded_string_rep(a, ndigits):
            return np.array2string(a, precision=ndigits,
                                   suppress_small=True).replace(
                                       '-0. ', ' 0. ')

        # Compute fractional positions, round them to ndigits, then sort them
        # for hash stability
        flat_frac_pos = np.dot(self.g, self.pos).flatten()
        flat_frac_pos.sort()

        # md5 module deprecated in Python 2.5 and later
        try:
            import hashlib
            md5 = hashlib.md5
        except ImportError:
            import md5
            md5 = md5.new

        m = md5()
        m.update(rounded_string_rep(self.lattice.flatten(), ndigits))
        m.update(str(self.z))
        m.update(rounded_string_rep(flat_frac_pos, ndigits))

        return m.hexdigest()

    def __hash__(self):
        return hash(self.md5_hash(4))

    #def __getitem__(self, i):
    # we override ase.Atoms.__getitem__ so we can raise
    # exception if we're using fortran indexing
    #    if self.fortran_indexing:
    #        raise RuntimeError('Atoms[i] inconsistent with fortran indexing')
    #    return ase.Atoms.__getitem__(self, i)

    def get_atom(self, i):
        """Return a dictionary containing the properties of the atom with
           index `i`. If fortran_indexing=True (the default), `i` should be in
           range 1..self.n, otherwise it should be in range 0..(self.n-1)."""
        if (get_fortran_indexing() and (i < 1 or i > self.n)) or \
            (not get_fortran_indexing() and (i < 0 or i > self.n-1)):
            raise IndexError('Atoms index out of range')
        atom = {}
        atom['_index'] = i
        atom['atoms'] = self
        for k in self.properties.keys():
            v = self.properties[k][..., i]
            if isinstance(v, np.ndarray):
                if v.dtype.kind == 'S':
                    v = ''.join(v).strip()
                elif v.shape == ():
                    v = v.item()
            atom[k.lower()] = v
        return atom

    def print_atom(self, i):
        """Pretty-print the properties of the atom with index `i`"""
        at = self.get_atom(i)
        title = 'Atom %d' % at['_index']
        title = title + '\n' + '-' * len(title) + '\n\n'
        fields = [
            '%-15s =  %s' % (k, at[k]) for k in sorted(at.keys())
            if k not in ['_index', 'atoms']
        ]
        print title + '\n'.join(fields)

    def density(self):
        """Density in units of :math:`g/m^3`. If `mass` property exists,
           use that, otherwise we use `z` and ElementMass table."""
        if self.has_property('mass'):
            mass = sum(self.mass) / MASSCONVERT / 1.0e3
        else:
            mass = sum(ElementMass[z] for z in self.z) / MASSCONVERT / 1.0e3

        return mass / (N_A * self.cell_volume() * 1.0e-30) / 1.0e3

    def add_property(self,
                     name,
                     value,
                     n_cols=None,
                     overwrite=None,
                     property_type=None):
        """
        Add a new property to this Atoms object.

        `name` is the name of the new property and `value` should be
        either a scalar or an array representing the value, which should
        be either integer, real, logical or string.

        If a scalar is given for `value` it is copied to every element
        in the new property.  `n_cols` can be specified to create a 2D
        property from a scalar initial value - the default is 1 which
        creates a 1D property.

        If an array is given for `value` it should either have shape
        (self.n,) for a 1D property or (n_cols,self.n) for a 2D
        property.  In this case `n_cols` is inferred from the shape of
        the `value` and shouldn't be passed as an argument.

        If `property_type` is present, then no attempt is made to
        infer the type from `value`. This is necessary to resolve
        ambiguity between integer and logical types.

        If property with the same type is already present then no error
        occurs.If `overwrite` is true, the value will be overwritten with
        that given in `value`, otherwise the old value is retained.

        Here are some examples::

            a = Atoms(n=10, lattice=10.0*fidentity(3))

            a.add_property('mark', 1)                  # Scalar integer
            a.add_property('bool', False)              # Scalar logical
            a.add_property('local_energy', 0.0)        # Scalar real
            a.add_property('force', 0.0, n_cols=3)     # Vector real
            a.add_property('label', '')                # Scalar string

            a.add_property('count', [1,2,3,4,5,6,7,8,9,10])  # From list
            a.add_property('norm_pos', a.pos.norm())         # From 1D array
            a.add_property('pos', new_pos)                   # Overwrite positions with array new_pos
                                                             # which should have shape (3,10)
        """

        kwargs = {}
        if n_cols is not None: kwargs['n_cols'] = n_cols
        if overwrite is not None: kwargs['overwrite'] = overwrite

        if (isinstance(value, np.ndarray) and value.dtype.kind in ['O', 'S']
                and value.shape != (len(self), TABLE_STRING_LENGTH)):
            value = s2a(value.astype('str'), TABLE_STRING_LENGTH).T

        if property_type is None:
            _atoms.Atoms.add_property(self, name, value, **kwargs)

        else:
            # override value_ref if property_type is specified

            new_property = not self.has_property(name)

            type_to_value_ref = {
                T_INTEGER_A: 0,
                T_REAL_A: 0.0,
                T_CHAR_A: " " * TABLE_STRING_LENGTH,
                T_LOGICAL_A: False,
                T_INTEGER_A2: 0,
                T_REAL_A2: 0.0
            }
            try:
                value_ref = type_to_value_ref[property_type]
            except KeyError:
                raise ValueError('Unknown property_type %d' % property_type)

            if (hasattr(value, 'shape') and len(value.shape) == 2
                    and property_type != T_CHAR_A and n_cols is None):
                kwargs['n_cols'] = value.shape[0]

            _atoms.Atoms.add_property(self, name, value_ref, **kwargs)
            if new_property or overwrite:
                getattr(self, name.lower())[:] = value

    def __getstate__(self):
        return self.write('string')

    def __setstate__(self, state):
        self.read_from(state, format='string')

    def __reduce__(self):
        return (Atoms, (), self.__getstate__(), None, None)

    def mem_estimate(self):
        """Estimate memory usage of this Atoms object, in bytes"""

        sizeof_table = 320
        mem = sum([p.itemsize * p.size for p in self.properties.values()])
        if self.connect.initialised:
            c = self.connect
            mem += sizeof_table * self.n * 2  # neighbour1 and neighbour2 tables
            mem += 32 * c.n_neighbours_total()  # neighbour data
            mem += c.cell_heads.size * c.cell_heads.itemsize  # cell data

        return mem

    def extend(self, other):
        """Extend atoms object by appending atoms from *other*."""
        # modified version of ase.Atoms.extend() to work with QUIP data storage
        if isinstance(other, ase.Atom):
            other = self.__class__([other])

        n1 = len(self)
        n2 = len(other)

        # first make a copy of self.arrays so that we can resize Atoms
        arrays = dict([(key, value.copy())
                       for (key, value) in self.arrays.items()])

        atomslog.debug('old arrays %r' % arrays)

        for name, a1 in arrays.items():
            a = np.zeros((n1 + n2, ) + a1.shape[1:], a1.dtype)
            a[:n1] = a1
            if name == 'masses':
                a2 = other.get_masses()
            else:
                a2 = other.arrays.get(name)
            if a2 is not None:
                a[n1:] = a2
            self.arrays[name] = a

        for name, a2 in other.arrays.items():
            if name in self.arrays:
                continue
            a = np.empty((n1 + n2, ) + a2.shape[1:], a2.dtype)
            a[n1:] = a2
            if name == 'masses':
                a[:n1] = self.get_masses()[:n1]
            else:
                a[:n1] = 0

            self.set_array(name, a)
        atomslog.debug('new arrays %r' % self.arrays)

        return self

    __iadd__ = extend

    def __imul__(self, m):
        """In-place repeat of atoms."""
        # modified version of ase.Atoms.extend() to work with QUIP data storage
        if isinstance(m, int):
            m = (m, m, m)

        M = np.product(m)
        n = len(self)

        # first make a copy of self.arrays so that we can resize Atoms
        arrays = dict([(key, value.copy())
                       for (key, value) in self.arrays.items()])

        for name, a in arrays.items():
            self.arrays[name] = np.tile(a, (M, ) + (1, ) * (len(a.shape) - 1))

        positions = self.arrays['positions']
        i0 = 0
        for m0 in range(m[0]):
            for m1 in range(m[1]):
                for m2 in range(m[2]):
                    i1 = i0 + n
                    positions[i0:i1] += np.dot((m0, m1, m2), self._cell)
                    i0 = i1

        if self.constraints is not None:
            self.constraints = [c.repeat(m, n) for c in self.constraints]

        self._cell = np.array([m[c] * self._cell[c] for c in range(3)])

        return self
Esempio n. 2
0
class Connection(_atoms.Connection):
    __doc__ = update_doc_string(
        _atoms.Connection.__doc__, """
    The :class:`Connection` is a subclass of :class:`_atoms.Connection`
    which adds supports for iteration over all atoms, and indexing
    e.g. ``at.connect.neighbours[1]`` returns a list of the neighbours of
    the atom with index 1.

    When indexed with an integer from 1 to `at.n`, returns an array
    of :class:`NeighbourInfo` objects, each of which corresponds to
    a particular pair `(i,j)` and has attributes `j`, `distance`,
    `diff`, `cosines` and `shift`.

    If ``fortran_indexing`` is True, atom and neighbour indices
    start from 1; otherwise they are numbered from zero.

    If connectivity information has not already been calculated
    :meth:`calc_connect` will be called automatically. The code to
    loop over the neighbours of all atoms is quite idiomatic::

        for i in at.indices:
           for neighb in at.connect[i]:
               print (neighb.j, neighb.distance, neighb.diff,
                      neighb.cosines, neighb.shift)

    Note that this provides a more Pythonic interface to the atomic
    connectivity information than the wrapped Fortran functions
    :meth:`Atoms.n_neighbours` and :meth:`Atoms.neighbour`.
    """)

    # def __init__(self, n=None, nbuffer=None, pos=None,
    #              lattice=None, g=None, origin=None,
    #              extent=None, nn_guess=None, fill=None,
    #              fpointer=None, finalise=True):
    #     _atoms.Connection.__init__(self, n, nbuffer, pos,
    #                                lattice, g, origin,
    #                                extent, nn_guess, fill,
    #                                fpointer, finalise)

    def is_neighbour(self, i, j):
        return (i, j) in self.pairs()

    def pairs(self):
        """Yield pairs of atoms (i,j) with i < j which are neighbours"""
        for i, neighbour_list in zip(self.parent.indices,
                                     self.iterneighbours()):
            for neighb in neighbour_list:
                if i < neighb.j:
                    yield (i, neighb.j)

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False

        # Neighbours are considered to be equal if *topology* matches,
        # not distances, displacement vectors and shifts.
        return sorted(self.pairs()) == sorted(other.pairs())

    def __ne__(self, other):
        return not self.__eq__(other)

    def __iter__(self):
        return self.iterneighbours()

    def iterneighbours(self):
        """Iterate over the neighbours of all atoms"""
        for i in self.parent.indices:
            yield self[i]

    def __getitem__(self, i):
        if not self.initialised:
            if self is self.parent.hysteretic_connect:
                self.calc_connect_hysteretic(self.parent)
            else:
                self.calc_connect(self.parent)

        distance = farray(0.0)
        diff = fzeros(3)
        cosines = fzeros(3)
        shift = fzeros(3, dtype=np.int32)

        res = []
        if not get_fortran_indexing():
            i = i + 1  # convert to 1-based indexing

        for n in frange(self.n_neighbours(i)):
            j = self.neighbour(self.parent, i, n, distance, diff, cosines,
                               shift)
            if not get_fortran_indexing():
                j = j - 1
            res.append(NeighbourInfo(j, distance, diff, cosines, shift))

        if get_fortran_indexing():
            res = farray(res)  # to give 1-based indexing
        return res

    def distances(self, Z1=None, Z2=None):
        """Distances between pairs of neighbours, optionally
           filtered by species (Z1,Z2)"""
        for i in self.parent.indices:
            for neighb in self[i]:
                if neighb.j > i: continue
                if Z1 is not None and Z2 is not None:
                    if sorted(
                        (self.parent.z[i], self.parent.z[neighb.j])) == sorted(
                            (Z1, Z2)):
                        yield neighb.distance
                else:
                    yield neighb.distance

    def get_neighbours(self, i):
        """
        Return neighbours of atom i

        Return arrays of indices and offsets to neighbouring
        atoms. The positions of the neighbor atoms can be calculated like
        this::

            indices, offsets = atoms.connect.get_neighbors(42)
            for i, offset in zip(indices, offsets):
               print atoms.positions[i] + dot(offset, atoms.get_cell())

        Compatible with ase.calculators.neighborlist.NeighborList.get_neighbors(),
        providing that NeighborList is constructed with bothways=True and
        self_interaction=False.
        """
        neighbours = self[i]
        indices = np.array([n.j for n in neighbours])
        offsets = np.r_[[n.shift for n in neighbours]]
        return (indices, offsets)

    def get_neighbors(self, i):
        """
        Variant spelling of :meth:`get_neighbours`
        """
        return self.get_neighbours(i)
Esempio n. 3
0
class Descriptor(RawDescriptor):
    __doc__ = update_doc_string(
        RawDescriptor.__doc__,
        """Pythonic wrapper for GAP descriptor module""",
        signature='Descriptor(args_str)')

    def __init__(self, args_str=None, **init_args):
        """
        Initialises Descriptor object and calculate number of dimensions and
        permutations.
        """
        if args_str is None:
            args_str = dict_to_args_str(init_args)
        RawDescriptor.__init__(self, args_str)
        self._n_dim = self.dimensions()
        self._n_perm = self.n_permutations()

    #: Number of dimensions
    n_dim = property(lambda self: self._n_dim)
    #: Number of permutations
    n_perm = property(lambda self: self._n_perm)

    def __len__(self):
        return self.n_dim

    def permutations(self):
        """
        Returns array containing all valid permutations of this descriptor.
        """
        perm = RawDescriptor.permutations(self, self.n_dim, self.n_perm)
        return np.array(perm).T

    @convert_atoms_types_iterable_method
    def count(self, at):
        """
        Returns how many descriptors of this type are found in the Atoms
        object.
        """
        return self.descriptor_sizes(at)[0]

    @convert_atoms_types_iterable_method
    def calc_descriptor(self, at, args_str=None, **calc_args):
        """
        Calculates all descriptors of this type in the Atoms object, and
        returns the array of descriptor values. Does not compute gradients; use
        calc(at, grad=True, ...) for that.
        """
        return self.calc(at, False, args_str, **calc_args).descriptor

    @convert_atoms_types_iterable_method
    def calc(self, at, grad=False, args_str=None, **calc_args):
        """
        Calculates all descriptors of this type in the Atoms object, and
        gradients if grad=True. Results can be accessed dictionary- or
        attribute-style; 'descriptor' contains descriptor values, 
        'descriptor_index_0based' contains the 0-based indices of the central 
        atom(s) in each descriptor, 'grad' contains gradients, 
        'grad_index_0based' contains indices to gradients (descriptor, atom).
        Cutoffs and gradients of cutoffs are also returned.
        """
        if args_str is None:
            args_str = dict_to_args_str(calc_args)

        n_index = fzeros(1, 'i')
        n_desc, n_cross = self.descriptor_sizes(at, n_index=n_index)
        n_index = n_index[1]
        data = fzeros((self.n_dim, n_desc))
        cutoff = fzeros(n_desc)
        data_index = fzeros((n_index, n_desc), 'i')

        if grad:
            # n_cross is number of cross-terms, proportional to n_desc
            data_grad = fzeros((self.n_dim, 3, n_cross))
            data_grad_index = fzeros((2, n_cross), 'i')
            cutoff_grad = fzeros((3, n_cross))

        if not grad:
            RawDescriptor.calc(self,
                               at,
                               descriptor_out=data,
                               covariance_cutoff=cutoff,
                               descriptor_index=data_index,
                               args_str=args_str)
        else:
            RawDescriptor.calc(self,
                               at,
                               descriptor_out=data,
                               covariance_cutoff=cutoff,
                               descriptor_index=data_index,
                               grad_descriptor_out=data_grad,
                               grad_descriptor_index=data_grad_index,
                               grad_covariance_cutoff=cutoff_grad,
                               args_str=args_str)

        results = DescriptorCalcResult()
        convert = lambda data: np.array(data).T
        results.descriptor = convert(data)
        results.cutoff = convert(cutoff)
        results.descriptor_index_0based = convert(data_index - 1)
        if grad:
            results.grad = convert(data_grad)
            results.grad_index_0based = convert(data_grad_index - 1)
            results.cutoff_grad = convert(cutoff_grad)

        return results
Esempio n. 4
0
class Dictionary(DictMixin, ParamReaderMixin, _dictionary.Dictionary):
    __doc__ = update_doc_string(_dictionary.Dictionary.__doc__, """
    The quippy Python :class:`Dictionary` class is designed to behave
    as much as possible like a true Python dictionary, but since it is
    implemented in Fortran it can only store a restricted range of
    data types.  Keys much be strings and values must be one of the
    types above.

    Trying to store any other type of data will raise a :exc:`ValueError`.

    For Atoms' :attr:`~quippy.atoms.Atoms.params` entries, there are
    further restrictions imposed by the implementation of the XYZ and
    NetCDF I/O routines. The only types of data that can be stored here
    are:

    - Integer
    - Real
    - String
    - Integer 3-vector
    - Real 3-vector
    - Integer 3 x 3 matrix
    - Real 3 x 3 matrix

    A :class:`Dictionary` can be created from a standard Python dictionary,
    and easily converted back::

        >>> py_dict = {'a':1, 'b':2}
        >>> fortran_dict = Dictionary(py_dict)
        >>> py_dict == dict(fortran_dict)
        True

    It also supports all the standard dictionary operations and methods::

        >>> fortran_dict['c'] = 3
	>>> fortran_dict.keys()
	['a', 'b', 'c']

    An additional feature of the quippy :class:`Dictionary` is that it
    can read and write itself to a string in the format used within XYZ
    files::

        >>> str(fortran_dict)
	'a=1 b=2 c=3'
	>>> d2 = Dictionary('a=1 b=2 c=3')
	>>> d2.keys(), d2.values()
	(['a', 'b', 'c'], [1, 2, 3])
    """, signature='Dictionary([D])')

    _interfaces = _dictionary.Dictionary._interfaces
    _interfaces['set_value'] = [ k for k in _dictionary.Dictionary._interfaces['set_value'] if k[0] != 'set_value_s_a' ]

    _scalar_types = (T_INTEGER, T_REAL, T_COMPLEX, T_LOGICAL, T_CHAR, T_DICT)

    _array_types  = (T_INTEGER_A, T_REAL_A, T_COMPLEX_A, T_CHAR_A,
                     T_LOGICAL_A, T_INTEGER_A2, T_REAL_A2)

    def __init__(self, D=None, *args, **kwargs):
        _dictionary.Dictionary.__init__(self, *args, **kwargs)
        self._cache = {}
        self.key_cache_invalid = 1
        self._keys = []
        self._keys_lower = []
        if D is not None:
            self.read(D) # copy from D

    def keys(self):
        if self.key_cache_invalid or len(self._keys) != self.n: # HACK to solve shallow copy bug
            self._keys = [self.get_key(i).strip() for i in frange(self.n)]
            self._keys_lower = [k.lower() for k in self._keys]
            self.key_cache_invalid = 0
        return self._keys

    def has_key(self, key):
        k = self.keys() # ensure _keys_lower is up-to-date
        return key.lower() in self._keys_lower

    def get_value(self, k):
        "Return a _copy_ of a value stored in Dictionary"
        if not k in self:
            raise KeyError('Key "%s" not found ' % k)

        t, s, s2 = self.get_type_and_size(k)

        if t == T_NONE:
            v = None
        elif t == T_INTEGER:
            v,p = self._get_value_i(k)
        elif t == T_REAL:
            v,p = self._get_value_r(k)
        elif t == T_COMPLEX:
            v,p = self._get_value_c(k)
        elif t == T_CHAR:
            v,p = self._get_value_s(k)
            v = v.strip()
        elif t == T_LOGICAL:
            v,p = self._get_value_l(k)
            v = bool(v)
        elif t == T_INTEGER_A:
            v,p = self._get_value_i_a(k,s)
        elif t == T_REAL_A:
            v,p = self._get_value_r_a(k,s)
        elif t == T_COMPLEX_A:
            v,p = self._get_value_c_a(k,s)
        elif t == T_CHAR_A:
            v,p = self._get_value_s_a2(k,s2[1], s2[2])
            v = v[...,1]  # Last index is length of string, here fixed to 1
            v.strides = (1, v.shape[0])  # Column-major storage
        elif t == T_LOGICAL_A:
            v,p = self._get_value_l_a(k,s)
            v = farray(v, dtype=bool)
        elif t == T_INTEGER_A2:
            v,p = self._get_value_i_a2(k, s2[1], s2[2])
        elif t == T_REAL_A2:
            v,p = self._get_value_r_a2(k, s2[1], s2[2])
        elif t == T_DICT:
            v,p = self._get_value_dict(k)
        else:
            raise ValueError('Unsupported dictionary entry type %d' % t)
        return v

    def get_array(self, key):
        "Return a _reference_ to an array stored in this Dictionary"""

        import _quippy, arraydata
        if key in self and self.get_type_and_size(key)[0] in Dictionary._array_types:
            a = arraydata.get_array(self._fpointer, _quippy.qp_dictionary_get_array, key)
            if get_fortran_indexing():
                a = FortranArray(a, parent=self)
            return a
        else:
            raise KeyError('Key "%s" does not correspond to an array entry' % key)

    def get_type(self, key):
        "Return an integer code for the type of the value associated with a key"

        import _quippy, arraydata
        if key in self:
            return self.get_type_and_size(key)[0]
        else:
            raise KeyError('Key "%s" not found' % key)


    def is_scalar(self, key):
        if key in self:
            return self.get_type_and_size(key)[0] in Dictionary._scalar_types
        else:
            raise KeyError('Key "%s" not found')

    def is_array(self, key):
        if key in self:
            return self.get_type_and_size(key)[0] in Dictionary._array_types
        else:
            raise KeyError('Key "%s" not found')


    def __getitem__(self, k):
        k = k.lower()

        if self.cache_invalid:
            self._cache = {}
            self.cache_invalid = 0

        try:
            v = self._cache[k]
            if v is None: raise KeyError
            return v

        except KeyError:
            if not k in self:
                raise KeyError('Key "%s" not found ' % k)

            t = self.get_type_and_size(k)[0]

            if t == T_NONE:
                return None

            elif t in Dictionary._scalar_types:
                self._cache[k] = None
                return self.get_value(k)

            elif t in Dictionary._array_types:
                v = self.get_array(k)
                self._cache[k] = v
                return v

            else:
                raise ValueError('Unsupported dictionary entry type %d' % t)


    def __setitem__(self, k, v):
        k = str(k)
        if isinstance(v, basestring):
            v = str(v)
        if v is None:
            self.set_value(k)
        else:
            try:
                self.set_value(k, v)
            except TypeError:
                self.set_value(k,s2a(v,pad=None))

    def __delitem__(self, k):
        if not k in self:
            raise KeyError('Key %s not found in Dictionary' % k)
        self.remove_value(k)

    def __repr__(self):
        return ParamReaderMixin.__repr__(self)

    def __eq__(self, other):
        import logging
        if sorted(self.keys()) != sorted(other.keys()):
            logging.debug('keys mismatch: %s != %s' % (sorted(self.keys()), sorted(other.keys())))
            return False

        for key in self:
            v1, v2 = self[key], other[key]
            if isinstance(v1, np.ndarray) and isinstance(v2, np.ndarray):
                if v1.size == 0 and v2.size == 0:
                    continue
                elif v1.dtype.kind != 'f':
                    if (v1 != v2).any():
                        logging.debug('mismatch key=%s v1=%s v2=%s' % (key, v1, v2))
                        return False
                else:
                    if abs(v1 - v2).max() > self._cmp_tol:
                        logging.debug('mismatch key=%s v1=%s v2=%s' % (key, v1, v2))
                        return False
            else:
                if v1 != v2:
                    logging.debug('mismatch key=%s v1=%s v2=%s' % (key, v1, v2))
                    return False
        return True

    def __ne__(self, other):
        return not self.__eq__(other)

    def __str__(self):
        return ParamReaderMixin.__str__(self)

    def copy(self):
        return Dictionary(self)

    def subset(self, keys, out=None, case_sensitive=None, out_no_initialise=None):
        if out is None: out = Dictionary()
        _dictionary.Dictionary.subset(self, keys, out, case_sensitive, out_no_initialise)
        return out
Esempio n. 5
0
class Potential(_potential.Potential, Calculator):
    __doc__ = update_doc_string(
        _potential.Potential.__doc__,
        r"""
The :class:`Potential` class also implements the ASE
:class:`ase.calculators.interface.Calculator` interface via the
the :meth:`get_forces`, :meth:`get_stress`, :meth:`get_stresses`,
:meth:`get_potential_energy`, :meth:`get_potential_energies`
methods. For example::

    atoms = diamond(5.44, 14)
    atoms.rattle(0.01)
    atoms.set_calculator(pot)
    forces = atoms.get_forces()
    print forces

Note that the ASE force array is the transpose of the QUIP force
array, so has shape (len(atoms), 3) rather than (3, len(atoms)).

The optional arguments `pot1`, `pot2` and `bulk_scale` are
used by ``Sum`` and ``ForceMixing`` potentials (see also
wrapper class :class:`ForceMixingPotential`)

An :class:`quippy.mpi_context.MPI_context` object can be
passed as the `mpi_obj` argument to restrict the
parallelisation of this potential to a subset of the

The `callback` argument is used to implement the calculation of
the :class:`Potential` in a Python function: see :meth:`set_callback` for
an example.

In addition to the builtin QUIP potentials, it is possible to
use any ASE calculator as a QUIP potential by passing it as
the `calculator` argument to the :class:`Potential` constructor, e.g.::

   from ase.calculators.morse import MorsePotential
   pot = Potential(calculator=MorsePotential)

`atoms` if given, is used to set the calculator associated
with `atoms` to the new :class:`Potential` instance, by calling
:meth:'.Atoms.set_calculator`.

.. note::

    QUIP potentials do not compute stress and per-atom stresses
    directly, but rather the virial tensor which has units of stress
    :math:`\times` volume, i.e. energy. If the total stress is
    requested, it is computed by dividing the virial by the atomic
    volume, obtained by calling :meth:`.Atoms.get_volume`. If per-atom
    stresses are requested, a per-atom volume is needed. By default
    this is taken to be the total volume divided by the number of
    atoms. In some cases, e.g. for systems containing large amounts of
    vacuum, this is not reasonable. The ``vol_per_atom`` calc_arg can
    be used either to give a single per-atom volume, or the name of an
    array in :attr:`.Atoms.arrays` containing volumes for each atom.

""",
        signature=
        'Potential(init_args[, pot1, pot2, param_str, param_filename, bulk_scale, mpi_obj, callback, calculator, atoms, calculation_always_required])'
    )

    callback_map = {}

    implemented_properties = [
        'energy', 'energies', 'forces', 'stress', 'stresses', 'numeric_forces',
        'elastic_constants', 'unrelaxed_elastic_constants'
    ]

    def __init__(self,
                 init_args=None,
                 pot1=None,
                 pot2=None,
                 param_str=None,
                 param_filename=None,
                 bulk_scale=None,
                 mpi_obj=None,
                 callback=None,
                 calculator=None,
                 atoms=None,
                 calculation_always_required=False,
                 fpointer=None,
                 finalise=True,
                 error=None,
                 **kwargs):

        self._calc_args = {}
        self._default_properties = []
        self.calculation_always_required = calculation_always_required
        Calculator.__init__(self, atoms=atoms)

        if callback is not None or calculator is not None:
            if init_args is None:
                init_args = 'callbackpot'

        param_dirname = None
        if param_filename is not None:
            param_str = open(param_filename).read()
            param_dirname = path.dirname(param_filename) or None

        if init_args is None and param_str is None:
            raise ValueError('Need one of init_args,param_str,param_filename')

        if init_args is not None:
            if init_args.lower().startswith('callbackpot'):
                if not 'label' in init_args:
                    init_args = init_args + ' label=%d' % id(self)
            else:
                # if param_str missing, try to find default set of QUIP params,
                # falling back on a do-nothing parameter string.
                if param_str is None and pot1 is None and pot2 is None:
                    try:
                        param_str = quip_xml_parameters(init_args)
                    except IOError:
                        param_str = r'<params></params>'

        if kwargs != {}:
            if init_args is not None:
                init_args = init_args + ' ' + dict_to_args_str(kwargs)
            else:
                init_args = dict_to_args_str(kwargs)

        # Change to the xml directory to initialise, so that extra files
        # like sparseX can be found.
        old_dir = os.getcwd()
        try:
            if param_dirname is not None:
                os.chdir(param_dirname)

            _potential.Potential.__init__(self,
                                          init_args,
                                          pot1=pot1,
                                          pot2=pot2,
                                          param_str=param_str,
                                          bulk_scale=bulk_scale,
                                          mpi_obj=mpi_obj,
                                          fpointer=fpointer,
                                          finalise=finalise,
                                          error=error)
        finally:
            os.chdir(old_dir)

        if init_args is not None and init_args.lower().startswith(
                'callbackpot'):
            _potential.Potential.set_callback(self, Potential.callback)

            if callback is not None:
                self.set_callback(callback)

            if calculator is not None:
                self.set_callback(calculator_callback_factory(calculator))

        if atoms is not None:
            atoms.set_calculator(self)

        self.name = init_args

    __init__.__doc__ = _potential.Potential.__init__.__doc__

    def calc(self,
             at,
             energy=None,
             force=None,
             virial=None,
             local_energy=None,
             local_virial=None,
             args_str=None,
             error=None,
             **kwargs):

        if not isinstance(args_str, basestring):
            args_str = dict_to_args_str(args_str)

        kw_args_str = dict_to_args_str(kwargs)

        args_str = ' '.join((self.get_calc_args_str(), kw_args_str, args_str))

        if isinstance(energy, basestring):
            args_str = args_str + ' energy=%s' % energy
            energy = None
        if isinstance(energy, bool) and energy:
            args_str = args_str + ' energy'
            energy = None

        if isinstance(force, basestring):
            args_str = args_str + ' force=%s' % force
            force = None
        if isinstance(force, bool) and force:
            args_str = args_str + ' force'
            force = None

        if isinstance(virial, basestring):
            args_str = args_str + ' virial=%s' % virial
            virial = None
        if isinstance(virial, bool) and virial:
            args_str = args_str + ' virial'
            virial = None

        if isinstance(local_energy, basestring):
            args_str = args_str + ' local_energy=%s' % local_energy
            local_energy = None
        if isinstance(local_energy, bool) and local_energy:
            args_str = args_str + ' local_energy'
            local_energy = None

        if isinstance(local_virial, basestring):
            args_str = args_str + ' local_virial=%s' % local_virial
            local_virial = None
        if isinstance(local_virial, bool) and local_virial:
            args_str = args_str + ' local_virial'
            local_virial = None

        potlog.debug(
            'Potential invoking calc() on n=%d atoms with args_str "%s"' %
            (len(at), args_str))
        _potential.Potential.calc(self, at, energy, force, virial,
                                  local_energy, local_virial, args_str, error)

    calc.__doc__ = update_doc_string(
        _potential.Potential.calc.__doc__,
        """In Python, this method is overloaded to set the final args_str to
          :meth:`get_calc_args_str`, followed by any keyword arguments,
          followed by an explicit `args_str` argument if present. This ordering
          ensures arguments explicitly passed to :meth:`calc` will override any
          default arguments.""")

    @staticmethod
    def callback(at_ptr):
        from quippy import Atoms
        at = Atoms(fpointer=at_ptr, finalise=False)
        if 'label' not in at.params or at.params[
                'label'] not in Potential.callback_map:
            raise ValueError('Unknown Callback label %s' % at.params['label'])
        Potential.callback_map[at.params['label']](at)

    def set_callback(self, callback):
        """
        For a :class:`Potential` of type `CallbackPot`, this method is
        used to set the callback function. `callback` should be a Python
        function (or other callable, such as a bound method or class
        instance) which takes a single argument, of type
        :class:`~quippy.atoms.Atoms`. Information about which properties should be
        computed can be obtained from the `calc_energy`, `calc_local_e`,
        `calc_force`, and `calc_virial` keys in `at.params`. Results
        should be returned either as `at.params` entries (for energy and
        virial) or by adding new atomic properties (for forces and local
        energy).

        Here's an example implementation of a simple callback::

          def example_callback(at):
              if at.calc_energy:
                 at.params['energy'] = ...

              if at.calc_force:
                 at.add_property('force', 0.0, n_cols=3)
                 at.force[:,:] = ...

          p = Potential('CallbackPot')
          p.set_callback(example_callback)
          p.calc(at, energy=True)
          print at.energy
          ...
        """
        Potential.callback_map[str(id(self))] = callback

    def check_state(self, atoms, tol=1e-15):
        if self.calculation_always_required:
            return all_changes
        return Calculator.check_state(self, atoms, tol)

    def calculate(self, atoms, properties, system_changes):
        Calculator.calculate(self, atoms, properties, system_changes)

        # we will do the calculation in place, to minimise number of copies,
        # unless atoms is not a quippy Atoms
        if isinstance(atoms, Atoms):
            self.quippy_atoms = weakref.proxy(atoms)
        else:
            potlog.debug(
                'Potential atoms is not quippy.Atoms instance, copy forced!')
            self.quippy_atoms = Atoms(atoms)
        initial_arrays = self.quippy_atoms.arrays.keys()
        initial_info = self.quippy_atoms.info.keys()

        if properties is None:
            properties = ['energy', 'forces', 'stress']

        # Add any default properties
        properties = set(self.get_default_properties() + properties)

        if len(properties) == 0:
            raise RuntimeError('Nothing to calculate')

        if not self.calculation_required(atoms, properties):
            return

        args_map = {
            'energy': {
                'energy': None
            },
            'energies': {
                'local_energy': None
            },
            'forces': {
                'force': None
            },
            'stress': {
                'virial': None
            },
            'numeric_forces': {
                'force': 'numeric_force',
                'force_using_fd': True,
                'force_fd_delta': 1.0e-5
            },
            'stresses': {
                'local_virial': None
            },
            'elastic_constants': {},
            'unrelaxed_elastic_constants': {}
        }

        # list of properties that require a call to Potential.calc()
        calc_properties = [
            'energy', 'energies', 'forces', 'numeric_forces', 'stress',
            'stresses'
        ]

        # list of other properties we know how to calculate
        other_properties = ['elastic_constants', 'unrelaxed_elastic_constants']

        calc_args = {}
        calc_required = False
        for property in properties:
            if property in calc_properties:
                calc_required = True
                calc_args.update(args_map[property])
            elif property not in other_properties:
                raise RuntimeError(
                    "Don't know how to calculate property '%s'" % property)

        if calc_required:
            self.calc(self.quippy_atoms, args_str=dict_to_args_str(calc_args))

        if 'energy' in properties:
            self.results['energy'] = float(self.quippy_atoms.energy)
        if 'energies' in properties:
            self.results['energies'] = self.quippy_atoms.local_energy.copy(
            ).view(np.ndarray)
        if 'forces' in properties:
            self.results['forces'] = self.quippy_atoms.force.copy().view(
                np.ndarray).T
        if 'numeric_forces' in properties:
            self.results[
                'numeric_forces'] = self.quippy_atoms.numeric_force.copy(
                ).view(np.ndarray).T
        if 'stress' in properties:
            stress = -self.quippy_atoms.virial.copy().view(
                np.ndarray) / self.quippy_atoms.get_volume()
            # convert to 6-element array in Voigt order
            self.results['stress'] = np.array([
                stress[0, 0], stress[1, 1], stress[2, 2], stress[1, 2],
                stress[0, 2], stress[0, 1]
            ])
        if 'stresses' in properties:
            lv = np.array(self.quippy_atoms.local_virial)  # make a copy
            vol_per_atom = self.get(
                'vol_per_atom',
                self.quippy_atoms.get_volume() / len(atoms))
            if isinstance(vol_per_atom, basestring):
                vol_per_atom = self.quippy_atoms.arrays[vol_per_atom]
            self.results['stresses'] = -lv.T.reshape(
                (len(atoms), 3, 3), order='F') / vol_per_atom

        if 'elastic_constants' in properties:
            cij_dx = self.get('cij_dx', 1e-2)
            cij = fzeros((6, 6))
            self.calc_elastic_constants(self.quippy_atoms,
                                        fd=cij_dx,
                                        args_str=self.get_calc_args_str(),
                                        c=cij,
                                        relax_initial=False,
                                        return_relaxed=False)
            if not get_fortran_indexing():
                cij = cij.view(np.ndarray)
            self.results['elastic_constants'] = cij

        if 'unrelaxed_elastic_constants' in properties:
            cij_dx = self.get('cij_dx', 1e-2)
            c0ij = fzeros((6, 6))
            self.calc_elastic_constants(self.quippy_atoms,
                                        fd=cij_dx,
                                        args_str=self.get_calc_args_str(),
                                        c0=c0ij,
                                        relax_initial=False,
                                        return_relaxed=False)
            if not get_fortran_indexing():
                c0ij = c0ij.view(np.ndarray)
            self.results['unrelaxed_elastic_constants'] = c0ij

        # copy back any additional output data to results dictionary
        skip_keys = ['energy', 'force', 'virial', 'numeric_force']
        for key in self.quippy_atoms.arrays.keys():
            if key not in initial_arrays and key not in skip_keys:
                self.results[key] = self.quippy_atoms.arrays[key].copy()
        for key in self.quippy_atoms.info.keys():
            if key not in initial_info and key not in skip_keys:
                if isinstance(self.quippy_atoms.info[key], np.ndarray):
                    self.results[key] = self.quippy_atoms.info[key].copy()
                else:
                    self.results[key] = self.quippy_atoms.info[key]

    def get_potential_energies(self, atoms):
        """
        Return array of atomic energies calculated with this Potential
        """
        return self.get_property('energies', atoms)

    def get_numeric_forces(self, atoms):
        """
        Return forces on `atoms` computed with finite differences of the energy
        """
        return self.get_property('numeric_forces', atoms)

    def get_stresses(self, atoms):
        """
        Return the per-atoms virial stress tensors for `atoms` computed with this Potential
        """
        return self.get_property('stresses', atoms)

    def get_elastic_constants(self, atoms):
        """
        Calculate elastic constants of `atoms` using this Potential.

        Returns  6x6 matrix :math:`C_{ij}` of elastic constants.

        The elastic contants are calculated as finite difference
        derivatives of the virial stress tensor using positive and
        negative strains of magnitude the `cij_dx` entry in
        ``calc_args``.
        """
        return self.get_property('elastic_constants', atoms)

    def get_unrelaxed_elastic_constants(self, atoms):
        """
        Calculate unrelaxed elastic constants of `atoms` using this Potential

        Returns 6x6 matrix :math:`C^0_{ij}` of unrelaxed elastic constants.

        The elastic contants are calculated as finite difference
        derivatives of the virial stress tensor using positive and
        negative strains of magnitude the `cij_dx` entry in
        :attr:`calc_args`.
        """
        return self.get_property('unrelaxed_elastic_constants', atoms)

    def get_default_properties(self):
        "Get the list of properties to be calculated by default"
        return self._default_properties[:]

    def set_default_properties(self, properties):
        "Set the list of properties to be calculated by default"
        self._default_properties = properties[:]

    def get(self, param, default=None):
        """
        Get the value of a ``calc_args`` parameter for this :class:`Potential`

        Returns ``None`` if `param` is not in the current ``calc_args`` dictionary.

        All calc_args are passed to :meth:`calc` whenever energies,
        forces or stresses need to be re-computed.
        """
        return self._calc_args.get(param, default)

    def set(self, **kwargs):
        """
        Set one or more calc_args parameters for this Potential

        All calc_args are passed to :meth:`calc` whenever energies,
        forces or stresses need to be computed.

        After updating the calc_args, :meth:`set` calls :meth:`reset`
        to mark all properties as needing to be recaculated.
        """
        self._calc_args.update(kwargs)
        self.reset()

    def get_calc_args(self):
        """
        Get the current ``calc_args``
        """
        return self._calc_args.copy()

    def set_calc_args(self, calc_args):
        """
        Set the ``calc_args`` to be used subsequent :meth:`calc` calls
        """
        self._calc_args = calc_args.copy()

    def get_calc_args_str(self):
        """
        Get the ``calc_args`` to be passed to :meth:`calc` as a string
        """
        return dict_to_args_str(self._calc_args)
Esempio n. 6
0
class Potential(_potential.Potential):
    __doc__ = update_doc_string(
        _potential.Potential.__doc__,
        r"""
The :class:`Potential` class also implements the ASE
:class:`ase.calculators.interface.Calculator` interface via the
the :meth:`get_forces`, :meth:`get_stress`, :meth:`get_stresses`,
:meth:`get_potential_energy`, :meth:`get_potential_energies`
methods. This simplifies calculation since there is no need
to set the cutoff or to call :meth:`~quippy.atoms.Atoms.calc_connect`,
as this is done internally. The example above reduces to::

    atoms = diamond(5.44, 14)
    atoms.rattle(0.01)
    atoms.set_calculator(pot)
    forces = atoms.get_forces()
    print forces

Note that the ASE force array is the transpose of the QUIP force
array, so has shape (len(atoms), 3) rather than (3, len(atoms)).

The optional arguments `pot1`, `pot2` and `bulk_scale` are
used by ``Sum`` and ``ForceMixing`` potentials (see also
wrapper class :class:`ForceMixingPotential`)

An :class:`quippy.mpi_context.MPI_context` object can be
passed as the `mpi_obj` argument to restrict the
parallelisation of this potential to a subset of the

The `callback` argument is used to implement the calculation of
the :class:`Potential` in a Python function: see :meth:`set_callback` for
an example.

In addition to the builtin QUIP potentials, it is possible to
use any ASE calculator as a QUIP potential by passing it as
the `calculator` argument to the :class:`Potential` constructor, e.g.::

   from ase.calculators.morse import MorsePotential
   pot = Potential(calculator=MorsePotential)

`cutoff_skin` is used to set the :attr:`cutoff_skin` attribute.

`atoms` if given, is used to set the calculator associated
with `atoms` to the new :class:`Potential` instance, by calling
:meth:'.Atoms.set_calculator`.

.. note::

    QUIP potentials do not compute stress and per-atom stresses
    directly, but rather the virial tensor which has units of stress
    :math:`\times` volume, i.e. energy. If the total stress is
    requested, it is computed by dividing the virial by the atomic
    volume, obtained by calling :meth:`.Atoms.get_volume`. If per-atom
    stresses are requested, a per-atom volume is needed. By default
    this is taken to be the total volume divided by the number of
    atoms. In some cases, e.g. for systems containing large amounts of
    vacuum, this is not reasonable. The ``vol_per_atom`` calc_arg can
    be used either to give a single per-atom volume, or the name of an
    array in :attr:`.Atoms.arrays` containing volumes for each atom.

""",
        signature=
        'Potential(init_args[, pot1, pot2, param_str, param_filename, bulk_scale, mpi_obj, callback, calculator, cutoff_skin, atoms])'
    )

    callback_map = {}

    def __init__(self,
                 init_args=None,
                 pot1=None,
                 pot2=None,
                 param_str=None,
                 param_filename=None,
                 bulk_scale=None,
                 mpi_obj=None,
                 callback=None,
                 calculator=None,
                 cutoff_skin=1.0,
                 atoms=None,
                 fpointer=None,
                 finalise=True,
                 error=None,
                 **kwargs):

        self.atoms = None
        self._prev_atoms = None
        self.energy = None
        self.energies = None
        self.forces = None
        self.stress = None
        self.stresses = None
        self.elastic_constants = None
        self.unrelaxed_elastic_constants = None
        self.numeric_forces = None
        self._calc_args = {}
        self._default_quantities = []
        self.cutoff_skin = cutoff_skin

        if callback is not None or calculator is not None:
            if init_args is None:
                init_args = 'callbackpot'

        if param_filename is not None:
            param_str = open(param_filename).read()

        if init_args is None and param_str is None:
            raise ValueError('Need one of init_args,param_str,param_filename')

        if init_args is not None:
            if init_args.lower().startswith('callbackpot'):
                if not 'label' in init_args:
                    init_args = init_args + ' label=%d' % id(self)
            else:
                # if param_str missing, try to find default set of QUIP params
                if param_str is None and pot1 is None and pot2 is None:
                    param_str = quip_xml_parameters(init_args)

        if kwargs != {}:
            if init_args is not None:
                init_args = init_args + ' ' + dict_to_args_str(kwargs)
            else:
                init_args = dict_to_args_str(kwargs)

        _potential.Potential.__init__(self,
                                      init_args,
                                      pot1=pot1,
                                      pot2=pot2,
                                      param_str=param_str,
                                      bulk_scale=bulk_scale,
                                      mpi_obj=mpi_obj,
                                      fpointer=fpointer,
                                      finalise=finalise,
                                      error=error)

        if init_args is not None and init_args.lower().startswith(
                'callbackpot'):
            _potential.Potential.set_callback(self, Potential.callback)

            if callback is not None:
                self.set_callback(callback)

            if calculator is not None:
                self.set_callback(calculator_callback_factory(calculator))

        if atoms is not None:
            atoms.set_calculator(self)

    __init__.__doc__ = _potential.Potential.__init__.__doc__

    def calc(self,
             at,
             energy=None,
             force=None,
             virial=None,
             local_energy=None,
             local_virial=None,
             args_str=None,
             error=None,
             **kwargs):

        if not isinstance(args_str, basestring):
            args_str = dict_to_args_str(args_str)

        kw_args_str = dict_to_args_str(kwargs)

        args_str = ' '.join((self.get_calc_args_str(), kw_args_str, args_str))

        if isinstance(energy, basestring):
            args_str = args_str + ' energy=%s' % energy
            energy = None
        if isinstance(energy, bool) and energy:
            args_str = args_str + ' energy'
            energy = None

        if isinstance(force, basestring):
            args_str = args_str + ' force=%s' % force
            force = None
        if isinstance(force, bool) and force:
            args_str = args_str + ' force'
            force = None

        if isinstance(virial, basestring):
            args_str = args_str + ' virial=%s' % virial
            virial = None
        if isinstance(virial, bool) and virial:
            args_str = args_str + ' virial'
            virial = None

        if isinstance(local_energy, basestring):
            args_str = args_str + ' local_energy=%s' % local_energy
            local_energy = None
        if isinstance(local_energy, bool) and local_energy:
            args_str = args_str + ' local_energy'
            local_energy = None

        if isinstance(local_virial, basestring):
            args_str = args_str + ' local_virial=%s' % local_virial
            local_virial = None
        if isinstance(local_virial, bool) and local_virial:
            args_str = args_str + ' local_virial'
            local_virial = None

        potlog.debug(
            'Potential invoking calc() on n=%d atoms with args_str "%s"' %
            (len(at), args_str))
        _potential.Potential.calc(self, at, energy, force, virial,
                                  local_energy, local_virial, args_str, error)

    calc.__doc__ = update_doc_string(
        _potential.Potential.calc.__doc__,
        """In Python, this method is overloaded to set the final args_str to
          :meth:`get_calc_args_str`, followed by any keyword arguments,
          followed by an explicit `args_str` argument if present. This ordering
          ensures arguments explicitly passed to :meth:`calc` will override any
          default arguments.""")

    @staticmethod
    def callback(at_ptr):
        from quippy import Atoms
        at = Atoms(fpointer=at_ptr, finalise=False)
        if 'label' not in at.params or at.params[
                'label'] not in Potential.callback_map:
            raise ValueError('Unknown Callback label %s' % at.params['label'])
        Potential.callback_map[at.params['label']](at)

    def set_callback(self, callback):
        """
        For a :class:`Potential` of type `CallbackPot`, this method is
        used to set the callback function. `callback` should be a Python
        function (or other callable, such as a bound method or class
        instance) which takes a single argument, of type
        :class:`~quippy.atoms.Atoms`. Information about which quantities should be
        computed can be obtained from the `calc_energy`, `calc_local_e`,
        `calc_force`, and `calc_virial` keys in `at.params`. Results
        should be returned either as `at.params` entries (for energy and
        virial) or by adding new atomic properties (for forces and local
        energy).

        Here's an example implementation of a simple callback::

          def example_callback(at):
              if at.calc_energy:
                 at.params['energy'] = ...

              if at.calc_force:
                 at.add_property('force', 0.0, n_cols=3)
                 at.force[:,:] = ...

          p = Potential('CallbackPot')
          p.set_callback(example_callback)
          p.calc(at, energy=True)
          print at.energy
          ...
        """
        Potential.callback_map[str(id(self))] = callback

    def wipe(self):
        """
        Mark all quantities as needing to be recalculated
        """
        self.energy = None
        self.energies = None
        self.forces = None
        self.stress = None
        self.stresses = None
        self.numeric_forces = None
        self.elastic_constants = None
        self.unrelaxed_elastic_constants = None

    def update(self, atoms):
        """
        Set the :class:`~quippy.atoms.Atoms` object associated with this :class:`Potential` to `atoms`.

        Called internally by :meth:`get_potential_energy`,
        :meth:`get_forces`, etc.  Only a weak reference to `atoms` is
        kept, to prevent circular references.  If `atoms` is not a
        :class:`quippy.atoms.Atoms` instance, then a copy is made and a
        warning will be printed.
        """
        # we will do the calculation in place, to minimise number of copies,
        # unless atoms is not a quippy Atoms
        if isinstance(atoms, Atoms):
            self.atoms = weakref.proxy(atoms)
        else:
            potlog.debug(
                'Potential atoms is not quippy.Atoms instance, copy forced!')
            self.atoms = Atoms(atoms)

        # check if atoms has changed since last call
        if self._prev_atoms is not None and self._prev_atoms.equivalent(
                self.atoms):
            return

        # Mark all quantities as needing to be recalculated
        self.wipe()

        # do we need to reinitialise _prev_atoms?
        if self._prev_atoms is None or len(self._prev_atoms) != len(
                self.atoms) or not self.atoms.connect.initialised:
            self._prev_atoms = Atoms()
            self._prev_atoms.copy_without_connect(self.atoms)
            self._prev_atoms.add_property('orig_pos', self.atoms.pos)
        else:
            # _prev_atoms is OK, update it in place
            self._prev_atoms.z[...] = self.atoms.z
            self._prev_atoms.pos[...] = self.atoms.pos
            self._prev_atoms.lattice[...] = self.atoms.lattice

        # do a calc_connect(), setting cutoff_skin so full reconnect will only be done when necessary
        self.atoms.set_cutoff(self.cutoff(), cutoff_skin=self.cutoff_skin)
        potlog.debug(
            'Potential doing calc_connect() with cutoff %f cutoff_skin %r' %
            (self.atoms.cutoff, self.cutoff_skin))
        self.atoms.calc_connect()

    # Synonyms for `update` for compatibility with ASE calculator interface
    def initialize(self, atoms):
        self.update(atoms)

    def set_atoms(self, atoms):
        self.update(atoms)

    def calculation_required(self, atoms, quantities):
        self.update(atoms)
        for quantity in quantities:
            if getattr(self, quantity) is None:
                return True
        return False

    def calculate(self, atoms, quantities=None):
        """
        Perform a calculation of `quantities` for `atoms` using this Potential.

        Automatically determines if a new calculation is required or if previous
        results are still appliciable (i.e. if the atoms haven't moved since last call)
        Called internally by :meth:`get_potential_energy`, :meth:`get_forces`, etc.
        """
        if quantities is None:
            quantities = ['energy', 'forces', 'stress']

        # Add any default quantities
        quantities = set(self.get_default_quantities() + quantities)

        if len(quantities) == 0:
            raise RuntimeError('Nothing to calculate')

        if not self.calculation_required(atoms, quantities):
            return

        args_map = {
            'energy': {
                'energy': None
            },
            'energies': {
                'local_energy': None
            },
            'forces': {
                'force': None
            },
            'stress': {
                'virial': None
            },
            'numeric_forces': {
                'force': 'numeric_force',
                'force_using_fd': True,
                'force_fd_delta': 1.0e-5
            },
            'stresses': {
                'local_virial': None
            },
            'elastic_constants': {},
            'unrelaxed_elastic_constants': {}
        }

        # list of quantities that require a call to Potential.calc()
        calc_quantities = [
            'energy', 'energies', 'forces', 'numeric_forces', 'stress',
            'stresses'
        ]

        # list of other quantities we know how to calculate
        other_quantities = ['elastic_constants', 'unrelaxed_elastic_constants']

        calc_args = {}
        calc_required = False
        for quantity in quantities:
            if quantity in calc_quantities:
                calc_required = True
                calc_args.update(args_map[quantity])
            elif quantity not in other_quantities:
                raise RuntimeError(
                    "Don't know how to calculate quantity '%s'" % quantity)

        if calc_required:
            self.calc(self.atoms, args_str=dict_to_args_str(calc_args))

        if 'energy' in quantities:
            self.energy = float(self.atoms.energy)
        if 'energies' in quantities:
            self.energies = self.atoms.local_energy.view(np.ndarray)
        if 'forces' in quantities:
            self.forces = self.atoms.force.view(np.ndarray).T
        if 'numeric_forces' in quantities:
            self.numeric_forces = self.atoms.numeric_force.view(np.ndarray).T
        if 'stress' in quantities:
            stress = -self.atoms.virial.view(
                np.ndarray) / self.atoms.get_volume()
            # convert to 6-element array in Voigt order
            self.stress = np.array([
                stress[0, 0], stress[1, 1], stress[2, 2], stress[1, 2],
                stress[0, 2], stress[0, 1]
            ])
        if 'stresses' in quantities:
            lv = np.array(self.atoms.local_virial)  # make a copy
            vol_per_atom = self.get('vol_per_atom',
                                    self.atoms.get_volume() / len(atoms))
            if isinstance(vol_per_atom, basestring):
                vol_per_atom = self.atoms.arrays[vol_per_atom]
            self.stresses = -lv.T.reshape(
                (len(atoms), 3, 3), order='F') / vol_per_atom

        if 'elastic_constants' in quantities:
            cij_dx = self.get('cij_dx', 1e-2)
            cij = fzeros((6, 6))
            self.calc_elastic_constants(self.atoms,
                                        fd=cij_dx,
                                        args_str=self.get_calc_args_str(),
                                        c=cij,
                                        relax_initial=False,
                                        return_relaxed=False)
            if not get_fortran_indexing():
                cij = cij.view(np.ndarray)
            self.elastic_constants = cij

        if 'unrelaxed_elastic_constants' in quantities:
            cij_dx = self.get('cij_dx', 1e-2)
            c0ij = fzeros((6, 6))
            self.calc_elastic_constants(self.atoms,
                                        fd=cij_dx,
                                        args_str=self.get_calc_args_str(),
                                        c0=c0ij,
                                        relax_initial=False,
                                        return_relaxed=False)
            if not get_fortran_indexing():
                c0ij = c0ij.view(np.ndarray)
            self.unrelaxed_elastic_constants = c0ij

    def get_potential_energy(self, atoms):
        """
        Return potential energy of `atoms` calculated with this Potential
        """
        self.calculate(atoms, ['energy'])
        return self.energy

    def get_potential_energies(self, atoms):
        """
        Return array of atomic energies calculated with this Potential
        """
        self.calculate(atoms, ['energies'])
        return self.energies.copy()

    def get_forces(self, atoms):
        """
        Return forces on `atoms` calculated with this Potential
        """
        self.calculate(atoms, ['forces'])
        return self.forces.copy()

    def get_numeric_forces(self, atoms):
        """
        Return forces on `atoms` computed with finite differences of the energy
        """
        self.calculate(atoms, ['numeric_forces'])
        return self.numeric_forces.copy()

    def get_stress(self, atoms):
        """
        Return stress tensor for `atoms` computed with this Potential

        Result is a 6-element array in Voigt notation:
           [sigma_xx, sigma_yy, sigma_zz, sigma_yz, sigma_xz, sigma_xy]
        """
        self.calculate(atoms, ['stress'])
        return self.stress.copy()

    def get_stresses(self, atoms):
        """
        Return the per-atoms virial stress tensors for `atoms` computed with this Potential
        """
        self.calculate(atoms, ['stresses'])
        return self.stresses.copy()

    def get_elastic_constants(self, atoms):
        """
        Calculate elastic constants of `atoms` using this Potential.

        Returns  6x6 matrix :math:`C_{ij}` of elastic constants.

        The elastic contants are calculated as finite difference
        derivatives of the virial stress tensor using positive and
        negative strains of magnitude the `cij_dx` entry in
        ``calc_args``.
        """
        self.calculate(atoms, ['elastic_constants'])
        return self.elastic_constants.copy()

    def get_unrelaxed_elastic_constants(self, atoms):
        """
        Calculate unrelaxed elastic constants of `atoms` using this Potential

        Returns 6x6 matrix :math:`C^0_{ij}` of unrelaxed elastic constants.

        The elastic contants are calculated as finite difference
        derivatives of the virial stress tensor using positive and
        negative strains of magnitude the `cij_dx` entry in
        :attr:`calc_args`.
        """
        self.calculate(atoms, ['unrelaxed_elastic_constants'])
        return self.unrelaxed_elastic_constants.copy()

    def get_default_quantities(self):
        "Get the list of quantities to be calculated by default"
        return self._default_quantities[:]

    def set_default_quantities(self, quantities):
        "Set the list of quantities to be calculated by default"
        self._default_quantities = quantities[:]

    def get(self, param, default=None):
        """
        Get the value of a ``calc_args`` parameter for this :class:`Potential`

        Returns ``None`` if `param` is not in the current ``calc_args`` dictionary.

        All calc_args are passed to :meth:`calc` whenever energies,
        forces or stresses need to be re-computed.
        """
        return self._calc_args.get(param, default)

    def set(self, **kwargs):
        """
        Set one or more calc_args parameters for this Potential

        All calc_args are passed to :meth:`calc` whenever energies,
        forces or stresses need to be computed.

        After updating the calc_args, :meth:`set` calls :meth:`wipe`
        to mark all quantities as needing to be recaculated.
        """
        self._calc_args.update(kwargs)
        self.wipe()

    def get_calc_args(self):
        """
        Get the current ``calc_args``
        """
        return self._calc_args.copy()

    def set_calc_args(self, calc_args):
        """
        Set the ``calc_args`` to be used subsequent :meth:`calc` calls
        """
        self._calc_args = calc_args.copy()

    def get_calc_args_str(self):
        """
        Get the ``calc_args`` to be passed to :meth:`calc` as a string
        """
        return dict_to_args_str(self._calc_args)

    def get_cutoff_skin(self):
        return self._cutoff_skin

    def set_cutoff_skin(self, cutoff_skin):
        self._cutoff_skin = cutoff_skin
        self._prev_atoms = None  # force a recalculation

    cutoff_skin = property(get_cutoff_skin,
                           set_cutoff_skin,
                           doc="""
                           The `cutoff_skin` attribute is only relevant when the ASE-style
                           interface to the Potential is used, via the :meth:`get_forces`,
                           :meth:`get_potential_energy` etc. methods. In this case the
                           connectivity of the :class:`~quippy.atoms.Atoms` object for which
                           the calculation is requested is automatically kept up to date by
                           using a neighbour cutoff of :meth:`cutoff` + `cutoff_skin`, and
                           recalculating the neighbour lists whenever the maximum displacement
                           since the last :meth:`Atoms.calc_connect` exceeds `cutoff_skin`.
                           """)