Source code for asap3.Internal.OpenKIMcalculator

"""A calculator based on the OpenKIM project.

This module defines three object.

OpenKIMcalculator is a calculator class that interfaces to any OpenKIM V2 model.

OpenKIMparameters is a dictionary-like object that gives access to any
mutable parameters of the OpenKIM model, the object is created by
calling OpenKIMcalculator.get_parameters().

OpenKIMavailable is a function returning a list of available OpenKIM models.
"""

from ase.calculators.calculator import Calculator
from asap3 import _asap, AsapError
from asap3.Internal.ListOfElements import ListOfElements
from asap3.Internal.BuiltinPotentials import get_cell_heights, smallest_pbc_direction, AsapPotential
from ase.data import atomic_numbers, chemical_symbols
import numpy as np
from copy import copy
import subprocess
import shutil
import os
from collections.abc import MutableMapping
import numbers

[docs] class OpenKIMcalculator(_asap.OpenKIMcalculator, AsapPotential): """A calculator interfacing to the OpenKIM models. Parameters ---------- name : str The long name of the KIM Model. atoms : ase.Atoms, optional If given, ``set_atoms`` is called immediately. By default no atoms are set. stress : bool, optional Set to ``False`` to refrain from calculating the global virial, even if the model supports it. The default is ``True``. stresses : bool, optional As ``stress``, but for atomic virials / stresses. The default is ``True``. verbose : bool, optional Set to ``True`` to print additional information during neighbor list matching. The default is ``False``. """ def __init__(self, name, atoms=None, stress=True, stresses=True, verbose=False): _asap.OpenKIMcalculator.__init__(self, name, verbose) self.kimname = name self.atoms = None self.support_stress = stress self.support_stresses = stresses self.verbose = verbose if atoms is not None: self.set_atoms(atoms) def set_atoms(self, atoms, *args): """Set the atoms belonging to this calculator. Parameters ---------- atoms : ase.Atoms The atoms to attach to this calculator. *args : optional Internal access object passed on to the C++ implementation. Raises ------ RuntimeError If the OpenKIM model does not support one of the elements present in the atoms. Notes ----- The function ``set_atoms(atoms)`` is defined in C++ and just calls this Python function. This ensures that this Python function is called regardless of whether ``set_atoms`` is called from C++ or Python. """ # Inform the calculator about the translation from atomic numbers to particle types. self.z_to_typecode = {} elements = ListOfElements(atoms) for e in elements: symbol = chemical_symbols[e] try: code = self.get_type_code(chemical_symbols[e]) except AsapError: raise RuntimeError("The OpenKIM model '%s' does not support element Z=%i (%s)." % (self.kimname, e, chemical_symbols[e])) self.z_to_typecode[e] = code if self.verbose: print("Translation: Z = %i -> id = %i" % (e, code)) self.set_translation(self.z_to_typecode) # Check the compute arguments required by the Model. computearguments = self._get_compute_arguments() requiredargs = ['numberOfParticles', 'particleSpeciesCodes', 'particleContributing', 'coordinates', 'partialEnergy', 'partialForces', 'GetNeighborList'] for a in requiredargs: if computearguments[a] == 'notSupported': raise AsapError(f"The OpenKIM model '{self.kimname}' does not support '{a}'") del computearguments[a] # Processed self.supported_calculations = {} for k in ('partialEnergy', 'partialForces'): self.supported_calculations[k] = True # partialParticleEnergy is optional, but we will calculate it if possible arg = 'partialParticleEnergy' self.supported_calculations[arg] = computearguments[arg] != 'notSupported' del computearguments[arg] # partialVirial and partialParticleVirial are allocated if they are required by the model, or if # they are optional and stress is requested. arg = 'partialVirial' self.supported_calculations[arg] = ( computearguments[arg] == 'required' or (self.support_stress and computearguments[arg] == 'optional')) del computearguments[arg] arg = 'partialParticleVirial' self.supported_calculations[arg] = ( computearguments[arg] == 'required' or (self.support_stress and computearguments[arg] == 'optional')) del computearguments[arg] # Make sure that the model does not require something extra. for a, s in computearguments.items(): if s == 'required' or s == 'requiredByAPI': raise RuntimeError("OpenKIM model '{}' requires parameter '{}' (status: '{}')".format( self.name, a, s)) # Inform the calculator about which quantities should be allocated and calculated. for k,v in self.supported_calculations.items(): self.please_support(k, v) # Find out which neighborlist type we got. Check sanity and if ImageAtoms should be used. pbc = atoms.get_pbc() if self.verbose: print(f"Activating Image atoms (pbc = {str(pbc)})") self._use_imageatoms() _asap.OpenKIMcalculator.set_atoms(self, atoms, *args)
[docs] def get_supported_elements(self, user=True): """Get a list of supported elements, as chemical symbols. Parameters ---------- user : bool, optional If ``True`` (the default), elements with the names ``'user01'`` to ``'user20'`` may also be reported as supported. Returns ------- list of str The chemical symbols of the supported elements. """ supported = [] candidates = chemical_symbols[1:] # chemical_symbols[0] is 'X' and should be skipped. if user: candidates += [f'user{x:02d}' for x in range(1,21)] for element in candidates: try: self.get_type_code(element) except AsapError: pass else: supported.append(element) return supported
def get_name(self): return "asap3.OpenKIMcalculator(" + self.kimname + ")" def _get_parameters(self): return OpenKIMparameters(self) name = property(get_name, doc="Name of the potential") parameters = property(_get_parameters, doc="Published parameters of the OpenKIM model as a dict-like object")
[docs] class OpenKIMparameters(MutableMapping): """Published parameters of the OpenKIM model. A dictionary-like object containing the parameters of a OpenKIM model. Model parameters can be read and set. If the parameter is a multidimensional array then a flattened version of the array is used when accessing the parameters through normal dictionary indexing (``params['paramname']``). This object also has two helper functions that can be used to get and set multidimensional arrays, ``.getparameter(name, shape=None)`` and ``.setparameter(name, value, shape=None)``. They both take an optional argument specifying how an array of values should be interpreted. The following values are supported for the ``shape`` parameter: *a tuple* Explicitly gives the shape of the array. ``'square'`` The array is a square array. ``'ut'`` The array is an upper-triangular square matrix. ``'lt'`` The array is a lower-triangular square matrix. ``None`` The value is a scalar or a 1D array. """ def __init__(self, model): self.model = model self.parameters = {} self.descriptions = {} for i, (name, typecode, size, description) in enumerate(model._get_parameter_names_types()): self.parameters[name] = (i, typecode, size) self.descriptions[name] = description def __len__(self): return len(self.parameters) def __getitem__(self, key): return self.model._get_parameter(*self.parameters[key]) def __setitem__(self, key, value): index = self.parameters[key][0] if isinstance(value, numbers.Number): # A scalar self.model._set_parameter(index, value) else: # An array - flatten it. self.model._set_parameter(index, np.ravel(value)) def __delitem__(self, key): raise RuntimeError("Cannot delete a parameter from an OpenKIM model.") def __iter__(self): yield from self.parameters
[docs] def get_parameter(self, key, shape=None): """Get a model parameter Parameters ---------- key : str Name of the parameter. shape : str or tuple, optional How to interpret the shape of the array - see class documentation. """ p = self[key] if shape is None: return p elif isinstance(shape, tuple): return p.reshape(shape) shape = shape.lower() if shape == "square": size = int(np.round(np.sqrt(len(p)))) if size*size == len(p): return p.reshape((size,size)) else: raise ValueError(f"Cannot return parameter as a square matrix, its length is {len(p)}") if shape == "upper triangular": shape = "ut" elif shape == "lower triangular": shape = "lt" if shape == "ut" or shape == "lt": size = int(np.floor(np.sqrt(2*len(p)))) if size * (size + 1) // 2 == len(p): result = np.zeros((size, size), dtype=p.dtype) if shape == "ut": idx = np.triu_indices(size) else: idx = np.tril_indices(size) result[idx] = p return result else: raise ValueError(f"Cannot return parameter as a triangular matrix ({shape}), its length is {len(p)}") else: raise ValueError("Unknown shape "+shape)
[docs] def set_parameter(self, key, value, shape=None): """Set a model parameter Parameters ---------- key : str Name of the parameter. value: int or float or numpy.ndarray The new value of the parameter. shape : str or tuple, optional How to interpret the shape of the array - see class documentation. """ if shape is None: self[key] = value return if len(value.shape) != 2 or value.shape[0] != value.shape[1]: raise ValueError("All supported shapes assume a square matrix") shape = shape.lower() if shape == "square": self[key] = value # Flattened in __setitem__ elif shape == "ut" or shape == "upper triangular": idx = np.triu_indices(len(value)) self[key] = value[idx] elif shape == "lt" or shape == "lower triangular": idx = np.tril_indices(len(value)) self[key] = value[idx]
def _get_openkim_model_string(verbose): exename = "kim-api-collections-info" args = ' portable_models' sistername = 'kim-api-collections-management' # First try if the program is on the path. try: modelstring = subprocess.check_output(exename+args, shell=True, stderr=subprocess.STDOUT).decode('utf-8') return modelstring except subprocess.CalledProcessError: pass # Then look for it using pkg-config exepath = None try: prefix = subprocess.check_output("pkg-config libkim-api --variable=libexecdir", shell=True).decode('utf-8').strip() except subprocess.CalledProcessError: pass else: exepath = os.path.join(prefix, 'kim-api', exename) if verbose: print("Found using pkg-config:", exepath) # Finally, try to look for where kim-api-collections-management and then check ../libexec if not exepath and hasattr(shutil, 'which'): management = shutil.which(sistername) if management: prefix = os.path.dirname(os.path.dirname(management)) exepath = os.path.join(prefix, 'libexec', 'kim-api', exename) if verbose: print("Found relative to", sistername, ":", exepath) if not exepath: raise RuntimeError("Cannot locate "+exename+" to generate list of KIM models.") if not os.access(exepath, os.R_OK): raise RuntimeError("Executable should be at "+exepath+" but is absent or not usable.") try: modelstring = subprocess.check_output(exepath+args, shell=True).decode('utf-8') return modelstring except subprocess.CalledProcessError: raise RuntimeError("Running "+exepath+" failed.") def OpenKIMavailable(verbose=0): """Get the list of installed OpenKIM models. The list is obtained by locating and running the command line tool ``kim-api-collections-info``. Parameters ---------- verbose : int, optional Print information about how the command line tool is located. The default is 0 (quiet). Returns ------- list of str The names of the installed models. """ modelstring = _get_openkim_model_string(verbose) models = [] for line in modelstring.split('\n'): if line: models.append(line.split()[1]) return models