Example #1
0
    def _add_substrate(self):
        """
        Add a substrate to a cage by minimising the energy from
        self.energy_func

        :return: None
        """
        logger.info('Adding the substrate to the center of the cage defined by'
                    ' the COM')
        logger.info(f'Using {self.energy_func.__name__}')

        # For electrostatic addition need partial atomic charges
        if self.energy_func.__name__ in ['electrostatic', 'electrostatic_fast']:

            estimate = True if self.energy_func.__name__ == 'electrostatic_fast' else False
            self.cage.charges = self.cage.get_charges(estimate=estimate)
            self.substrate.charges = self.substrate.get_charges(estimate=estimate)

            if self.cage.charges is None or self.substrate.charges is None:
                logger.error('Could not get partial atomic charges')
                return None

        xyzs = add_substrate.add_substrate_com(self)
        self.set_atoms(xyzs)

        return None
Example #2
0
    def get_charges(self, estimate=False):
        """
        Get the partial atomic charges using either XTB or estimate with RDKit using the Gasteiger charge scheme

        :param estimate: (bool)
        :param guess: (bool)
        :return:
        """

        if estimate and self.mol_obj is None:
            raise CgbindCritical(
                'Cannot estimate charges without a rdkit molecule object')

        if estimate:
            try:
                rdPartialCharges.ComputeGasteigerCharges(self.mol_obj)
                charges = [
                    float(
                        self.mol_obj.GetAtomWithIdx(i).GetProp(
                            '_GasteigerCharge')) for i in range(self.n_atoms)
                ]
            except:
                logger.error('RDKit failed to generate charges')
                return None

        else:
            charges = calculations.get_charges(self)

        return charges
Example #3
0
def get_linker_atoms_and_cost(linker, template_linker, current_atoms, x_coords=None):
    """
    Get the xyzs of a linker that is fitted to a template_linker object and
    the associated cost function – i.e. the repulsion to the current cage
    structure

    :param linker: (Linker)
    :param template_linker: (Template.Linker)
    :param curr_coords: (list(list))
    :return: list(list)), float
    """

    if x_coords is None:
        x_coords = linker.get_xmotif_coordinates()

    # Ensure the shift amount dr is set
    if linker.dr is None:
        logger.error('Cannot build a cage dr was None')
        return linker.atoms, 9999999.9

    # Expand the template by an about dr
    shifted_coords = get_shifted_template_x_motif_coords(linker_template=template_linker,
                                                         dr=linker.dr)

    linker_coords, cost = get_fitted_linker_coords_and_cost(linker=linker,
                                                            template_x_coords=shifted_coords,
                                                            coords_to_fit=x_coords,
                                                            curr_coords=[atom.coord for atom in current_atoms])
    logger.info(f'Repulsive + fitting cost for adding the linker is {cost:.5f}')

    atoms = [Atom(linker.atoms[i].label, coord=linker_coords[i])
             for i in range(linker.n_atoms)]

    return atoms, cost
Example #4
0
def xyzfile_to_atoms(filename):
    """
    Convert a standard xyz file into a list of atoms

    :param filename: (str)
    :return: (list(cgbind.atoms.Atom))
    """
    logger.info(f'Converting {filename} to list of atoms')

    if not filename.endswith('.xyz'):
        logger.error('Could not read .xyz file')
        raise FileMalformatted

    atoms = []

    with open(filename, 'r') as xyz_file:
        xyz_lines = xyz_file.readlines()[2:]
        for line in xyz_lines:
            atom_label, x, y, z = line.split()
            atoms.append(Atom(atom_label, float(x), float(y), float(z)))

    if len(atoms) == 0:
        logger.error(f'Could not read xyz lines in {filename}')
        raise FileMalformatted

    return atoms
