def check_x_motifs(linker=None, linker_template=None): if linker is None and linker_template is not None: if not all([ motif.n_atoms == linker_template.x_motifs[0].n_atoms for motif in linker_template.x_motifs ]): logger.critical('Found x motifs in the structure that have ' 'different number of atoms') raise CgbindCritical else: return None if not all([ motif.n_atoms == linker_template.x_motifs[0].n_atoms for motif in linker.x_motifs ]): logger.warning('Found x motifs in the structure that have different ' 'number of atoms') logger.info('Stripping the motifs with the wrong number of atoms') linker.x_motifs = [ motif for motif in linker.x_motifs if motif.n_atoms == linker_template.x_motifs[0].n_atoms ] logger.info(f'Now have {len(linker.x_motifs)} motifs in the linker') if len(linker.x_motifs) == 0: raise CgbindCritical('Have 0 Xmotifs – cannot build a cage. ' 'Is the template correct?') if len(linker.x_motifs) > 0: logger.info(f'Number of atoms in the x motifs is ' f'{linker.x_motifs[0].n_atoms}') return None
def build_heteroleptic_cage(cage, max_cost): logger.info('Building a heteroleptic cage') logger.warning('Due to the very large space that needs to be minimised ' 'only the *best* linker conformer is used') added_linkers, atoms = [], [] for i, linker in enumerate(cage.linkers): linker.set_ranked_linker_possibilities(metal=cage.metal) linker_atoms, cost = get_linker_atoms_and_cost(linker.possibilities[0], cage.cage_template.linkers[i], atoms) logger.info(f'L-L repulsion + fit to template in building cage is {cost:.2f}') atoms += linker_atoms linker.dr = linker.possibilities[0].dr logger.warning('Heteroleptic cages will have the average dr of all linkers ' '- using the average') cage.dr = np.average(np.array([linker.dr for linker in cage.linkers])) # Add the metals from the template shifted by dr for metal in cage.cage_template.metals: metal_coord = cage.dr * metal.shift_vec / np.linalg.norm(metal.shift_vec) + metal.coord atoms.append(Atom(cage.metal, x=metal_coord[0], y=metal_coord[1], z=metal_coord[2])) cage.set_atoms(atoms) return None
def get_bond_list_from_atoms(atoms, relative_tolerance=0.2): """Determine the 'bonds' between atoms defined in a xyzs list. :param atoms: (list(cgbind.atoms.Atom)) :param relative_tolerance: (float) :return: (list(tuple(int))) """ logger.info(f'Getting bond list from xyzs. Maximum bond is ' f'{1 + relative_tolerance}x average') bond_list = [] for i in range(len(atoms)): for j in range(len(atoms)): if i > j: # Calculate the distance between the two points in Å dist = np.linalg.norm(atoms[j].coord - atoms[i].coord) i_j_bond_length = get_avg_bond_length(atoms[i].label, atoms[j].label) if dist < i_j_bond_length * (1.0 + relative_tolerance): bond_list.append((i, j)) if len(bond_list) == 0: logger.warning('Bond list is empty') return bond_list
def is_geom_reasonable(molecule): """ For an xyz list check to ensure the geometry is sensible, before an optimisation is carried out. There should be no distances smaller than 0.7 Å :param molecule: (cgbind.molecule.BaseStruct) :return: (bool) """ logger.info('Checking to see whether the geometry is reasonable') coords = molecule.get_coords() # Compute the distance matrix with all i,j pairs, thus add 1 to the # diagonals to remove the d(ii) = 0 components that would otherwise result # in an unreasonable geometry dist_mat = distance_matrix(coords, coords) + np.identity(len(coords)) if np.min(dist_mat) < 0.8: logger.warning('There is a distance < 0.8 Å. There is likely a problem' ' with the geometry') return False if np.max(dist_mat) > 1000: logger.warning('There is a distance > 1000 Å. There is likely a ' 'problem with the geometry') return False return True
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 _set_shift_vectors(self): """ For the linkers set the shift vectors for the x motifs. These are the centroid -> closest metal_label atom vector :return: None """ metal_atom_ids = [metal.atom_id for metal in self.metals] for linker in self.linkers: for x_motif in linker.x_motifs: motif_atom_id = x_motif.atom_ids[0] closest_metal_id = None closest_dist = 99999.9 for metal_id in metal_atom_ids: dist = np.linalg.norm(linker.coords[motif_atom_id] - self.coords[metal_id]) if dist < closest_dist: closest_dist = dist closest_metal_id = metal_id shift_vec = self.coords[closest_metal_id] - self.centroid # Set the shift vector for the metal closest to this x_motif for metal in self.metals: if metal.atom_id == closest_metal_id: metal.shift_vec = shift_vec # Set the shift vector for the x motif x_motif.shift_vec = shift_vec x_motif.r = np.linalg.norm(x_motif.shift_vec) x_motif.norm_shift_vec = x_motif.shift_vec / x_motif.r metal_coords = np.array([metal.coord for metal in self.metals]) metals_dists = distance_matrix(metal_coords, metal_coords) # Ensure that all the metals have shift vectors for i, metal in enumerate(self.metals): if metal.shift_vec is not None: continue logger.warning('Unassigned shift vector – setting the same' 'as the closest metal to this one') closest_metals = np.argsort(np.min(metals_dists, axis=0)) # Set the shift vector of this metal as the one closest # that is not itself for metal_idx in closest_metals[1:]: if self.metals[metal_idx].shift_vec is not None: metal.shift_vec = self.metals[metal_idx].shift_vec break return None
def get_avg_bond_length(atom_i_label, atom_j_label): """Get the average bond length from either a molecule and a bond or two atom labels (e.g. atom_i_label = 'C' atom_j_label = 'H')""" key1, key2 = atom_i_label + atom_j_label, atom_j_label + atom_i_label if key1 in avg_bond_lengths.keys(): return avg_bond_lengths[key1] elif key2 in avg_bond_lengths.keys(): return avg_bond_lengths[key2] else: logger.warning(f'Couldn\'t find a default bond length for ' f'({atom_i_label},{atom_j_label}) using 1.5 Å') return 1.5
def get_fitted_linker_coords_and_cost(linker, template_x_coords, coords_to_fit, curr_coords): """ For a linker get the best mapping onto a list of template X coords (e.g. NCN motifs in a pyridyl donor) these will this can be achieved in normal or reverse order of the coordinates as to maximise the distance to the rest of the metallocage structure. Also returns a measure of the repulsion to the rest of the cage structure :param linker: (Linker object) :param template_x_coords: (list(np.ndarray)) :param coords_to_fit: (list(np.ndarray)) :param curr_coords: (list(list)) :return: (list(np.ndarray)), (float) """ min_cost, best_linker_coords = 99999999999.9, None coord_set = [coords_to_fit] if Config.allow_permutations: logger.info('Allowing permutations of coords - only reverse') coord_set.append(list(reversed(coords_to_fit))) for coords in coord_set: new_linker_coords, cost = get_kfitted_coords_and_cost(linker=linker, template_x_coords=template_x_coords, coords_to_fit=coords) if len(curr_coords) == 0: return new_linker_coords, 0.0 repulsion = np.sum(np.power(distance_matrix(new_linker_coords, curr_coords), -12)) # Add the linker with the least repulsion to the rest of the structure if repulsion + cost < min_cost: best_linker_coords = new_linker_coords min_cost = repulsion + cost if best_linker_coords is None: logger.warning('Fitted linker coords could not be found') best_linker_coords = linker.get_coords() return best_linker_coords, min_cost
def set_atoms(self, atoms=None, coords=None): """ Set the xyzs of a molecular structure :param atoms: (list(cgbind.atoms.Atom)) :param coords: (np.ndarray) n_atoms x 3 positions of the atoms :return: None """ # Reset the atoms in this species using an array of coordinates if coords is not None: assert type(coords) == np.ndarray assert coords.shape == (self.n_atoms, 3) # Set the coordinates on a copy of the atoms atoms = deepcopy(self.atoms) for i, coord in enumerate(coords): atoms[i].coord = coord # Reset the atoms, number of atoms and the centre of mass if atoms is not None: assert type(atoms) == list assert len(atoms) > 0 assert hasattr(atoms[0], 'label') assert hasattr(atoms[0], 'coord') assert len(atoms[0].coord) == 3 self.atoms = atoms self.n_atoms = len(atoms) self.com = calc_com(atoms=self.atoms) self.reasonable_geometry = is_geom_reasonable(self) logger.info(f'Geometry is reasonable: {self.reasonable_geometry}') else: self.reasonable_geometry = False logger.warning( 'xyzs were None -> n_atoms also None & geometry is *not* reasonable' ) return None
def get_cost_metal_x_atom_interaction(x_motifs, linker, metal): """ Calculate a cost based on the favourability of the M--X interaction :param x_motifs: ((list(Xmotif)) :param linker: (Linker) :param metal: (str) :return: """ if metal is None: logger.warning('Could not sort x motifs list. Metal was not specified') return 0 fav_x_atoms = get_metal_favoured_heteroatoms(metal=metal) cost = 0 for x_motif in x_motifs: for atom_id in x_motif.atom_ids: atom_label = linker.atoms[atom_id].label if atom_label in fav_x_atoms: cost += 10 * fav_x_atoms.index(atom_label) return cost
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 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