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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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}')
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
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
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
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)
def _check_structure(self): if self.n_atoms == 0: logger.error('Could get atoms for linker') raise NoXYZs return None
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