Example #5
0
    def get_m_m_dist(self):
        """
        For a cage calculate the average M-M distance

        :return: (float) Distance in Å
        """
        try:
            m_m_dists = []
            for m_id_i in range(len(self.m_ids)):
                for m_id_j in range(len(self.m_ids)):
                    if m_id_i > m_id_j:
                        dist = np.linalg.norm(
                            self.atoms[self.m_ids[m_id_i]].coord -
                            self.atoms[self.m_ids[m_id_j]].coord)
                        m_m_dists.append(dist)

            if len(m_m_dists) > 0:
                return np.average(np.array(m_m_dists))
            else:
                logger.error('Could not find any metal_label atoms')

        except TypeError or ValueError or AttributeError:
            logger.error('Could not calculate the M-M distance. Returning 0.0')

        return 0.0
Example #6
0
def singlepoint(molecule, method, keywords, n_cores=None):
    """
    Run a single point energy evaluation on a molecule

    :param molecule: (object)
    :param method: (autode.ElectronicStructureMethod)
    :param keywords: (list(str)) Keywords to use for the electronic structure
    calculation e.g. ['Opt', 'PBE', 'def2-SVP']
    :param n_cores: (int) Number of cores to use
    :return:
    """
    logger.info('Running single point calculation')

    n_cores = Config.n_cores if n_cores is None else int(n_cores)

    try:
        from autode.calculation import Calculation
        from autode.wrappers.XTB import xtb
        from autode.wrappers.ORCA import orca
        from autode.wrappers.keywords import SinglePointKeywords

    except ModuleNotFoundError:
        logger.error('autode not found. Calculations not available')
        raise RequiresAutodE

    if keywords is None:
        if method == orca:
            keywords = SinglePointKeywords(
                ['SP', 'M062X', 'def2-TZVP', 'RIJCOSX', 'def2/J', 'SlowConv'])

            logger.warning('No keywords were set for the single point but an '
                           'ORCA calculation was requested. '
                           f'Using {str(keywords)}')

        elif method == xtb:
            keywords = xtb.keywords.sp

        else:
            logger.critical('No keywords were set for the single-point '
                            'calculation')
            raise Exception

    else:
        # If the keywords are specified as a list convert them to a set of
        # OptKeywords, required for autodE
        if type(keywords) is list:
            keywords = SinglePointKeywords(keywords)

    sp = Calculation(name=molecule.name + '_sp',
                     molecule=molecule,
                     method=method,
                     keywords=keywords,
                     n_cores=n_cores)
    sp.run()
    molecule.energy = sp.get_energy()

    return None
Example #7
0
    def _is_linker_reasonable(self, linker):

        if linker is None:
            logger.error(f'Linker was None. Cannot build {self.name}')
            return False

        if linker.n_atoms == 0 or linker.arch is None or linker.name is None:
            logger.error(f'Linker doesn\'t have all the required attributes. '
                         f'Cannot build {self.name}')
            return False

        return True
Example #8
0
def molfile_to_atoms(filename):
    """
    Convert a .mol file to a list of atoms

    :param filename: (str)
    :return: (list(Atom))
    """
    """
    e.g. for methane:
    _____________________

     OpenBabel03272015013D

      5  4  0  0  0  0  0  0  0  0999 V2000
       -0.2783    0.0756    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        0.7917    0.0756    0.0000 H   0  0  0  0  0  0  0  0  0  0  0  0
       -0.6349   -0.9294   -0.0876 H   0  0  0  0  0  0  0  0  0  0  0  0
       -0.6349    0.6539   -0.8266 H   0  0  0  0  0  0  0  0  0  0  0  0
       -0.6349    0.5022    0.9141 H   0  0  0  0  0  0  0  0  0  0  0  0
      1  2  1  0  0  0  0
      1  3  1  0  0  0  0
      1  4  1  0  0  0  0
      1  5  1  0  0  0  0
    M  END
    _____________________
    """
    atoms = []

    if not filename.endswith('.mol'):
        logger.error('Could not read .mol file')
        raise FileMalformatted

    with open(filename, 'r') as mol_file:
        mol_lines = mol_file.readlines()[3:]
        try:
            n_atoms = int(mol_lines[0].split()[0])

        except ValueError:
            raise FileMalformatted

        for line in mol_lines[1:n_atoms + 1]:
            x, y, z, atom_label = line.split()[:4]
            atoms.append(Atom(atom_label, float(x), float(y), float(z)))

    if len(atoms) == 0:
        logger.error(f'Could not read xyz lines in {filename}')
        raise FileMalformatted

    return atoms
