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 _find_possible_donor_atoms(self): """ For the atoms in the linker find all those capable of donating a 'lone pair' to a metal i.e. being a donor/ 'X-atom' :return: (list(int)) donor atoms """ donor_atom_ids = [] for atom_id, atom in enumerate(self.atoms): if atom.label in heteroatoms: max_valency = get_max_valency(atom=atom) n_bonds = 0 for bond in self.bonds: if atom_id in bond: n_bonds += 1 # If the number of bonds is lower than the max valancy for that # atom then there should be a lone pair and a donor atom has # been found if n_bonds < max_valency: donor_atom_ids.append(atom_id) logger.info(f'Found {len(donor_atom_ids)} possible X atoms') return donor_atom_ids
def _find_metallocage_mol(self): """ From a list of distinct molecules find the metallocage. This is assumed to be the molecule with the highest frequency of metal_label atoms :return: atoms, metal_label label """ mol_metals_and_freqs, metal = [], None for atoms in self.mols_atoms: metals_and_freq = dict.fromkeys(metals, 0) for atom in atoms: if atom.label in metals: metals_and_freq[atom.label] += 1 # Add the maximum frequency that any metal_label arises in the # structure metal = max(metals_and_freq, key=metals_and_freq.get) freq = metals_and_freq[metal] mol_metals_and_freqs.append((metal, freq)) logger.info(f'Max metal_label frequencies in molecules are ' f'{metal} with n = {freq}') mol_id_with_max_metals, max_freq, max_metal = 0, 0, None for i, (metal, freq) in enumerate(mol_metals_and_freqs): if freq > max_freq: max_freq = freq mol_id_with_max_metals = i max_metal = metal logger.info(f'Found metal_label {max_metal}') return self.mols_atoms[mol_id_with_max_metals], max_metal
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 __init__(self, arch_name, mol2_filename): """ Initialise a Template object :param arch_name: (str) Name of the architecture :param mol2_filename: (str) Name of the .mol2 file to read a structure from e.g. downloaded from the CCDC """ self.arch_name = arch_name all_atoms = mol2file_to_atoms(filename=mol2_filename) self.mols_atoms = find_mols_in_xyzs(atoms=all_atoms) self.atoms, self.metal_label = self._find_metallocage_mol() self.metals = self._find_metals() self.n_metals = len(self.metals) assert self.n_metals > 0 logger.info(f'Found {self.n_metals} metals') self.bonds = get_bond_list_from_atoms(self.atoms) self.x_atoms = self._find_donor_atoms() # donor atom ids self.coords = np.array([atom.coord for atom in self.atoms]) # centroid of the cage ~ average metal_label coordinate self.centroid = self._find_centroid() self.linkers = self._find_linkers() self.n_linkers = len(self.linkers) logger.info(f'Found {self.n_linkers} linkers') self._set_shift_vectors()
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_maximally_connected_x_motifs(x_motifs, x_atoms): """ Given a list of Xmotifs find those that are maximally connected, i.e. the ones that contain all the donor atoms but also are the largest in size :param x_motifs: (list(cgbind.x_motifs.Xmotif) :param x_atoms: (list(int)) :return: """ # X motif lengths sorted from high to low for x_motif_length in reversed(sorted(set([len(x) for x in x_motifs]))): new_x_motifs = [x for x in x_motifs if len(x) == x_motif_length] # Add all the atom ids of the xmotifs to a single list x_motifs_atoms = [] for x_motif in new_x_motifs: x_motifs_atoms += x_motif.atom_ids # All the donor (X) atoms need to be in the full list if all(x_atom in x_motifs_atoms for x_atom in x_atoms): logger.info(f'Returning {len(new_x_motifs)} Xmotifs each with ' f'{len(new_x_motifs[0])} atoms') return new_x_motifs logger.critical('Could not find a set of x motifs of the same length with' ' all the donor atoms') raise CgbindCritical
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 _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 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 find_x_motifs(linker): """ Find the X motifs in a structure which correspond to the X atoms and their nearest neighbours. These may be joined if they are bonded :return: (list(list(int))) """ def centroid_atom_distance(atom_i): return np.linalg.norm(linker.atoms[atom_i].coord - linker.com) x_motifs = [] for donor_atom in linker.x_atoms: x_motif = [] # Add all the atoms that are connected to the donor atom for (i, j) in linker.bonds: if donor_atom == i and j not in x_motif: x_motif.append(j) if donor_atom == j not in x_motif: x_motif.append(i) logger.info(f'X atom {donor_atom} had {len(x_motif)} ' f'nearest neighbours') x_motif.append(donor_atom) x_motifs.append(x_motif) # Get all the combinations of x motifs with length > 2 up to the total # number of x_motifs x_motif_combinations = powerset(s=deepcopy(x_motifs)) logger.info(f'Have {len(list(powerset(s=deepcopy(x_motifs))))} groups of X' f' motifs to determine if they are bonded') for i, x_motif_group in enumerate(x_motif_combinations): logger.info(f'Determining if all {len(x_motif_group)} x motifs in this' f' group are bonded') x_motif_group_atom_indexes = [] for x_motif in x_motif_group: x_motif_group_atom_indexes += list(x_motif) if is_fully_connected(x_motif_group_atom_indexes, bonds=linker.bonds): logger.info(f'X-motifs are bonded') x_motifs.append(list(set(x_motif_group_atom_indexes))) logger.info(f'Found {len(x_motifs)} X motifs in the linker, ' f'with {set([len(x) for x in x_motifs])} atoms') # Order the x_motifs according to the centroid – coord # distance: smallest -> largest sorted_x_motifs_ids = [ sorted(list(x_motif), key=centroid_atom_distance) for x_motif in x_motifs ] return [ Xmotif(atom_ids=motif, coords=[linker.atoms[i].coord for i in motif]) for motif in sorted_x_motifs_ids ]
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 _find_linkers(self): logger.info('Stripping the metals from the structure') atoms_no_metals = [ atom for atom in self.atoms if self.metal_label != atom.label ] logger.info('Finding the distinct linker molecules ') linkers_atoms = find_mols_in_xyzs(atoms=atoms_no_metals, allow_same=True) linkers = [] # Add the x_atoms which are contained within each linker, that were # found bonded to each metal_label for atoms in linkers_atoms: coords = np.array([atom.coord for atom in atoms]) linker_x_atoms = [] # Iterate through the coordinates until one matches that of the # full template for i, coord in enumerate(coords): for donor_atom_id in self.x_atoms: if list(coord) == list(self.coords[donor_atom_id]): linker_x_atoms.append(i) break linkers.append(Linker(atoms=atoms, x_atoms=linker_x_atoms)) logger.info(f'Linker has {len(linker_x_atoms)} donor atoms') logger.info(f'Found {len(linkers_atoms)} linkers each with ' f'{len(linker_x_atoms)} donor atoms') return linkers
def __init__(self, arch_name, smiles=None, name='linker', charge=0, n_confs=300, filename=None, use_etdg_confs=False): """ Metallocage Linker. Inherits from cgbind.molecule.Molecule :param arch_name: (str) Name of the architecture :param smiles: (str) SMILES string :param name: (str) Linker name :param charge: (int) :param n_confs: (int) Number of initial conformers to search through :param filename: (str) :param use_etdg_confs: (bool) Use a different, sometimes better, conformer generation algorithm """ logger.info(f'Initialising a Linker object for {name} with {n_confs} ' f'conformers') self.arch = None #: (Arch object) Metallocage architecture self._set_arch(arch_name) # May exit here if the specified architecture is not found super(Linker, self).__init__(smiles=smiles, name=name, charge=charge, n_confs=n_confs, filename=filename, use_etdg_confs=use_etdg_confs) # Allow linker construction with no atoms if self.n_atoms == 0: return self._check_structure() self.cage_template = get_template( arch_name=arch_name) #: (Template object) Metallocage template self.x_atoms = self._find_possible_donor_atoms( ) #: (list(int)) List of donor atom ids self.x_motifs = find_x_motifs(self) #: (list(Xmotif object)) check_x_motifs(self, linker_template=self.cage_template.linkers[0]) self._strip_possible_x_motifs_on_connectivity() self.dr = None #: (float) Template shift distance self.possibilities = []
def __init__(self, smiles=None, name='substrate', n_confs=1, charge=0, mult=1, filename=None, solvent=None): """ Substrate. Inherits from cgbind.molecule.Molecule :param smiles: (str) SMILES string :param name: (str) Molecule name :param n_confs: (int) Number of conformers to initialise with :param charge: (int) Charge on the molecule :param mult: (int) Spin multiplicity on the molecule :param filename: (str) """ logger.info('Initialising a Substrate object for {}'.format(name)) super().__init__(smiles=smiles, name=name, charge=charge, n_confs=n_confs, mult=mult, filename=filename, solvent=solvent)
def __init__(self, smiles=None, name='molecule', charge=0, mult=1, n_confs=1, filename=None, solvent=None, use_etdg_confs=False): """ Molecule. Inherits from cgbind.molecule.BaseStruct :param smiles: (str) SMILES string :param name: (str) Molecule name :param n_confs: (int) Number of conformers to initialise with :param charge: (int) Charge on the molecule :param mult: (int) Spin multiplicity on the molecule :param filename: (str) :param use_etdg_confs: (bool) Use an alternate conformer generation algorithm """ logger.info('Initialising a Molecule object for {}'.format(name)) super(Molecule, self).__init__(name=name, charge=charge, mult=mult, filename=filename, solvent=solvent) self.smiles = smiles #: (str) SMILES string self.n_confs = n_confs #: (int) Number of conformers initialised with self.mol_obj = None #: (RDKit.mol object) self.n_rot_bonds = None #: (int) Number of rotatable bonds self.n_h_donors = None #: (int) Number of H-bond donors self.n_h_acceptors = None #: (int) Number of H-bond acceptors self.volume = None #: (float) Molecular volume in Å^3 self.bonds = None #: (list(tuple)) List of bonds defined by atom ids self.conformers = None #: (list(BaseStruct)) List of conformers if smiles: self._init_smiles(smiles, use_etdg_confs=use_etdg_confs) if filename is not None: self.bonds = get_bond_list_from_atoms(self.atoms) self.conformers = [deepcopy(self)]
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 _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 calc_com(atoms): """ Calculate the centre of mass for a list of xyzs :param atoms: (list(cgbind.atoms.Atom)) :return: (np.ndarray) shape: 3 """ logger.info('Calculating centre of mass ') com = np.zeros(3) total_mass = 0.0 for atom in atoms: atom_mass = get_atomic_mass(atom) total_mass += atom_mass com += atom_mass * atom.coord return com / total_mass
def save_template(self): """ Save the template to ./lib/self.arch_name.obj :return: None """ logger.info('Saving metallocage template') assert self.n_metals > 0 assert all(metal.shift_vec is not None for metal in self.metals) # Templates will be saved to here/lib/ folder_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib') with open(os.path.join(folder_path, self.arch_name + '.obj'), 'wb') as pickled_file: pickle.dump(self, file=pickled_file) return None
def _find_donor_atoms(self): """ Find the donor atoms or 'x_atoms' in a metallocage. Will be bonded to the metal_label with a bond distance up to 1.1 x the value defined in cgbind.bonds. :return: (list(int)) """ logger.info('Getting the donor (x) atoms in a structure') donor_atoms = [] for (i, j) in self.bonds: if i in [metal.atom_id for metal in self.metals]: donor_atoms.append(j) if j in [metal.atom_id for metal in self.metals]: donor_atoms.append(i) logger.info(f'Found {len(donor_atoms)} donor atoms in the structure') return donor_atoms
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 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 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 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 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 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 find_mols_in_xyzs(atoms, allow_same=False): """ From a list of xyzs determine the bonds in the system, thus the distinct molecules in the system :param atoms: (list(cgbind.atoms.Atom)) :param allow_same: (bool) add only the unique molecules (False) or add every molecule (True) :return: (list(xyzs)) """ logger.info('Finding the distinct molecules in the system') # Get the 'molecules' for which each atom is contained in full_graph = nx.Graph() [ full_graph.add_node(n, atom_label=atoms[n].label) for n in range(len(atoms)) ] bond_list = get_bond_list_from_atoms(atoms) for (u, v) in bond_list: full_graph.add_edge(u, v) unique_mols, unique_mol_ids = [], [] connected_molecules = [ list(mol) for mol in nx.connected_components(full_graph) ] for molecule in connected_molecules: mol_atom_labels = sorted([atoms[n].label for n in molecule]) if mol_atom_labels not in unique_mols or allow_same: unique_mols.append(mol_atom_labels) unique_mol_ids.append(molecule) unique_mol_atoms = [[atoms[n] for n in mol_ids] for mol_ids in unique_mol_ids] logger.info(f'Found {len(unique_mol_atoms)} molecule(s)') return unique_mol_atoms
def __init__(self, atoms, x_atoms): """ Make a template linker object from the corresponding xyzs and the donor atoms which were bonded to the metals which form the basis of the cage :param atoms: (list(list)) :param x_atoms: (list(int)) Donor atom ids in the xyzs """ logger.info('Generating a template linker...') self.atoms = atoms #: (list(list)) self.x_atoms = x_atoms #: (list(int)) List of donor atoms in the linker self.coords = np.array([atom.coord for atom in atoms ]) #: (list(np.ndarray)) Linker coordinates self.bonds = get_bond_list_from_atoms(self.atoms) #: (list(Atom)) self.com = calc_com(atoms) #: (np.ndarray) self.x_motifs = find_x_motifs(self) #: (list(Xmotif objects) self.x_motifs = get_maximally_connected_x_motifs(self.x_motifs, x_atoms=x_atoms) check_x_motifs(linker_template=self ) # check that the x_motifs are the same length