Source code for asaplib.descriptors.atomic_descriptors

"""
Methods and functions to compute atomic desciptors
"""
import json

import numpy as np

from ..io import NpEncoder


[docs]class Atomic_Descriptors: def __init__(self, desc_spec_dict={}): """ Object handing the specification and the computation of atomic descriptors Parameters ---------- desc_spec_dict: dictionaries that specify which atomic descriptor to use e.g. desc_spec_dict = { "firstsoap": {"type": 'SOAP',"species": [1, 6, 7, 8], "cutoff": 2.0, "atom_gaussian_width": 0.2, "n": 4, "l": 4} } """ self.desc_spec_dict = desc_spec_dict # list of Atomic_Descriptor objections self.engines = {} self.acronym = "" self.bind()
[docs] def add(self, desc_spec, tag): """ adding the specifications of a new atomic descriptors Parameters ---------- desc_spec: a dictionary that specify which atomic descriptor to use """ self.desc_spec_dict[tag] = desc_spec
[docs] def pack(self): return json.dumps(self.desc_spec_dict, sort_keys=True, cls=NpEncoder)
[docs] def get_acronym(self): if self.acronym == "": for element in self.desc_spec_dict.keys(): self.acronym += self.engines[element].get_acronym() return self.acronym
[docs] def bind(self): """ binds the objects that actually compute the descriptors these objects need to have .create(frame) method to compute the descriptors of frame (a xyz object) """ # clear up the objects self.engines = {} for element in self.desc_spec_dict.keys(): self.engines[element] = self._call(self.desc_spec_dict[element]) self.desc_spec_dict[element]['acronym'] = self.engines[element].get_acronym()
def _call(self, desc_spec): """ call the specific descriptor objects """ if "type" not in desc_spec.keys(): raise ValueError("Did not specify the type of the descriptor.") if desc_spec["type"] == "SOAP": return Atomic_Descriptor_SOAP(desc_spec) if desc_spec["type"] == "ACSF": return Atomic_Descriptor_ACSF(desc_spec) if desc_spec["type"] == "LMBTR_K2": return Atomic_Descriptor_LMBTR_K2(desc_spec) if desc_spec["type"] == "LMBTR_K3": return Atomic_Descriptor_LMBTR_K3(desc_spec) if desc_spec["type"] == "FCHL19": return Atomic_Descriptor_FCHL19(desc_spec) else: raise NotImplementedError
[docs] def compute(self, frame): """ compute the global descriptor vector for a frame from atomic contributions Parameters ---------- frame: ASE atom object. Coordinates of a frame. Returns ------- atomic_desc_dict : a dictionary. each entry contains the essential info of the descriptor (acronym) and a np.array [N_desc*N_atoms]. Atomic descriptors for a frame. e.g. {'ad1':{'acronym':'soap-1', 'atomic_descriptors': `a np.array [N_desc*N_atoms]`}} """ atomic_desc_dict = {} for element in self.desc_spec_dict.keys(): atomic_desc_dict[element] = {} atomic_desc_dict[element]['acronym'], atomic_desc_dict[element]['atomic_descriptors'] = self.engines[ element].create(frame) return atomic_desc_dict
[docs]class Atomic_Descriptor_Base: def __init__(self, desc_spec): self._is_atomic = True self.acronym = "" pass
[docs] def is_atomic(self): return self._is_atomic
[docs] def get_acronym(self): # we use an acronym for each descriptor, so it's easy to find it and refer to it return self.acronym
[docs] def create(self, frame): # notice that we return the acronym here!!! return self.acronym, []
[docs]class Atomic_Descriptor_SOAP(Atomic_Descriptor_Base): def __init__(self, desc_spec): """ make a DScribe SOAP object """ from dscribe.descriptors import SOAP if "type" not in desc_spec.keys() or desc_spec["type"] != "SOAP": raise ValueError("Type is not SOAP or cannot find the type of the descriptor") # required try: self.species = desc_spec['species'] self.cutoff = desc_spec['cutoff'] self.g = desc_spec['atom_gaussian_width'] self.n = desc_spec['n'] self.l = desc_spec['l'] except: raise ValueError("Not enough information to intialize the `Atomic_Descriptor_SOAP` object") # we have defaults here if 'rbf' in desc_spec.keys(): self.rbf = desc_spec['rbf'] else: self.rbf = 'gto' if 'crossover' in desc_spec.keys(): self.crossover = bool(desc_spec['crossover']) else: self.crossover = False if 'periodic' in desc_spec.keys(): self.periodic = bool(desc_spec['periodic']) else: self.periodic = True self.soap = SOAP(species=self.species, rcut=self.cutoff, nmax=self.n, lmax=self.l, sigma=self.g, rbf=self.rbf, crossover=self.crossover, average='off', periodic=self.periodic) print("Using SOAP Descriptors ...") # make an acronym self.acronym = "SOAP-n" + str(self.n) + "-l" + str(self.l) + "-c" + str(self.cutoff) + "-g" + str(self.g)
[docs] def create(self, frame): # notice that we return the acronym here!!! return self.acronym, self.soap.create(frame, n_jobs=1)
[docs]class Atomic_Descriptor_ACSF(Atomic_Descriptor_Base): def __init__(self, desc_spec): """ make a DScribe ACSF object see: https://singroup.github.io/dscribe/tutorials/acsf.html # template for an ACSF descriptor # currenly Dscribe only supports ASCF for finite system! """ if "type" not in desc_spec.keys() or desc_spec["type"] != "ACSF": raise ValueError("Type is not ACSF or cannot find the type of the descriptor") if 'periodic' in desc_spec.keys(): self.periodic = bool(desc_spec['periodic']) if self.periodic == True: raise ValueError("Warning: currently DScribe only supports ACSF for finite systems") from dscribe.descriptors import ACSF self.acsf_dict = { 'g2_params': None, 'g3_params': None, 'g4_params': None, 'g5_params': None} # required try: self.species = desc_spec['species'] self.cutoff = desc_spec['cutoff'] except: raise ValueError("Not enough information to intialize the `Atomic_Descriptor_ACF` object") # fill in the values for k, v in desc_spec.items(): if k in self.acsf_dict.keys(): if isinstance(v, list): self.acsf_dict[k] = np.asarray(v) else: self.acsf_dict[k] = v self.acsf = ACSF(species=self.species, rcut=self.cutoff, **self.acsf_dict, sparse=False) print("Using ACSF Descriptors ...") # make an acronym self.acronym = "ACSF-c" + str(self.cutoff) if self.acsf_dict['g2_params'] is not None: self.acronym += "-g2-" + str(len(self.acsf_dict['g2_params'])) if self.acsf_dict['g3_params'] is not None: self.acronym += "-g3-" + str(len(self.acsf_dict['g3_params'])) if self.acsf_dict['g4_params'] is not None: self.acronym += "-g4-" + str(len(self.acsf_dict['g4_params'])) if self.acsf_dict['g5_params'] is not None: self.acronym += "-g5-" + str(len(self.acsf_dict['g5_params']))
[docs] def create(self, frame): # notice that we return the acronym here!!! return self.acronym, self.acsf.create(frame, n_jobs=1)
[docs]class Atomic_Descriptor_LMBTR(Atomic_Descriptor_Base): def __init__(self, desc_spec): """ make a DScribe LMBTR object (see https://singroup.github.io/dscribe/tutorials/lmbtr.html)') Args: species: periodic (bool): Determines whether the system is considered to be periodic. k2 (dict): Dictionary containing the setup for the k=2 term. Contains setup for the used geometry function, discretization and weighting function. For example:: k2 = { "geometry": {"function": "inverse_distance"}, "grid": {"min": 0.1, "max": 2, "sigma": 0.1, "n": 50}, "weighting": {"function": "exp", "scale": 0.75, "cutoff": 1e-2} } k3 (dict): Dictionary containing the setup for the k=3 term. Contains setup for the used geometry function, discretization and weighting function. For example:: k3 = { "geometry": {"function": "angle"}, "grid": {"min": 0, "max": 180, "sigma": 5, "n": 50}, "weighting" = {"function": "exp", "scale": 0.5, "cutoff": 1e-3} } normalize_gaussians (bool): Determines whether the gaussians are normalized to an area of 1. Defaults to True. If False, the normalization factor is dropped and the gaussians have the form. :math:`e^{-(x-\mu)^2/2\sigma^2}` normalization (str): Determines the method for normalizing the output. The available options are: * "none": No normalization. * "l2_each": Normalize the Euclidean length of each k-term individually to unity. flatten (bool): Whether the output should be flattened to a 1D array. If False, a dictionary of the different tensors is provided, containing the values under keys: "k1", "k2", and "k3": sparse (bool): Whether the output should be a sparse matrix or a dense numpy array. """ # required try: self.species = desc_spec['species'] except: raise ValueError("Not enough information to intialize the `Atomic_Descriptor_LMBTR` object") # we have defaults here if 'normalization' in desc_spec.keys(): self.normalization = desc_spec['normalization'] else: self.normalization = None # or "l2_each" if 'normalize_gaussians' in desc_spec.keys(): self.normalize_gaussians = desc_spec['normalize_gaussians'] else: self.normalize_gaussians = "True" # or False if 'periodic' in desc_spec.keys(): self.periodic = bool(desc_spec['periodic']) else: self.periodic = True
[docs] def create(self, frame): # notice that we return the acronym here!!! return self.acronym, self.lmbtr.create(frame, n_jobs=1)
[docs]class Atomic_Descriptor_LMBTR_K2(Atomic_Descriptor_LMBTR): def __init__(self, desc_spec): super().__init__(desc_spec) from dscribe.descriptors import LMBTR if "type" not in desc_spec.keys() or desc_spec["type"] != "LMBTR_K2": raise ValueError("Type is not LMBTR_K2 or cannot find the type of the descriptor") # required try: self.k2 = desc_spec['k2'] except: raise ValueError("Not enough information to intialize the `Atomic_Descriptor_LMBTR` object") self.lmbtr = LMBTR(species=self.species, periodic=self.periodic, flatten=True, normalize_gaussians=self.normalize_gaussians, k2=self.k2) print("Using LMBTR-K2 Descriptors ...") # make an acronym self.acronym = "LMBTR-K2" # perhaps add more info here
[docs]class Atomic_Descriptor_LMBTR_K3(Atomic_Descriptor_LMBTR): def __init__(self, desc_spec): super().__init__(desc_spec) from dscribe.descriptors import LMBTR if "type" not in desc_spec.keys() or desc_spec["type"] != "LMBTR_K3": raise ValueError("Type is not LMBTR_K3 or cannot find the type of the descriptor") # required try: self.k2 = desc_spec['k3'] except: raise ValueError("Not enough information to intialize the `Atomic_Descriptor_LMBTR` object") self.lmbtr = LMBTR(species=self.species, periodic=self.periodic, flatten=True, normalize_gaussians=self.normalize_gaussians, k3=self.k3) print("Using LMBTR-K3 Descriptors ...") # make an acronym self.acronym = "LMBTR-K3" # perhaps add more info here
[docs]class Atomic_Descriptor_FCHL19(Atomic_Descriptor_Base): """ Generate the FCHL19 representation (https://doi.org/10.1063/1.5126701). Requires the developer version of the QML package, see https://www.qmlcode.org/installation.html for installation instructions. Parameters ---------- :param nRs2: Number of gaussian basis functions in the two-body terms :type nRs2: integer :param nRs3: Number of gaussian basis functions in the three-body radial part :type nRs3: integer :param nFourier: Order of Fourier expansion :type nFourier: integer :param eta2: Precision in the gaussian basis functions in the two-body terms :type eta2: float :param eta3: Precision in the gaussian basis functions in the three-body radial part :type eta3: float :param zeta: Precision parameter of basis functions in the three-body angular part :type zeta: float :param two_body_decay: exponential decay for the two body function :type two_body_decay: float :param three_body_decay: exponential decay for the three body function :type three_body_decay: float :param three_body_weight: relative weight of the three body function :type three_body_weight: float :is_periodic: Boolean determining Whether the system is periodic. :type Boolean: """ def __init__(self, desc_spec): if "type" not in desc_spec.keys() or desc_spec["type"] != "FCHL19": raise ValueError("Type is not FCHL19 or cannot find the type of the descriptor") if 'periodic' in desc_spec.keys(): self.periodic = bool(desc_spec['periodic']) if self.periodic == True: raise ValueError("Warning: currently DScribe only supports FCHL19 for finite systems") print("Warning: This FCHL19 atomic descriptor is untested, because I (Bingqing) cannot install QML!") raise NotImplementedError self.fchl_acsf_dict = {'nRs2': None, 'nRs3': None, 'nFourier': None, 'eta2': None, 'eta3': None, 'zeta': None, 'rcut': None, 'acut': None, 'two_body_decay': None, 'three_body_decay': None, 'three_body_weight': None, 'pad': False, 'gradients': False} # required try: self.species = desc_spec['species'] except: raise ValueError("Not enough information to intialize the `Atomic_Descriptor_ACF` object") # fill in the values for k, v in desc_spec.items(): if k in self.fchl_acsf_dict.keys(): self.fchl_acsf_dict[k] = v print("Using FCHL19 Descriptors ...") # make an acronym self.acronym = "FCHL19-c" # add more stuff here def _repr_wrapper(frame, elements, nRs2=24, nRs3=20, nFourier=1, eta2=0.32, eta3=2.7, zeta=np.pi, rcut=8.0, acut=8.0, two_body_decay=1.8, three_body_decay=0.57, three_body_weight=13.4, stride=1): nuclear_charges, coordinates = frame.get_atomic_numbers(), frame.get_positions() rep = generate_fchl_acsf(nuclear_charges, coordinates, elements, nRs2=nRs2, nRs3=nRs3, nFourier=nFourier, eta2=eta2, eta3=eta3, zeta=zeta, rcut=rcut, acut=acut, two_body_decay=two_body_decay, three_body_decay=three_body_decay, three_body_weight=three_body_weight, pad=False, gradients=False) rep_out = np.zeros((rep.shape[0], len(elements), rep.shape[1])) for i, z in enumerate(nuclear_charges): j = np.where(np.equal(z, elements))[0][0] rep_out[i, j] = rep[i] rep_out = rep_out.reshape(len(rep_out), -1) return rep_out
[docs] def create(self, frame): # notice that we return the acronym here!!! return self.acronym, self_repr_wrapper(frame, self.species, **self.fchl_acsf_dict)