Example #9
0
    def print_xyz_file(self, filename=None):
        """
        Print a .xyz file from self.xyzs provided self.reasonable_geometry is
        True

        :param filename: (str) Override the default filename
        :return: None
        """
        if not self.reasonable_geometry:
            logger.error('Geometry is not reasonable')

        filename = filename if filename is not None else f'{self.name}.xyz'

        atoms_to_xyz_file(atoms=self.atoms, filename=filename)
        return None
Example #10
0
    def _init_smiles(self, smiles, use_etdg_confs=False):
        """
        Initialise a Molecule object from a SMILES sting using RDKit
        :param smiles: (str) SMILES string
        :param use_etdg_confs: (bool) override the default conformer generation and use the ETDG algorithm
        :return:
        """
        logger.info('Initialising a Molecule from a SMILES string')
        try:
            self.mol_obj = Chem.MolFromSmiles(smiles)
            self.mol_obj = Chem.AddHs(self.mol_obj)
            self.charge = Chem.GetFormalCharge(self.mol_obj)
            self.n_rot_bonds = rdMolDescriptors.CalcNumRotatableBonds(
                self.mol_obj)
            self.n_h_donors = rdMolDescriptors.CalcNumHBD(self.mol_obj)
            self.n_h_acceptors = rdMolDescriptors.CalcNumHBA(self.mol_obj)

        except:
            logger.error('RDKit failed to generate mol objects')
            return

        logger.info('Running conformation generation with RDKit... running')
        method = AllChem.ETKDGv2(
        ) if use_etdg_confs is False else AllChem.ETDG()
        method.pruneRmsThresh = 0.3
        method.numThreads = Config.n_cores
        conf_ids = list(
            AllChem.EmbedMultipleConfs(self.mol_obj,
                                       numConfs=self.n_confs,
                                       params=method))
        logger.info('                                          ... done')

        try:
            self.volume = AllChem.ComputeMolVolume(self.mol_obj)
        except ValueError:
            logger.error('RDKit failed to compute the molecular volume')
            return

        self.bonds = [(b.GetBeginAtomIdx(), b.GetEndAtomIdx())
                      for b in self.mol_obj.GetBonds()]
        self.conformers = extract_conformers_from_rdkit_mol_object(
            mol_obj=self.mol_obj, conf_ids=conf_ids)

        # Default to the first generated conformer in the absence of any other information
        self.set_atoms(atoms=self.conformers[0].atoms)

        return None
Example #11
0
    def _find_metals(self):
        logger.info(f'Getting metals with label {self.metal_label}')
        metals = []
        try:
            for i in range(len(self.atoms)):
                if self.atoms[i].label == self.metal_label:
                    metals.append(
                        Metal(label=self.metal_label,
                              atom_id=i,
                              coord=self.atoms[i].coord))

            return metals

        except (TypeError, IndexError, AttributeError):
            logger.error('Could not get metal_label atom ids. Returning None')

        return None
Example #12
0
    def _init_homoleptic_cage(self, linker):
        logger.info(f'Initialising a homoleptic cage')
        self.homoleptic = True

        if not self._is_linker_reasonable(linker):
            logger.error('Linker was not reasonable')
            return

        if self.name == 'cage':
            # Only override the default name
            self.name = 'cage_' + linker.name

        self.arch = linker.arch
        self.linkers = [linker for _ in range(linker.arch.n_linkers)]
        self.cage_template = linker.cage_template

        return None
Example #13
0
    def print_esp_cube_file(self):
        """
        Print an electrostatic potential (ESP) .cube file. Prints the lines
        from self.get_esp_cube()

        :return: None
        """

        cube_file_lines = self.get_esp_cube()

        if len(cube_file_lines) == 0:
            logger.error('Could not generate cube')
            return None

        with open(self.name + '_esp.cube', 'w') as cube_file:
            [print(line, end='', file=cube_file) for line in cube_file_lines]

        return None
