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)
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`. """)