Example #14
0
    def _build(self, max_cost):
        logger.info('Building a cage geometry')
        assert self.homoleptic or self.heteroleptic

        if self.homoleptic:
            build_homoleptic_cage(self, max_cost)

        if self.heteroleptic:
            build_heteroleptic_cage(self, max_cost)

        if self.reasonable_geometry:
            if self.n_atoms != self.arch.n_metals + sum(
                [linker.n_atoms for linker in self.linkers]):
                logger.error('Failed to build a cage')
                self.reasonable_geometry = False
                return None

        return None
Example #15
0
    def get_metal_atom_ids(self):
        """
        Get the atom ids of the self.metal atoms in the xyzs

        :return: (list(int))
        """

        logger.info(f'Getting metal_label atom ids with label {self.metal}')
        if self.n_atoms == 0:
            logger.error('Could not get metal atom ids. xyzs were None')
            return None

        try:
            return [
                i for i in range(self.n_atoms)
                if self.atoms[i].label == self.metal
            ]
        except TypeError or IndexError or AttributeError:
            logger.error('Could not get metal label atom ids. Returning None')
            return None
Example #16
0
    def gen_confs(self, n_confs=1):
        """Populate self.conf_xyzs by calling RDKit"""

        if self.smiles is None:
            logger.error('Could not generate conformers. Substrate was not '
                         'initialised from a SMILES string')
            return None

        if self.mol_obj is None:
            logger.error('Could not generate conformers. Molecule did not '
                         'have an associated RDKit mol_obj')
            return None

        conf_ids = list(AllChem.EmbedMultipleConfs(self.mol_obj,
                                                   numConfs=n_confs,
                                                   params=AllChem.ETKDG()))

        self.conformers = extract_conformers_from_rdkit_mol_object(mol_obj=self.mol_obj,
                                                                   conf_ids=conf_ids)

        return None
Example #17
0
    def get_cavity_vol(self):
        """
        For a cage extract the cavity volume defined as the volume of the
        largest sphere, centered on the cage centroid that may be constructed
        while r < r(midpoint--closest atom)

        :return: (float) Cavity volume in Å^3
        """
        logger.info('Calculating maximum enclosed sphere')

        min_centriod_atom_dist = 999.9
        centroid, min_atom_dist_id = None, None

        try:
            centroid = self.get_centroid()
            if centroid is None:
                logger.error('Could not find the cage centroid. Returning 0.0')
                return 0.0

            # Compute the smallest distance to the centroid
            for i in range(self.n_atoms):
                dist = np.linalg.norm(self.atoms[i].coord - centroid)
                if dist < min_centriod_atom_dist:
                    min_centriod_atom_dist = dist
                    min_atom_dist_id = i

        except TypeError or ValueError or AttributeError:
            pass

        if min_atom_dist_id is not None:
            vdv_radii = get_vdw_radii(atom=self.atoms[min_atom_dist_id])
            # V = 4/3 π r^3, where r is the centroid -> closest atom distance,
            # minus it's VdW volume
            return (4.0 / 3.0) * np.pi * (min_centriod_atom_dist -
                                          vdv_radii)**3

        else:
            logger.error(
                'Could not calculate the cavity volume. Returning 0.0')
            return 0.0
Example #18
0
    def _check_reasonable_cage_substrate(self, cage, substrate):
        """
        Determine if the cage and substrate are 'reasonable' i.e. both exist
        and they have the appropriate attributes

        :param cage: (Cage object)
        :param substrate: (Substrate object)
        :return: (bool)
        """

        if cage is None or substrate is None:
            logger.error(f'Cannot build a cage-substrate complex for '
                         f'{self.name} either cage or substrate was None')
            raise CannotBuildCSComplex

        attrs = [cage.charge, substrate.charge, cage.atoms, substrate.atoms,
                 cage.m_ids, cage.n_atoms]

        if not all([attr is not None for attr in attrs]) or (substrate.mol_obj is None and self.n_subst_confs > 1):
            logger.error(f'Cannot build a cage-substrate complex for '
                         f'{self.name} a required attribute was None')
            raise CannotBuildCSComplex
Example #19
0
    def _init_heteroleptic_cage(self, linkers):
        logger.info(f'Initialising a heteroleptic cage')
        self.heteroleptic = True

        if not all([self._is_linker_reasonable(linker) for linker in linkers]):
            logger.error('Not all linkers were reasonable')
            raise CannotBuildCage

        if not all(
            [linker.arch.name == linkers[0].arch.name for linker in linkers]):
            logger.error(
                'Linkers had different architectures, not building a cage')
            raise CannotBuildCage

        if self.name == 'cage':
            # Only override the default name
            self.name = 'cage_' + '_'.join([linker.name for linker in linkers])

        self.arch = linkers[0].arch
        self.linkers = linkers
        self.cage_template = linkers[0].cage_template

        return None
Example #20
0
def get_charges(molecule):
    """
    Get the partial atomic charges with XTB (tested with v. 6.2) will generate
    then trash a temporary directory

    :return:
    """
    logger.info('Getting charges')

    try:
        from autode.calculation import Calculation
        from autode.wrappers.XTB import xtb
        from autode.exceptions import MethodUnavailable
        from autode.wrappers.keywords import SinglePointKeywords

    except ModuleNotFoundError:
        logger.error('autode not found. Calculations not available')
        raise RequiresAutodE

    # Run the calculation
    try:
        xtb_sp = Calculation(name=molecule.name + '_xtb_sp',
                             molecule=molecule,
                             method=xtb,
                             n_cores=1,
                             keywords=xtb.keywords.sp)
        xtb_sp.run()

        charges = xtb_sp.get_atomic_charges()

    except MethodUnavailable:
        logger.error('Could not calculate without an XTB install')
        return None

    if len(charges) == molecule.n_atoms:
        return charges

    else:
        logger.error('XTB failed to generate charges')
        return None
Example #21
0
    def __init__(self,
                 linker=None,
                 metal='M',
                 metal_charge=0,
                 linkers=None,
                 solvent=None,
                 mult=1,
                 name='cage',
                 max_cost=5):
        """
        Metallocage object. Inherits from cgbind.molecule.BaseStruct

        :ivar self.metal: (str)
        :ivar self.linkers: (list(Linker object))
        :ivar self.dr: (float)
        :ivar self.arch: (Arch object)
        :ivar self.cage_template: (Template object)
        :ivar self.m_ids: (list(int))
        :ivar self.metal_charge: (int)

        :param name: (str) Name of the cage
        :param solvent: (str)
        :param linker: (Linker object) Linker to initialise a homoleptic
                        metallocage
        :param linkers: (list(Linker object)) List of Linkers to inialise a metallocage
        :param metal: (str) Atomic symbol of the metal
        :param metal_charge: (int) Formal charge on the metal atom/ion
        :param mult: (int) Total spin multiplicity of the cage
        :param max_cost: (float) Acceptable ligand-ligand repulsion to
                         accommodate in metallocage construction
        """
        super(Cage, self).__init__(name=name,
                                   charge=0,
                                   mult=mult,
                                   filename=None,
                                   solvent=solvent)

        logger.info(f'Initialising a Cage object')

        self.metal = str(metal)
        self.linkers = None
        self.dr = None
        self.arch = None
        self.cage_template = None
        self.m_ids = None
        self.metal_charge = int(metal_charge)

        self.reasonable_geometry = False
        self.homoleptic = False
        self.heteroleptic = False

        if linker is not None:
            self._init_homoleptic_cage(linker)

        elif linkers is not None:
            self._init_heteroleptic_cage(linkers)

        else:
            logger.error('Could not generate a cage object without either a '
                         'linker or set of linkers')
            raise CannotBuildCage

        if self.linkers is None:
            logger.error('Cannot build a cage with linkers as None')
            raise CannotBuildCage

        self._calc_charge()

        self.reasonable_geometry = False
        self._build(max_cost=max_cost)

        self.m_ids = self.get_metal_atom_ids()
        logger.info(f'Generated cage successfully. '
                    f'Geometry is reasonable: {self.reasonable_geometry}')
Example #22
0
def optimise(molecule,
             method,
             keywords,
             n_cores=None,
             cartesian_constraints=None):
    """
    Optimise a molecule

    :param molecule: (object)
    :param method: (autode.ElectronicStructureMethod)
    :param keywords: (list(str)) Keywords to use for the electronic structure c
                                 alculation e.g. ['Opt', 'PBE', 'def2-SVP']
    :param n_cores: (int) Number of cores to use
    :param cartesian_constraints: (list(int)) List of atom ids to constrain
    :return:
    """
    logger.info('Running an optimisation calculation')

    n_cores = Config.n_cores if n_cores is None else int(n_cores)

    try:
        from autode.calculation import Calculation
        from autode.wrappers.XTB import xtb
        from autode.wrappers.ORCA import orca
        from autode.wrappers.keywords import OptKeywords

    except ModuleNotFoundError:
        logger.error('autode not found. Calculations not available')
        raise RequiresAutodE

    if keywords is None:
        if method == orca:

            keywords = OptKeywords(['LooseOpt', 'PBE', 'D3BJ', 'def2-SVP'])
            logger.warning(f'No keywords were set for the optimisation but an'
                           f' ORCA calculation was requested. '
                           f'Using {str(keywords)}')

        elif method == xtb:
            keywords = xtb.keywords.opt

        else:
            logger.critical('No keywords were set for the optimisation '
                            'calculation')
            raise Exception

    else:
        # If the keywords are specified as a list convert them to a set of
        # OptKeywords, required for autodE
        if type(keywords) is list:
            keywords = OptKeywords(keywords)

    opt = Calculation(name=molecule.name + '_opt',
                      molecule=molecule,
                      method=method,
                      keywords=keywords,
                      n_cores=n_cores,
                      cartesian_constraints=cartesian_constraints)
    opt.run()
    molecule.energy = opt.get_energy()
    molecule.set_atoms(atoms=opt.get_final_atoms())

    return None
Example #23
0
def mol2file_to_atoms(filename):
    """
    Convert a .mol file into a standard set of atoms

    :param filename: (str)
    :return: (lis(Atom))
    """
    logger.info('Converting .mol2 file to atoms')

    if not filename.endswith('.mol2'):
        logger.error('Could not read .mol2 file')
        raise FileMalformatted

    mol_file_lines = open(filename, 'r').readlines()

    # Get the unformatted atoms from the .mol2 file. The atom labels will not
    # be standard
    atoms, xyz_block = [], False
    for n_line, line in enumerate(mol_file_lines):

        if '@' in line and xyz_block:
            break

        if xyz_block:
            try:
                atom_label, x, y, z = line.split()[1:5]
                try:
                    atoms.append(Atom(atom_label, float(x), float(y),
                                      float(z)))
                except TypeError:
                    logger.error('There was a problem with the .mol2 file')
                    raise FileMalformatted

            except IndexError:
                logger.error('There was a problem with the .mol2 file')
                raise FileMalformatted

        # e.g.   @<TRIPOS>ATOM
        #        1 Pd1     -2.1334  12.0093  11.5778   Pd        1 RES1   2.0000
        if '@' in line and 'ATOM' in line and len(
                mol_file_lines[n_line + 1].split()) == 9:
            xyz_block = True

    # Fix any atom labels
    for atom in atoms:

        if len(atom.label) == 1:
            continue

        # e.g. P1 or C58
        elif atom.label[0].isalpha() and not atom.label[1].isalpha():
            atom.label = atom.label[0]

        # e.g. Pd10
        elif atom.label[0].isalpha() and atom.label[1].isalpha():
            atom.label = atom.label[:2].title()

        else:
            logger.error('Unrecognised atom type')
            raise FileMalformatted

    return atoms
Example #24
0
def build_homoleptic_cage(cage, max_cost):
    """
    Construct the geometry (atoms) of a homoleptic cage

    :param cage: (Cage)
    :param max_cost: (float) Maximum cost to break out of the loop over
    :return:
    """

    # Get the list of Linkers ordered by the best fit to the template
    cage.linkers[0].set_ranked_linker_possibilities(metal=cage.metal)
    logger.info(f'Have {len(cage.linkers[0].possibilities)} linkers to fit')

    min_cost, best_linker = 99999999.9, None
    atoms, cage_cost = [], 99999999.9

    # For all the possible linker conformer / Xmotif set possibilities
    for linker in cage.linkers[0].possibilities:

        # Atoms for and cost in building this cage
        atoms, cage_cost = [], 0.0

        # Coordinates of the X motif atoms in this linker - used to rotate
        x_coords = linker.get_xmotif_coordinates()

        for i, template_linker in enumerate(cage.cage_template.linkers):
            linker_atoms, cost = get_linker_atoms_and_cost(linker,
                                                           template_linker,
                                                           atoms,
                                                           x_coords)
            cage_cost += cost
            atoms += linker_atoms

        if cage_cost < min_cost:
            min_cost = cage_cost
            best_linker = deepcopy(linker)

        if cage_cost < max_cost:
            logger.info(f'Total L-L repulsion + fit to template in building '
                        f'cage is {cage_cost:.2f}')
            break

    # If there is no break due to a small repulsion then build the best
    # possible cage
    if cage_cost > max_cost:
        if best_linker is None:
            logger.error('Could not achieve the required cost threshold for '
                         'building the cage')
            return None
        else:
            logger.warning('Failed to reach the threshold. Returning the cage '
                           'that minimises the L-L repulsion')
            atoms = []
            for i, template_linker in enumerate(cage.cage_template.linkers):
                linker_atoms, _ = get_linker_atoms_and_cost(best_linker,
                                                            template_linker,
                                                            atoms)
                atoms += linker_atoms

    # Set the delta r for the whole cage
    cage.dr = best_linker.dr

    # Add the metals from the template shifted by dr
    for metal in cage.cage_template.metals:

        if cage.dr is None:
            raise CannotBuildCage('Cage had no shift distance (∆r)')

        if metal.shift_vec is None:
            raise CannotBuildCage('Template shift vector not defined')

        metal_coord = cage.dr * metal.shift_vec / np.linalg.norm(metal.shift_vec) + metal.coord
        atoms.append(Atom(cage.metal, coord=metal_coord))

    cage.set_atoms(atoms)
    return None
Example #25
0
def get_esp_cube_lines(charges, atoms):
    """
    From a list of charges and a set of xyzs create the electrostatic potential
    map grid-ed uniformly between the most negative x, y, z values -5 Å
    and the largest x, y, z +5 Å

    :param charges: (list(float))
    :param atoms: (list(autode.atoms.Atom))
    :return: (list(str)), (min ESP value, max ESP value)
    """
    logger.info('Calculating the ESP and generating a .cube file')
    start_time = time()

    try:
        from esp_gen import get_cube_lines

    except ModuleNotFoundError:
        raise CgbindCritical('esp_gen not available. cgbind must be '
                             'installed with the --esp_gen flag')

    if charges is None:
        logger.error('Could not generate an .cube file, charges were None')
        return [], (None, None)

    coords = np.array([atom.coord for atom in atoms])
    charges = np.array(charges)

    # Get the max and min points from the coordinates
    max_cart_values = np.max(coords, axis=0)
    min_cat_values = np.min(coords, axis=0)

    # The grid needs to be slightly larger than the smallest/largest Cartesian
    # coordinate
    # NOTE: All distances from here are in Bohr (a0) i.e. atomic units
    min_carts = Constants.ang2a0 * (min_cat_values - 5 * np.ones(3))
    max_carts = Constants.ang2a0 * (max_cart_values + 5 * np.ones(3))

    coords = np.array([Constants.ang2a0 * np.array(coord) for coord in coords])

    # Number of voxels will be nx * ny * nz
    nx, ny, nz = 50, 50, 50

    vox_size = max_carts - min_carts
    rx, ry, rz = vox_size[0] / nx, vox_size[1] / ny, vox_size[2] / nz

    # Write the .cube file lines
    cube_file_lines = ['Generated by cgbind\n', 'ESP\n']

    n_atoms = len(coords)
    min_x, min_y, min_z = min_carts
    cube_file_lines.append(
        f'{n_atoms:>5d}{min_x:>12f}{min_y:>12f}{min_z:>12f}\n'
    )  # n_atoms origin(x y z)

    cube_file_lines.append(f'{nx:>5d}{rx:>12f}{0.0:>12f}{0.0:>12f}\n'
                           )  # Number of voxels and their size
    cube_file_lines.append(f'{ny:>5d}{0.0:>12f}{ry:>12f}{0.0:>12f}\n')
    cube_file_lines.append(f'{nz:>5d}{0.0:>12f}{0.0:>12f}{rz:>12f}\n')

    for atom in atoms:
        x, y, z = atom.coord
        cube_file_lines.append(
            f'{get_atomic_number(atom):>5d}{0.0:>12f}'
            f'{Constants.ang2a0*x:>12f}{Constants.ang2a0*y:>12f}{Constants.ang2a0*z:>12f}\n'
        )

    # Looping over x, y, z is slow in python so use Cython extension
    cube_val_lines, min_val, max_val = get_cube_lines(nx, ny, nz, coords,
                                                      min_carts, charges,
                                                      vox_size)
    cube_file_lines += cube_val_lines

    logger.info(f'ESP generated in {time()-start_time:.3f} s')
    return cube_file_lines, (min_val, max_val)
Example #26
0
    def _check_structure(self):
        if self.n_atoms == 0:
            logger.error('Could get atoms for linker')
            raise NoXYZs

        return None
Example #27
0
def add_substrate_com(cagesubt):
    """
    Add a substrate the centre of a cage defined by its centre of mass (com)
    will minimise the energy with respect to rotation of the substrate and the
    substrate conformer using cagesubt.energy_func. Will rotate cagesubt.n_init_geom
    times and use cagesubt.n_subst_confs number of substrate conformers

    :param cagesubt: (CageSubstrateComplex object)

    :return: xyzs: (list(list))
    """
    logger.info(f'Adding substrate to the cage COM and minimising the energy '
                f'with {cagesubt.energy_func.__name__}')

    # Minimum energy initialisation and the x parameter array (angles to
    # rotate about the x, y, z axes)
    min_energy, curr_x = 9999999999.9, np.zeros(3)

    # Optimum (minimum energy) conformer
    best_coords = None

    c, s = cagesubt.cage, cagesubt.substrate
    cage_coords = get_centered_cage_coords(c)
    c.vdw_radii = [get_vdw_radii(atom) for atom in c.atoms]

    if cagesubt.n_subst_confs > 1:
        try:
            s.gen_confs(n_confs=cagesubt.n_subst_confs)
        except (ValueError, RuntimeError):
            logger.error('Could not generate substrate conformers')
            return None

    for i, substrate in enumerate(s.conformers):
        subst_coords = get_centered_substrate_coords(substrate)
        s.vdw_radii = [get_vdw_radii(atom) for atom in s.atoms]
        if s.mol_obj is not None:
            s.volume = AllChem.ComputeMolVolume(s.mol_obj, confId=i)

        for _ in range(cagesubt.n_init_geom):
            rot_angles = 2.0 * np.pi * np.random.rand(
                3)  # rand generates in [0, 1] so multiply with

            # Minimise the energy with a BFGS minimiser supporting bounds on
            # the values (rotation is periodic)
            result = minimize(get_energy,
                              x0=np.array(rot_angles),
                              args=(c, s, cagesubt.energy_func, cage_coords,
                                    subst_coords),
                              method='L-BFGS-B',
                              bounds=Bounds(lb=0.0, ub=2 * np.pi),
                              tol=0.01)

            energy = result.fun
            logger.info(f'Energy = {energy:.4f}')

            if energy < min_energy:
                min_energy = energy
                best_coords = get_rotated_subst_coords(result.x, subst_coords)

    logger.info(f'Min energy = {min_energy:.4f} kcal mol-1')
    cagesubt.binding_energy_kcal = min_energy

    if best_coords is not None:
        s.set_atoms(coords=best_coords)
        c.set_atoms(coords=cage_coords)

        return c.atoms + s.atoms

    else:
        return None