diff --git a/architector/complex_construction.py b/architector/complex_construction.py index 7784376..40b43e4 100644 --- a/architector/complex_construction.py +++ b/architector/complex_construction.py @@ -26,8 +26,10 @@ class Ligand: """Class to contain all information about a ligand including conformers.""" - def __init__(self, smiles, ligcoordList, corecoordList, core, ligGeo, ligcharge, ca_metal_dist_constraints, - covrad_metal=None, vdwrad_metal=None, debug=False): + def __init__(self, smiles, ligcoordList, corecoordList, core, + ligGeo, ligcharge, ca_metal_dist_constraints, + covrad_metal=None, vdwrad_metal=None, enforce_symmetry=False, + debug=False): """Set up initial variables for ligand and run conformer generation routines. Parameters @@ -50,6 +52,8 @@ def __init__(self, smiles, ligcoordList, corecoordList, core, ligGeo, ligcharge, Covalent radii of the metal, default values from io_ptable.rcov1 vdwrad_metal : float, optional VDW radii of the metal, default values from io_ptable.rvdw + enforce_symmetry : bool, optional + Enforce the ligand symmetry elements during conformer generation? debug: bool, optional debug turns on/off output text/comments. """ @@ -59,32 +63,35 @@ def __init__(self, smiles, ligcoordList, corecoordList, core, ligGeo, ligcharge, self.corecoordList = corecoordList self.metal = core self.ca_metal_dist_constraints = ca_metal_dist_constraints - self.geo = ligGeo + self.geo = ligGeo self.BO_dict = None self.atom_types = None self.out_ligcoordLists = None self.charge = ligcharge + self.enforce_symmetry = enforce_symmetry self.liggen_start_time = time.time() # Generate conformations if debug: print("GENERATING CONFORMATIONS for {}".format(smiles)) - conformers, rotscores, tligcoordList, relax, bo_dict, atypes, rotlist = io_lig.find_conformers(self.smiles, - self.ligcoordList, - self.corecoordList, - metal=self.metal, - ligtype=self.geo, - ca_metal_dist_constraints=self.ca_metal_dist_constraints, - covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal, - debug=debug - ) + conformers, rotscores, tligcoordList, relax, bo_dict, atypes, rotlist = io_lig.find_conformers( + self.smiles, + self.ligcoordList, + self.corecoordList, + metal=self.metal, + ligtype=self.geo, + ca_metal_dist_constraints=self.ca_metal_dist_constraints, + covrad_metal=covrad_metal, + vdwrad_metal=vdwrad_metal, + enforce_symmetry=self.enforce_symmetry, + debug=debug + ) if len(conformers) > 0: self.conformerList = conformers self.conformerRotScore = rotscores self.rotList = rotlist self.out_ligcoordLists = tligcoordList self.selectedConformer = self.conformerList[0] - self.exists = True + self.exists = True self.relax = relax self.BO_dict = bo_dict self.atom_types = atypes @@ -238,10 +245,10 @@ def compute_conformer_efficacy(self,conformerList,rot_vals,ligand): elif (not out_eval.successful) and (self.parameters['debug']): print('Ligand {} failed xtb/uff or overlapped.'.format(i)) return bestConformer, assembled - + def final_eval(self, single_point=False): """final_eval perform final evaulation of full complex conformer with XTB. - + Involves either a relaxation or not for each "sane" conformer. Parameters @@ -366,7 +373,9 @@ def gen_aligned_complex(newLigInputDicts, lig_ca_metal_dist_constraints, covrad_metal=inputDict['parameters']['covrad_metal'], vdwrad_metal=inputDict['parameters']['vdwrad_metal'], - debug=inputDict['parameters']['debug'] + enforce_symmetry=inputDict['parameters'].get( + 'enforce_ligand_internal_symmetry', False), + debug=inputDict['parameters']['debug'], ) # Store results ligandDict[ligid] = ligandClass diff --git a/architector/io_arch_dock.py b/architector/io_arch_dock.py index 64b942a..eb7323e 100644 --- a/architector/io_arch_dock.py +++ b/architector/io_arch_dock.py @@ -30,7 +30,7 @@ # e.g. Reduce to allow for closer molecule-species distances 'species_location_method': 'default', # Default attempts a basic colomb repulsion placement, targeted # Only other option is 'random' at the moment. - 'species_add_copies': 1, # Number of species addition orientations to build + 'species_add_copies': 1, # Number of species addition orientations to build 'species_method': 'GFN2-xTB', # Method to use on full species - right now only GFN2-xTB really works 'species_relax': True, # Whether or not to relax the generated secondary solvation structures. 'species_intermediate_method': 'GFN-FF', # Method to use for intermediate species screening - Suggested GFN-FF diff --git a/architector/io_calc.py b/architector/io_calc.py index 4bb2b76..37fb09a 100644 --- a/architector/io_calc.py +++ b/architector/io_calc.py @@ -208,7 +208,7 @@ def __init__(self, structure, parameters={}, init_sanity_check=False, self.detect_spin_charge = detect_spin_charge if len(parameters) > 0: for key,val in parameters.items(): - setattr(self,key,val) + setattr(self, key, val) if assembly: self.init_sanity_check = True self.relax = False diff --git a/architector/io_lig.py b/architector/io_lig.py index c5c4c0f..6076295 100644 --- a/architector/io_lig.py +++ b/architector/io_lig.py @@ -930,6 +930,7 @@ def reorder_ligcoordList(coords, catoms, shape, ligcoordList, isCp=False): def clean_conformation_ff(X, OBMol, catoms, shape, graph, atomic_numbers, ligcoordList, + ligsmiles=None, original_metal='Fe', add_angle_constraints=True, ca_metal_dist_constraints=None, @@ -937,6 +938,7 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, skip_mff=False, add_hydrogens=True, edge_ligand=False, + enforce_symmetry=False, debug=False ): """clean_conformation_ff @@ -957,6 +959,10 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, atomic_numbers : np.ndarray N array containing the atomic numbers -> used for flagging hydrogens as second-nearest neighbors to the metal. + ligcoordList : list + List of lists indicating [coordinating atom index, coordinating atom location on core] + ligsmiles : str + SMILES string of ligand to look at. original_metal : str, optional Atomic symbol of original metal, by default 'Fe' add_angle_constraints : bool @@ -971,6 +977,9 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, Whether hydrogens need to be added to the structure, default True edge_ligand : bool, optional Whether this is an "edge" bound ligand. + enforce_symmetry : bool, optional + Enforce the symmetry assigned by the SMILES string -- [@@, /] + Tetrahedral/cis/trans only! debug : bool, optional Whether to print debug messages. @@ -984,6 +993,8 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, whether or not to relax at the final step. tligcoordList : list re-ordered ligcoord list based on the actual geometry from ff relaxation + symmetry_match : bool + Does the symmetry of the structure match that of the SMILES string? """ ##### STEP 1 Relaxation with UFF ###### last_atom_index = OBMol.NumAtoms() @@ -1112,13 +1123,37 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, tligcoordList = ligcoordList.copy() if add_hydrogens: # Re-add hydrogens after generation and UFF relaxation. OBMol.AddHydrogens() - ########## Second stage of cleaning removes the metal - MMFF94 ################# + ########## Second stage of cleaning removes the metal - MMFF94 ################# #### Uses constraints on the bonding atoms to ensure the binding conformation is maintained #### If not Cp ligand - otherwise - fully relaxing! # Delete the dummy metal atom that we added earlier for MMFF94 metal_atom = OBMol.GetAtom(last_atom_index) OBMol.DeleteAtom(metal_atom) + + match_symmetry = True + # Check symmetry if asked for. + if enforce_symmetry: + if debug: + print('Starting Symmetry') + smiobmol = io_obabel.get_obmol_smiles(ligsmiles, build=False) + cansmi = io_obabel.get_smiles_obmol(smiobmol, canonicalize=True) + smiobmol1 = io_obabel.get_obmol_smiles(cansmi, build=False) + smi_symmetry = io_obabel.get_stereo_label(smiobmol1, tetrahedral=True, + ct=False) + + out_cansmi = io_obabel.get_smiles_obmol(OBMol, canonicalize=True) + out_obmol = io_obabel.get_obmol_smiles(out_cansmi, build=False) + out_symmetry = io_obabel.get_stereo_label(out_obmol, tetrahedral=True, + ct=False) + + match_symmetry = smi_symmetry == out_symmetry + if debug: + print("SYMMETRY_MATCH: ", match_symmetry) + print("CANSMI: ", smi_symmetry) + print("STRUCT: " + out_symmetry) + print(io_obabel.convert_obmol_mol2(OBMol)) + _, atomic_numbers, graph = io_obabel.get_OBMol_coords_anums_graph(OBMol, return_coords=False) @@ -1139,7 +1174,7 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, for n in neighs: # Freeze Hs on coordinating atoms (w/o metal will go into metal position) if atomic_numbers[n] == 1: - constr.AddAtomConstraint(int(n+1)) + constr.AddAtomConstraint(int(n+1)) s = ff.Setup(OBMol, constr) else: # Fixing tail/non-coordinating positions with MMFF -> attempt to fix hydrogen positions @@ -1175,11 +1210,11 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, fixCon = ase_con.FixAtoms(indices=cp_catoms) constraintList.append(fixCon) final_relax = False - + constraintList.append(fixCore) ase_atoms_tmp.set_constraint(constraintList) ase_atoms_tmp = set_XTB_calc(ase_atoms_tmp) - try: # Optimize structure with GFNFF + try: # Optimize structure with GFNFF with arch_context_manage.make_temp_directory() as _: dyn = BFGSLineSearch(ase_atoms_tmp, master=True) dyn.run(fmax=0.1, steps=1000) @@ -1191,74 +1226,7 @@ def clean_conformation_ff(X, OBMol, catoms, shape, graph, ase_atoms_tmp.calc = None # Remove calculator ase_atoms_tmp.pop() # re-remove metal atom - return ase_atoms_tmp, fail, final_relax, tligcoordList - - -def nonclean_conformation_ff(X, OBMol, catoms, shape, graph, - atomic_numbers, ligcoordList, - add_angle_constraints=True, - isCp=False, cp_catoms=[], - skip_mff=False, add_hydrogens=False - ): - """nonclean_conformation_ff -> Mostly used for debug/DG optimization - Skips all OB FF and saves to a new ase object. - - Generaly not as useful except for debugging. Leaving here for reference. - - Parameters - ---------- - X : np.array - Array of distgeom generated coordinates. - catoms : list - List of coordinating atoms used to generate FF constraints. - OBMol : OBmol - OBmol of original molecule with dummy metal added - shape : np.ndarray - Matrix containing ideal angles for coordinating atoms through metal - graph : np.ndarray - NXN array representing the molecular graph - atomic_numbers : np.ndarray - N array containing the atomic numbers -> used for flagging hydrogens as - second-nearest neighbors to the metal. - add_angle_constraints : bool - Whether or not to add angle constraints to force ligand into position, default False - isCp : bool, optional - whether this is a cp ligand, by default False - skip_mff : bool, optional - wheter to skip the mmff94 intermediate relaxatino, default False - add_hydrogens : bool, optional - Whether hydrogens need to be added to the structure, default True - - Returns - ------- - ase_atoms : ase.Atoms - ff-relaxed conformer in ase atoms - fail : bool - if any of the FF steps failed to converge. - final_relax : bool - whether or not to relax at the final step. - tligcoordList : list - re-ordered ligcoord list based on the actual geometry from ff relaxation - """ - # print('Starting FF relaxation.') - last_atom_index = OBMol.NumAtoms() - fail = False - # Set metal to zero - metal_coords = (X[last_atom_index-1,0],X[last_atom_index-1,1],X[last_atom_index-1,2]) - last_atom_index = OBMol.NumAtoms() - # set coordinates using OBMol to keep bonding info - for i, atom in enumerate(openbabel.OBMolAtomIter(OBMol)): - atom.SetVector(X[i, 0]-metal_coords[0], X[i, 1]-metal_coords[1], X[i, 2]-metal_coords[2]) - coords, _, _ = io_obabel.get_OBMol_coords_anums_graph(OBMol, return_coords=True) - # Reorder the connecting atom assignment to more closely match the actual geometry - tligcoordList, catoms = reorder_ligcoordList(coords, catoms, shape, ligcoordList, isCp=isCp) - #Delete the dummy metal atom that we added earlier - metal_atom = OBMol.GetAtom(last_atom_index) - OBMol.DeleteAtom(metal_atom) - ase_atoms_tmp = io_obabel.convert_obmol_ase(OBMol,add_hydrogens=add_hydrogens) - fail = False - final_relax = True - return ase_atoms_tmp, fail, final_relax, tligcoordList + return ase_atoms_tmp, fail, final_relax, tligcoordList, match_symmetry def set_position_align(ase_atoms, ligcoordList, corecoordList, isCp=False, debug=False, @@ -1338,17 +1306,17 @@ def set_position_align(ase_atoms, ligcoordList, corecoordList, isCp=False, debug minval = outr[1] r = outr if (minval > 1.0) or isCp: # Take combinations of coordinating atoms to try and maximize overlap with coordination sites. - inds = np.arange(0,len(ideal),1) + inds = np.arange(0, len(ideal), 1) orderings = np.array([np.array(x) for x in itertools.permutations(inds,len(inds))]) if len(orderings) > 100: - ordering_inds = np.random.choice(np.arange(0,len(orderings),1),100) + ordering_inds = np.random.choice(np.arange(0, len(orderings), 1), 100) orderings = orderings[ordering_inds] r = [] minval = 1000 save_ordering = [] for ordering in orderings: tactual = actual[ordering] - outr = Rot.align_vectors(ideal,tactual) + outr = Rot.align_vectors(ideal, tactual) if outr[1] < minval: r = outr save_ordering = ordering @@ -1410,16 +1378,17 @@ def set_position_align(ase_atoms, ligcoordList, corecoordList, isCp=False, debug rotatedConformer = conformerMolCpy return (rotatedConformer, minval, sane) -def get_aligned_conformer(ligsmiles, ligcoordList, corecoordList, metal='Fe', +def get_aligned_conformer(ligsmiles, ligcoordList, corecoordList, metal='Fe', add_angle_constraints=True, non_triangle=False, ca_metal_dist_constraints=None, rot_coord_vect=False, rot_angle=0, skip_mmff=False, covrad_metal=None, vdwrad_metal=None, no_ff=False, ligtype=None, + enforce_symmetry=False, debug=False ): """Uses distance geometry to get a random conformer. - + Parameters ---------- ligsmiles : str @@ -1427,7 +1396,7 @@ def get_aligned_conformer(ligsmiles, ligcoordList, corecoordList, metal='Fe', ligcoordList : list List of lists indicating [coordinating atom index, coordinating atom location on core] corecoordList : list - List of lists inidicated connection sites by geometry + List of lists inidicated connection sites by geometry metal : str, optional Metal to be calculated -> by default, Fe covrad_metal : float, optional @@ -1438,9 +1407,11 @@ def get_aligned_conformer(ligsmiles, ligcoordList, corecoordList, metal='Fe', Whether to do any ff cleaning at all. ligtype : None/str Ligand type if given. + enforce_symmetry: Bool, optional + Perform symmetry checks on generated smiles before returning? Default False debug : bool, optional Print out debug messages, default False - + Returns ------- outatoms : ase.Atoms @@ -1448,327 +1419,344 @@ def get_aligned_conformer(ligsmiles, ligcoordList, corecoordList, metal='Fe', minval : float rotational loss for the ligand atoms to the coordination sites. """ - edge_ligand=False - if isinstance(ligtype,str): + edge_ligand = False + if isinstance(ligtype, str): if 'edge' in ligtype: - edge_ligand=True - Conf3D = io_obabel.get_obmol_smiles(ligsmiles, addHydrogens=True) - # Detect Cp rings - move before adding metal center. - isCp, cp_rings, shared_coords = detect_cps(Conf3D, ligcoordList) - catoms = [x[0] for x in ligcoordList] - # Calc charges from total charge from OBmol - init_charges_lig = np.zeros(Conf3D.NumAtoms()) - tmetal, _ = io_ptable.convert_actinides_lanthanides(metal) - catoms = [x[0] for x in ligcoordList] - dummy_metal = openbabel.OBAtom() # Add the dummy metal to the OBmol - dummy_metal.SetAtomicNum(io_ptable.elements.index(tmetal)) # Match atomic number - Conf3D.AddAtom(dummy_metal) - for i in catoms: - Conf3D.AddBond(int(i+1), Conf3D.NumAtoms(), 1) - allcoords, anums, graph = io_obabel.get_OBMol_coords_anums_graph(Conf3D) - bo_dict, atypes = io_obabel.get_OBMol_bo_dict_atom_types(Conf3D) - init_charges_lig[0] = Conf3D.GetTotalCharge() - cp_catoms = [] - if isCp: - ligcoordList, corecoordList, cp_catoms, catoms = manage_cps(Conf3D, cp_rings, - shared_coords, - ligcoordList, - corecoordList, - tmetal, - covrad_metal=covrad_metal - ) - # Check if any h-bound metal centers. - if (not any([True for x in catoms if (anums[x] == 1)])): - # If not - run DG without hydrogens - add back during FF relaxation! - Conf3D = io_obabel.get_obmol_smiles(ligsmiles, addHydrogens=False) - dummy_metal = openbabel.OBAtom() # Add the dummy metal to the OBmol - dummy_metal.SetAtomicNum(io_ptable.elements.index(tmetal)) # Match atomic number + edge_ligand = True + leave_loop = False + while not leave_loop: # Symmetry iteration loop + Conf3D = io_obabel.get_obmol_smiles(ligsmiles, addHydrogens=True) + # Detect Cp rings - move before adding metal center. + isCp, cp_rings, shared_coords = detect_cps(Conf3D, ligcoordList) + catoms = [x[0] for x in ligcoordList] + # # Get cis/trans double bond fixed symmetries + # fix_pairs_dict = io_obabel.get_stereo_fix_bond_dihedrals(Conf3D) + # Calc charges from total charge from OBmol + init_charges_lig = np.zeros(Conf3D.NumAtoms()) + tmetal, _ = io_ptable.convert_actinides_lanthanides(metal) + catoms = [x[0] for x in ligcoordList] + dummy_metal = openbabel.OBAtom() # Add the dummy metal to the OBmol + dummy_metal.SetAtomicNum(io_ptable.elements.index(tmetal)) # Match atomic number Conf3D.AddAtom(dummy_metal) for i in catoms: Conf3D.AddBond(int(i+1), Conf3D.NumAtoms(), 1) - natoms = Conf3D.NumAtoms() allcoords, anums, graph = io_obabel.get_OBMol_coords_anums_graph(Conf3D) - add_hydrogens=True - else: - natoms = Conf3D.NumAtoms() - add_hydrogens=False - core_vects = [corecoordList[x[1]] for x in ligcoordList] - vdwrad = [io_ptable.rvdw[x] for x in anums] - if isinstance(vdwrad_metal,float): - vdwrad[-1] = vdwrad_metal - cov1rad = [io_ptable.rcov1[x] for x in anums] - if isinstance(covrad_metal,float): - cov1rad[-1] = covrad_metal - ml_dists = [] - if isCp: # Specifiy distances from geometry - for cp_at in cp_catoms: - tdist = np.linalg.norm([corecoordList[x[1]] for x in ligcoordList if x[0] == cp_at][0]) - ml_dists.append(tdist) - for c_atom in [x for x in catoms if x not in cp_catoms]: - ml_dists.append(cov1rad[-1] + cov1rad[c_atom]) - + bo_dict, atypes = io_obabel.get_OBMol_bo_dict_atom_types(Conf3D) + init_charges_lig[0] = Conf3D.GetTotalCharge() + cp_catoms = [] + if isCp: + ligcoordList, corecoordList, cp_catoms, catoms = \ + manage_cps( + Conf3D, cp_rings, + shared_coords, + ligcoordList, + corecoordList, + tmetal, + covrad_metal=covrad_metal + ) + # Check if any h-bound metal centers. + if (not any([True for x in catoms if (anums[x] == 1)])): + # If not - run DG without hydrogens - add back during FF relaxation! + Conf3D = io_obabel.get_obmol_smiles(ligsmiles, addHydrogens=False) + dummy_metal = openbabel.OBAtom() # Add the dummy metal to the OBmol + dummy_metal.SetAtomicNum(io_ptable.elements.index(tmetal)) # Match atomic number + Conf3D.AddAtom(dummy_metal) + for i in catoms: + Conf3D.AddBond(int(i+1), Conf3D.NumAtoms(), 1) + natoms = Conf3D.NumAtoms() + allcoords, anums, graph = io_obabel.get_OBMol_coords_anums_graph(Conf3D) + add_hydrogens = True + else: + natoms = Conf3D.NumAtoms() + add_hydrogens = False + core_vects = [corecoordList[x[1]] for x in ligcoordList] + vdwrad = [io_ptable.rvdw[x] for x in anums] + if isinstance(vdwrad_metal, float): + vdwrad[-1] = vdwrad_metal + cov1rad = [io_ptable.rcov1[x] for x in anums] + if isinstance(covrad_metal, float): + cov1rad[-1] = covrad_metal + ml_dists = [] + if isCp: # Specifiy distances from geometry + for cp_at in cp_catoms: + tdist = np.linalg.norm([corecoordList[x[1]] for x in ligcoordList if x[0] == cp_at][0]) + ml_dists.append(tdist) + for c_atom in [x for x in catoms if x not in cp_catoms]: + ml_dists.append(cov1rad[-1] + cov1rad[c_atom]) + shape = get_ideal_angles(core_vects) ############# Distance Geometry Section ############## # Here, I start with tighter constraints and loosen them to get to a final conformer. - shape = get_ideal_angles(core_vects) - status = False - count = 0 - fail_gen = False - LB, UB = get_bounds_matrix(allcoords, graph, natoms, - catoms, shape, - ml_dists, vdwrad, anums, - isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, - add_angle_constraints=add_angle_constraints) - while not status: - try: - tLB = LB.copy() - tUB = UB.copy() - D = metrize(tLB, tUB, natoms, non_triangle=non_triangle,debug=debug) - D0 = get_cm_dists(D, natoms) - G = get_metric_matrix(D, D0, natoms) - L, V = get_3_eigs(G, natoms) - X = np.dot(V, L) # get projection - except: - X = np.array([np.nan]) - - if np.any(np.isnan(X)): # If any are not nan continue! - if count > 9: - fail_gen = True - status = True - else: - count += 1 - else: - status = True - if fail_gen: - if debug: - print('Trying More Dist Geom with 0.2 Bond Tol LB and Loosened Metal_Center Constraints.') status = False count = 0 - LB, UB = get_bounds_matrix(allcoords, graph, natoms, - catoms, shape, - ml_dists, vdwrad, anums, bond_tol=0.2, metal_center_lb_multiplier=0.9, - h_bond_tol=0.0, h_angle_tol=0.1, - isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, - add_angle_constraints=add_angle_constraints) + fail_gen = False + LB, UB = get_bounds_matrix( + allcoords, graph, natoms, + catoms, shape, + ml_dists, vdwrad, anums, + isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, + add_angle_constraints=add_angle_constraints) while not status: try: tLB = LB.copy() tUB = UB.copy() - D = metrize(tLB, tUB, natoms, non_triangle=non_triangle,debug=debug) + D = metrize(tLB, tUB, natoms, + non_triangle=non_triangle, + debug=debug) D0 = get_cm_dists(D, natoms) G = get_metric_matrix(D, D0, natoms) L, V = get_3_eigs(G, natoms) X = np.dot(V, L) # get projection except: - if debug > 1: - print('Gen Exception.') X = np.array([np.nan]) - - if np.any(np.isnan(X)): # If any are not nan continue! - if debug > 1: - # print('V: ',V, 'L: ', L, 'X: ',X) - print('DG iteration passing.') - if count > 200: + + if np.any(np.isnan(X)): # If any are not nan continue! + if count > 9: fail_gen = True status = True else: count += 1 else: status = True - fail_gen = False # Worked! - if fail_gen: - if debug > 1: - print('Trying More Dist Geom with 0.2 Bond Tol LB, 0.2 Angle tol LB, and Loosened Metal_Center Constraints') - status = False - count = 0 - LB, UB = get_bounds_matrix(allcoords, graph, natoms, - catoms, shape, - ml_dists, vdwrad, anums, bond_tol=0.2,angle_tol=0.2, - metal_center_lb_multiplier=0.8, - h_bond_tol=0.1, h_angle_tol=0.1, - isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, - add_angle_constraints=add_angle_constraints) - while not status: - try: - tLB = LB.copy() - tUB = UB.copy() - D = metrize(tLB, tUB, natoms, non_triangle=non_triangle,debug=debug) - D0 = get_cm_dists(D, natoms) - G = get_metric_matrix(D, D0, natoms) - L, V = get_3_eigs(G, natoms) - X = np.dot(V, L) # get projection - except: - if debug > 1: - print('Gen Exception.') - X = np.array([np.nan]) - - if np.any(np.isnan(X)): # If any are not nan continue! - if debug > 1: - # print('V: ',V, 'L: ', L, 'X: ',X) - print('DG iteration passing.') - if count > 200: - fail_gen = True + if fail_gen: + if debug: + print('Trying More Dist Geom with 0.2 Bond Tol LB and Loosened Metal_Center Constraints.') + status = False + count = 0 + LB, UB = get_bounds_matrix( + allcoords, graph, natoms, + catoms, shape, + ml_dists, vdwrad, anums, bond_tol=0.2, + metal_center_lb_multiplier=0.9, + h_bond_tol=0.0, h_angle_tol=0.1, + isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, + add_angle_constraints=add_angle_constraints) + while not status: + try: + tLB = LB.copy() + tUB = UB.copy() + D = metrize(tLB, tUB, natoms, non_triangle=non_triangle,debug=debug) + D0 = get_cm_dists(D, natoms) + G = get_metric_matrix(D, D0, natoms) + L, V = get_3_eigs(G, natoms) + X = np.dot(V, L) # get projection + except: + if debug > 1: + print('Gen Exception.') + X = np.array([np.nan]) + + if np.any(np.isnan(X)): # If any are not nan continue! + if debug > 1: + # print('V: ',V, 'L: ', L, 'X: ',X) + print('DG iteration passing.') + if count > 200: + fail_gen = True + status = True + else: + count += 1 + else: status = True + fail_gen = False # Worked! + if fail_gen: + if debug > 1: + print('Trying More Dist Geom with 0.2 Bond Tol LB, 0.2 Angle tol LB, and Loosened Metal_Center Constraints') + status = False + count = 0 + LB, UB = get_bounds_matrix( + allcoords, graph, natoms, + catoms, shape, + ml_dists, vdwrad, anums, bond_tol=0.2, angle_tol=0.2, + metal_center_lb_multiplier=0.8, + h_bond_tol=0.1, h_angle_tol=0.1, + isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, + add_angle_constraints=add_angle_constraints) + while not status: + try: + tLB = LB.copy() + tUB = UB.copy() + D = metrize(tLB, tUB, natoms, + non_triangle=non_triangle, + debug=debug) + D0 = get_cm_dists(D, natoms) + G = get_metric_matrix(D, D0, natoms) + L, V = get_3_eigs(G, natoms) + X = np.dot(V, L) # get projection + except: + if debug > 1: + print('Gen Exception.') + X = np.array([np.nan]) + + if np.any(np.isnan(X)): # If any are not nan continue! + if debug > 1: + # print('V: ',V, 'L: ', L, 'X: ',X) + print('DG iteration passing.') + if count > 200: + fail_gen = True + status = True + else: + count += 1 else: - count += 1 - else: - status = True - fail_gen = False # Worked! - if fail_gen: - if debug: - print('Trying more Dist Geom Loosened Ca-M-Ca constraints') - LB, UB = get_bounds_matrix(allcoords, graph, natoms, - catoms, shape, - ml_dists, vdwrad, anums, bond_tol=0.3,angle_tol=0.2, + status = True + fail_gen = False # Worked! + if fail_gen: + if debug: + print('Trying more Dist Geom Loosened Ca-M-Ca constraints') + LB, UB = get_bounds_matrix( + allcoords, graph, natoms, + catoms, shape, + ml_dists, vdwrad, anums, bond_tol=0.3, angle_tol=0.2, metal_center_lb_multiplier=0.8, ca_angle_tol=0.2, h_bond_tol=0.1, h_angle_tol=0.2, isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, add_angle_constraints=add_angle_constraints) - status = False - count = 0 - while not status: - try: - tLB = LB.copy() - tUB = UB.copy() - D = metrize(tLB, tUB, natoms, non_triangle=non_triangle, debug=debug) - D0 = get_cm_dists(D, natoms) - G = get_metric_matrix(D, D0, natoms) - L, V = get_3_eigs(G, natoms) - X = np.dot(V, L) # get projection - except: - if debug > 1: - print('Gen Exception.') - X = np.array([np.nan]) - - if np.any(np.isnan(X)): # If any are not nan continue! - if debug > 1: - # print('V: ',V, 'L: ', L, 'X: ',X) - print('DG iteration passing.') - if count > 200: - fail_gen = True - status = True + status = False + count = 0 + while not status: + try: + tLB = LB.copy() + tUB = UB.copy() + D = metrize(tLB, tUB, natoms, + non_triangle=non_triangle, debug=debug) + D0 = get_cm_dists(D, natoms) + G = get_metric_matrix(D, D0, natoms) + L, V = get_3_eigs(G, natoms) + X = np.dot(V, L) # get projection + except: + if debug > 1: + print('Gen Exception.') + X = np.array([np.nan]) + + if np.any(np.isnan(X)): # If any are not nan continue! + if debug > 1: + # print('V: ',V, 'L: ', L, 'X: ',X) + print('DG iteration passing.') + if count > 200: + fail_gen = True + status = True + else: + count += 1 else: - count += 1 - else: - status = True - fail_gen = False # Worked! - if fail_gen: - if debug > 1: - print('Trying more Dist Geom with Dirty LB') - LB, UB = get_bounds_matrix(allcoords, graph, natoms, - catoms, shape, - ml_dists, vdwrad, anums, bond_tol=0.3,angle_tol=0.3, - metal_center_lb_multiplier=0.6, ca_angle_tol=0.3, - h_bond_tol=0.1, h_angle_tol=0.3, - isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, - add_angle_constraints=add_angle_constraints) - status = False - count = 0 - while not status: - try: - tLB = LB.copy() - tUB = UB.copy() - D = metrize(tLB, tUB, natoms, non_triangle=non_triangle, debug=debug) - D0 = get_cm_dists(D, natoms) - G = get_metric_matrix(D, D0, natoms) - L, V = get_3_eigs(G, natoms) - X = np.dot(V, L) # get projection - except: - if debug: - print('Gen Exception.') - X = np.array([np.nan]) - - if np.any(np.isnan(X)): # If any are not nan continue! - if debug > 1: - # print('V: ',V, 'L: ', L, 'X: ',X) - print('DG iteration passing.') - if count > 200: - fail_gen = True status = True + fail_gen = False # Worked! + if fail_gen: + if debug > 1: + print('Trying more Dist Geom with Dirty LB') + LB, UB = get_bounds_matrix( + allcoords, graph, natoms, + catoms, shape, + ml_dists, vdwrad, anums, bond_tol=0.3, angle_tol=0.3, + metal_center_lb_multiplier=0.6, ca_angle_tol=0.3, + h_bond_tol=0.1, h_angle_tol=0.3, + isCp=isCp, cp_catoms=cp_catoms, edge_ligand=edge_ligand, + add_angle_constraints=add_angle_constraints) + status = False + count = 0 + while not status: + try: + tLB = LB.copy() + tUB = UB.copy() + D = metrize(tLB, tUB, natoms, non_triangle=non_triangle, debug=debug) + D0 = get_cm_dists(D, natoms) + G = get_metric_matrix(D, D0, natoms) + L, V = get_3_eigs(G, natoms) + X = np.dot(V, L) # get projection + except: + if debug: + print('Gen Exception.') + X = np.array([np.nan]) + + if np.any(np.isnan(X)): # If any are not nan continue! + if debug > 1: + # print('V: ',V, 'L: ', L, 'X: ',X) + print('DG iteration passing.') + if count > 200: + fail_gen = True + status = True + else: + count += 1 else: - count += 1 + status = True + fail_gen = False # Worked! + + ############# END Distance Geometry Section ############## + ############# Start FF Relaxation Section ############## + + if not fail_gen: + x = np.reshape(X, 3*natoms) + res1 = optimize.fmin_cg(distance_error, x, fprime=dist_error_gradient, + gtol=0.1, args=(LB, UB, natoms), disp=0) + if np.any(np.isnan(res1)): + if debug > 1: + print('Dist Geom produced Nan. - Skipping!') + Conf3D_out = io_obabel.convert_obmol_ase(Conf3D, add_hydrogens=add_hydrogens) + final_relax = False + fail = True + leave_loop = True + tligcoordList = ligcoordList.copy() else: - status = True - fail_gen = False # Worked! - - ############# END Distance Geometry Section ############## - ############# Start FF Relaxation Section ############## - - if not fail_gen: - x = np.reshape(X, 3*natoms) - res1 = optimize.fmin_cg(distance_error, x, fprime=dist_error_gradient, - gtol=0.1, args=(LB, UB, natoms), disp=0) - if np.any(np.isnan(res1)): + X = np.reshape(res1, (natoms, 3)) + Conf3D_out, fail, final_relax, tligcoordList, symmetry_match = \ + clean_conformation_ff( + X, + Conf3D, catoms, shape, + graph, anums, ligcoordList, + ligsmiles=ligsmiles, # + original_metal=metal, + add_angle_constraints=add_angle_constraints, + ca_metal_dist_constraints=ca_metal_dist_constraints, + isCp=isCp, + edge_ligand=edge_ligand, + cp_catoms=cp_catoms, + skip_mff=skip_mmff, + add_hydrogens=add_hydrogens, + enforce_symmetry=enforce_symmetry, # + debug=debug + ) + + # Set charges from total charge from OBmol + Conf3D_out.set_initial_charges(init_charges_lig) + if (((enforce_symmetry) and (symmetry_match)) or (not enforce_symmetry)): + leave_loop = True + else: if debug > 1: - print('Dist Geom produced Nan. - Skipping!') - Conf3D_out = io_obabel.convert_obmol_ase(Conf3D, add_hydrogens=add_hydrogens) - final_relax = False + print('Dist Geom Failed entirely!') + Conf3D_out = io_obabel.convert_obmol_ase(Conf3D, + add_hydrogens=add_hydrogens) + final_relax = True fail = True + leave_loop = True tligcoordList = ligcoordList.copy() - else: - X = np.reshape(res1, (natoms, 3)) - if no_ff: - Conf3D_out, fail, final_relax, tligcoordList = nonclean_conformation_ff(X, - Conf3D, catoms, shape, - graph, anums, ligcoordList, - add_angle_constraints=add_angle_constraints, - isCp=isCp, - cp_catoms=cp_catoms, - skip_mff=skip_mmff,add_hydrogens=add_hydrogens - ) - else: - Conf3D_out, fail, final_relax, tligcoordList = clean_conformation_ff(X, - Conf3D, catoms, shape, - graph, anums, ligcoordList, - original_metal=metal, - add_angle_constraints=add_angle_constraints, - ca_metal_dist_constraints=ca_metal_dist_constraints, - isCp=isCp, - edge_ligand=edge_ligand, - cp_catoms=cp_catoms, - skip_mff=skip_mmff,add_hydrogens=add_hydrogens, - debug=debug - ) - - # Set charges from total charge from OBmol - Conf3D_out.set_initial_charges(init_charges_lig) - else: - if debug >1: - print('Dist Geom Failed entirely!') - Conf3D_out = io_obabel.convert_obmol_ase(Conf3D,add_hydrogens=add_hydrogens) - final_relax = True - fail = True - tligcoordList = ligcoordList.copy() ############# END FF Relaxation Section ############## - if (not fail): # Catch FF optimization failures - (outatoms, minval, sane) = set_position_align(Conf3D_out, - tligcoordList, - corecoordList, - isCp=isCp, - debug=debug, - rot_coord_vect=rot_coord_vect, - rot_angle=rot_angle - ) + if (not fail): # Catch FF optimization failures + (outatoms, minval, sane) = set_position_align( + Conf3D_out, + tligcoordList, + corecoordList, + isCp=isCp, + debug=debug, + rot_coord_vect=rot_coord_vect, + rot_angle=rot_angle + ) crowding_penalty = 0 - if sane: # Perform check to see if metal is extra crowded. + if sane: # Perform check to see if metal is extra crowded. cov1metal = cov1rad[-1] if add_hydrogens: LB_dists = np.array([io_ptable.rcov1[x] for x in outatoms.get_atomic_numbers()]) + cov1metal else: LB_dists = np.array(cov1rad[0:-1]) + cov1metal coords = outatoms.positions - dists = np.linalg.norm(coords,axis=1) + dists = np.linalg.norm(coords, axis=1) max_catom_dist = np.max(dists[catoms]) if np.any(dists < LB_dists*0.5): sane = False # Add penalty against crowded conformations. crowding_penalty += len(np.where(dists < LB_dists*0.8)[0]) # Super penalize conformers with any atoms closer than the max connecting atom distance. - if np.any(np.delete(dists,catoms) < max_catom_dist): + if np.any(np.delete(dists, catoms) < max_catom_dist): crowding_penalty += 100 ### Potentially add penalty for crowding other binding sites. - else: # Save conformation - but set it's rotational error high. + else: # Save conformation - but set it's rotational error high. (outatoms, minval, sane) = Conf3D_out, 10000, False crowding_penalty = 1000 if debug: @@ -1778,13 +1766,24 @@ def get_aligned_conformer(ligsmiles, ligcoordList, corecoordList, metal='Fe', minval += crowding_penalty return outatoms, minval, sane, final_relax, bo_dict, atypes, tligcoordList -def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', - nconformers=3, ligtype=None, ca_metal_dist_constraints=None, - skip_mmff=False, covrad_metal=None, vdwrad_metal=None, - no_ff=False, debug=False - ): - """find_conformers - Take in ligsmiles and generate N different conformers + +def find_conformers( + ligsmiles, + ligcoordList, + corecoordList, + metal='Fe', + nconformers=3, + ligtype=None, + ca_metal_dist_constraints=None, + skip_mmff=False, + covrad_metal=None, + vdwrad_metal=None, + no_ff=False, + debug=False, + enforce_symmetry=False, + ): + """find_conformers + Take in ligsmiles and generate N different conformers Parameters ---------- @@ -1830,21 +1829,22 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', val_list = [] tligcoordList_out = [] rot_list = [] - + if debug: seeds = [np.random.randint(1,100) for x in range(nconformers)] print('Init seeds (pre-ligand generation): ', seeds) OBmol_lig = io_obabel.get_obmol_smiles(ligsmiles) if (OBmol_lig.NumAtoms() < 4): # Limit smaller molecules conformers (not useful) - conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff, - ligtype=ligtype, - debug=debug) + conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, corecoordList, + metal=metal, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=skip_mmff, covrad_metal=covrad_metal, + vdwrad_metal=vdwrad_metal, no_ff=no_ff, + ligtype=ligtype, enforce_symmetry=enforce_symmetry, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1856,13 +1856,17 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', elif ('cis' in ligtype) or ('mer' in ligtype) or ('fac' in ligtype): if debug: print('Generating cis/mer/facs') - conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=skip_mmff,covrad_metal=covrad_metal, - ligtype=ligtype, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=skip_mmff, covrad_metal=covrad_metal, + ligtype=ligtype, + vdwrad_metal=vdwrad_metal, no_ff=no_ff, + enforce_symmetry=enforce_symmetry, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1873,13 +1877,17 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', print('Failed sanity checks after rotation!') if debug: print('Generating cis/mer/facs.') - conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=True, covrad_metal=covrad_metal, - ligtype=ligtype, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=True, covrad_metal=covrad_metal, + ligtype=ligtype, + vdwrad_metal=vdwrad_metal, + enforce_symmetry=enforce_symmetry, + no_ff=no_ff, debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1888,16 +1896,20 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', else: if debug: print('Failed sanity checks after rotation!') - + # Limit conformers for bidentate cis/tri_mer/tri_fac -> generate rotated/flipped versions!!! # for i in range(1): - conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ca_metal_dist_constraints=ca_metal_dist_constraints, - ligtype=ligtype, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ca_metal_dist_constraints=ca_metal_dist_constraints, + ligtype=ligtype, + skip_mmff=skip_mmff, covrad_metal=covrad_metal, + enforce_symmetry=enforce_symmetry, + vdwrad_metal=vdwrad_metal, no_ff=no_ff, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1906,32 +1918,41 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', else: if debug: print('Failed sanity checks after rotation!') - + # Add N+1 without the explicit angle constraints - conf, val, sane, final_relax, _ , _, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ligtype=ligtype, - ca_metal_dist_constraints=ca_metal_dist_constraints, - add_angle_constraints=False, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, _, _, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ligtype=ligtype, + ca_metal_dist_constraints=ca_metal_dist_constraints, + add_angle_constraints=False, + skip_mmff=skip_mmff, covrad_metal=covrad_metal, + enforce_symmetry=enforce_symmetry, + vdwrad_metal=vdwrad_metal, no_ff=no_ff, debug=debug) if sane: conf_list.append(conf) val_list.append(val) tligcoordList_out.append(tligcoordList) rot_list.append(0) - + # Add N+2 without the triangle angle constraint for metal (encouraging different conformers for multidentate) - conf, val, sane, final_relax, _, _, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ligtype=ligtype, - add_angle_constraints=True, - ca_metal_dist_constraints=ca_metal_dist_constraints, - non_triangle=True, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, _, _, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ligtype=ligtype, + add_angle_constraints=True, + ca_metal_dist_constraints=ca_metal_dist_constraints, + non_triangle=True, + skip_mmff=skip_mmff, + covrad_metal=covrad_metal, + enforce_symmetry=enforce_symmetry, + vdwrad_metal=vdwrad_metal, + no_ff=no_ff, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1939,15 +1960,19 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', rot_list.append(0) # Add N+2 without the triangle angle constraint for metal (encouraging different conformers for multidentate) - conf, val, sane, final_relax, _, _, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ligtype=ligtype, - add_angle_constraints=False, - ca_metal_dist_constraints=ca_metal_dist_constraints, - non_triangle=True, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, _, _, tligcoordList = \ + get_aligned_conformer( + ligsmiles, + ligcoordList, + corecoordList, + metal=metal, + ligtype=ligtype, + add_angle_constraints=False, + ca_metal_dist_constraints=ca_metal_dist_constraints, + non_triangle=True, + skip_mmff=skip_mmff, covrad_metal=covrad_metal, + enforce_symmetry=enforce_symmetry, + vdwrad_metal=vdwrad_metal, no_ff=no_ff, debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1955,13 +1980,20 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', rot_list.append(0) else: for i in range(nconformers): - conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ligtype=ligtype, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = \ + get_aligned_conformer( + ligsmiles, + ligcoordList, + corecoordList, + metal=metal, + ligtype=ligtype, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=skip_mmff, + covrad_metal=covrad_metal, + vdwrad_metal=vdwrad_metal, + no_ff=no_ff, + enforce_symmetry=enforce_symmetry, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1972,14 +2004,17 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', print('Failed sanity checks after rotation!') # Add N+1 without the explicit angle constraints - conf, val, sane, final_relax, _ , _, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ligtype=ligtype, - add_angle_constraints=False, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, _ , _, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ligtype=ligtype, + add_angle_constraints=False, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=skip_mmff, covrad_metal=covrad_metal, + enforce_symmetry=enforce_symmetry, + vdwrad_metal=vdwrad_metal, no_ff=no_ff, debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -1987,13 +2022,19 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', rot_list.append(0) # print('Add conformer with no MMFF relaxation') - conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ligtype=ligtype, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=True, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, bo_dict, atypes, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ligtype=ligtype, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=True, + covrad_metal=covrad_metal, + vdwrad_metal=vdwrad_metal, + enforce_symmetry=enforce_symmetry, + no_ff=no_ff, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) @@ -2002,34 +2043,44 @@ def find_conformers(ligsmiles, ligcoordList, corecoordList, metal='Fe', else: if debug: print('Failed sanity checks after rotation!') - + # Add N+2 with the triangle angle constraint for metal (encouraging different conformers for multidentate) - conf, val, sane, final_relax, _, _, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - ligtype=ligtype, - add_angle_constraints=True, - non_triangle=True, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=skip_mmff, covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, _, _, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + ligtype=ligtype, + add_angle_constraints=True, + non_triangle=True, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=skip_mmff, covrad_metal=covrad_metal, + enforce_symmetry=enforce_symmetry, + vdwrad_metal=vdwrad_metal, + no_ff=no_ff, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) tligcoordList_out.append(tligcoordList) rot_list.append(0) - + # Add N+2 without the triangle angle constraint for metal (encouraging different conformers for multidentate) - conf, val, sane, final_relax, _, _, tligcoordList = get_aligned_conformer(ligsmiles, ligcoordList, - corecoordList, - metal=metal, - add_angle_constraints=False, - non_triangle=True, - ligtype=ligtype, - ca_metal_dist_constraints=ca_metal_dist_constraints, - skip_mmff=skip_mmff, - covrad_metal=covrad_metal, - vdwrad_metal=vdwrad_metal,no_ff=no_ff,debug=debug) + conf, val, sane, final_relax, _, _, tligcoordList = \ + get_aligned_conformer( + ligsmiles, ligcoordList, + corecoordList, + metal=metal, + add_angle_constraints=False, + non_triangle=True, + ligtype=ligtype, + ca_metal_dist_constraints=ca_metal_dist_constraints, + skip_mmff=skip_mmff, + covrad_metal=covrad_metal, + enforce_symmetry=enforce_symmetry, + vdwrad_metal=vdwrad_metal, + no_ff=no_ff, + debug=debug) if sane: conf_list.append(conf) val_list.append(val) diff --git a/architector/io_obabel.py b/architector/io_obabel.py index d5c1e7b..9a4d386 100644 --- a/architector/io_obabel.py +++ b/architector/io_obabel.py @@ -6,6 +6,7 @@ # Imports import ase +import itertools from ase.io import read from ase import units import numpy as np @@ -1002,7 +1003,7 @@ def obmol_lig_split(mol2string, calc_coord_atoms=True, allow_radicals=False, calc_all=False): - """obmol_lig_split + """obmol_lig_split Take in a mol2string and use openbabel to split into ligands, convert to smiles, and calculate metal-ligand coordinating atoms implicit in the mol2string. @@ -1249,6 +1250,86 @@ def obmol_lig_split(mol2string, return ligand_smiles, coord_atom_lists, info_dict +def obmol_lig_compare(mol2string, + smiles): + """obmol_lig_compare + Take in a mol2string and smiles string - use openbabel to + calculate metal-ligand coordinating atoms + implicit in the mol2string as related to the smiles string. + + Parameters + ---------- + mol2string : str + mol2string of ligand-metal complex + smiles : str + smiles string of the ligand + + Returns + ------- + coord_atom_list : list(int) + list of coordinating atom indices for the smiles str + """ + if 'un' in mol2string: + mol2string = mol2string.replace('un', '0') + obmol = convert_mol2_obmol(mol2string) + obmol.ConvertZeroBonds() + else: + obmol = convert_mol2_obmol(mol2string) + _, anums, graph = get_OBMol_coords_anums_graph(obmol) + bo_dict, _ = get_OBMol_bo_dict_atom_types(obmol, metal_passed=False) + met_inds = [i for i, x in enumerate(anums) if ( + io_ptable.elements[x] in io_ptable.all_metals)] + shape = graph.shape + only_mets_graph = np.zeros(shape) + init_graph = graph.copy() + # Create 2 graphs - one with metals removed, one with only the metals graphs + for ind in sorted(met_inds): + only_mets_graph[:, ind] = graph[ind] + only_mets_graph[ind, :] = graph[ind] + # Zero out the deleted inds + init_graph[ind, :] = np.zeros(shape[0]) + init_graph[:, ind] = np.zeros(shape[0]) + csg = csgraph.csgraph_from_dense(init_graph) + # Break apart zeroed graph into connected components + disjoint_components = csgraph.connected_components(csg)[1] + ligs_inds = [] + for ind in sorted(list(set(disjoint_components))): # sort for reproducability + subgraph = np.where(disjoint_components == ind)[0] + sg = np.array([x for x in subgraph if x not in met_inds]) # Check not deleted atoms + sg.sort() + if len(sg) > 0: + ligs_inds.append(sg) + lig = ligs_inds[0].tolist() + ligobmol = ob.OBMol() + coord_atom_list = [] + for i, atom_ind in enumerate(lig): + atom_ind += 1 + for j, atom in enumerate(ob.OBMolAtomIter(obmol)): + if (j+1 == atom_ind): + ligobmol.AddAtom(atom) + for k in bo_dict.keys(): + if (atom_ind in k): + other_ind = [x for x in k if x != atom_ind][0] - 1 + if other_ind in met_inds: + coord_atom_list.append(i) + for k in bo_dict.keys(): + if (k[0]-1 in lig) and (k[1]-1 in lig): + start_ind = lig.index(k[0]-1) + 1 + end_ind = lig.index(k[1]-1) + 1 + ligobmol.AddBond(start_ind, end_ind, bo_dict[k]) + tmp1 = get_obmol_smiles(smiles, build=False) + if tmp1.NumAtoms() == ligobmol.NumAtoms(): + can1 = get_canonical_label(tmp1) + can2 = get_canonical_label(ligobmol) # Know indices + smicat = [] + for atom in coord_atom_list: # Match canonicalized graph indices + a_ind = can2.index(atom) + smicat.append(can1[a_ind]) + else: + smicat = None + return smicat + + def get_canonical_label(obmol): """Create a canonical label for the molecule using pynauty. Warning - can fail/hang on particularly gnarly graphs. @@ -1286,11 +1367,11 @@ def map_coord_ats_smiles(lig_smiles, lig_obmol, coord_atoms, return_all=False): to the pynauty canonicalized form of the molecular graphs (colored by atom type) to map the 3d structure graph to the smiles graph. """ - tmp1 = get_obmol_smiles(lig_smiles) - can1 = get_canonical_label(tmp1) - can2 = get_canonical_label(lig_obmol) # Know indices + tmp1 = get_obmol_smiles(lig_smiles, build=False) + can1 = get_canonical_label(tmp1) + can2 = get_canonical_label(lig_obmol) # Know indices smicat = [] - for atom in coord_atoms: # Match canonicalized graph indices + for atom in coord_atoms: # Match canonicalized graph indices a_ind = can2.index(atom) smicat.append(can1[a_ind]) if return_all: @@ -1323,6 +1404,102 @@ def get_fingerprint(obmol, fp='FP2'): fp = pybelmol.calcfp(fp) return fp + +def min_circular(intuple): + """take in a tuple and re-order such that the lowest + value in the tuple is first (preserving circular relative ordering) + + Parameters + ---------- + intuple : tuple + tuple with any number of values + + Returns + ------- + out : tuple + tuple re-ordered with lowest element first (preserving circular ordering) + """ + tlist = list(intuple) + minind = tlist.index(min(intuple)) + repeated = tlist + tlist + out = repeated[minind:minind + len(tlist)] + return tuple(out) + + +def get_stereo_label(obmol, tetrahedral=True, ct=False): + """get_stereo_label + gives a unique label for the stereochemistry for a given molecule + + Openbabel only handles tetrahedral or cis/trans stereochemistry, + So higher order stereochemistries aren't necessarily preserved! + + Parameters + ---------- + obmol : ob.OBmol + Molecule in openbabel representation + + Returns + ------- + out : str + Unique label containing the stereochemistry as assigned/detected by openbabel. + """ + if not obmol.HasChiralityPerceived(): + ob.PerceiveStereo(obmol) + facade = ob.OBStereoFacade(obmol) + out = '' + if tetrahedral: + for atom in ob.OBMolAtomIter(obmol): + mid = atom.GetId() + if facade.HasTetrahedralStereo(mid): + tetra = facade.GetTetrahedralStereo(mid) + config = tetra.GetConfig() + if tetra.IsSpecified(): + out += 'tet:' + str(mid) + \ + str(config.from_or_towards) + \ + str(min_circular(config.refs)) + if ct: + for bond in ob.OBMolBondIter(obmol): + mid = bond.GetId() + if facade.HasCisTransStereo(mid): + cistrans = facade.GetCisTransStereo(mid) + config = cistrans.GetConfig() + if config.specified: + out += 'ct:' + str(mid) + \ + str(sorted([config.begin, config.end])) + \ + str(min_circular(config.refs)) + return out + +def get_stereo_fix_bond_dihedrals(obmol): + """get_stereo_fix_bond_dihedrals + gives a unique label for the stereochemistry for a given molecule + + Openbabel only handles tetrahedral or cis/trans stereochemistry, + So higher order stereochemistries aren't necessarily preserved! + + Parameters + ---------- + obmol : ob.OBmol + Molecule in openbabel representation + + Returns + ------- + out : str + Unique label containing the stereochemistry as assigned/detected by openbabel. + """ + if not obmol.HasChiralityPerceived(): + ob.PerceiveStereo(obmol) + facade = ob.OBStereoFacade(obmol) + bond_fixes = dict() + for bond in ob.OBMolBondIter(obmol): + mid = bond.GetId() + if facade.HasCisTransStereo(mid): + cistrans = facade.GetCisTransStereo(mid) + config = cistrans.GetConfig() + if config.specified: + bond_fixes[(config.begin, config.end)] = config.refs + return bond_fixes + + # Main if (__name__ == '__main__'): # Variables diff --git a/architector/io_process_input.py b/architector/io_process_input.py index 06d3c37..27fa439 100644 --- a/architector/io_process_input.py +++ b/architector/io_process_input.py @@ -105,8 +105,13 @@ def assign_ligType_default(core_geo_class, ligsmiles, ligcoords, metal, tolerance=20) lig_denticity = len(ligcoords) possible_Ligtypes = tcore_geo_class.cn_ligType_dict[lig_denticity] - exitloop=False + exitloop = False + maxcount = 20 + count = 0 while not exitloop: + if count == maxcount: + raise ValueError('No Possible ligand type for this ligand.') + count += 1 rot_vals = [] possible_saves = [] conformers = [] @@ -919,6 +924,7 @@ def inparse(inputDict): "force_trans_oxos": True, # Force trans configurations for oxos (Useful for actinyls) "override_oxo_opt": False, # Override no relaxation of oxo groups (not generally suggested) "lig_assignment": 'default', # or "similarity" How to automatically assign ligand types. + 'enforce_ligand_internal_symmetry': False, # Whether to enforce the ligand's internal symmetry during generation. # Cutoff parameters "assemble_sanity_checks":True, # Turn on/off assembly sanity checks. @@ -943,7 +949,7 @@ def inparse(inputDict): "metal_spin": None, # Spin State "test_alternate_metal_spin": False, # Test a second spin state for the metal? "alternate_metal_spin": None, # Secondary spin state to check. - + # Method parameters. "calculator":None, # ASE calculator class input for usage during construction or for optimization. "calculator_kwargs":dict(), # ASE calculator kwargs. @@ -970,7 +976,7 @@ def inparse(inputDict): # For very large speedup - use GFN-FF, though this is much less stable (especially for Lanthanides) # Or UFF "skip_duplicate_tests":False, # Skip the duplicate tests (return all generated/relaxed configurations) - + # Covalent radii and vdw radii of the metal if deviations requested. "vdwrad_metal":vdwrad_metal, "covrad_metal":covrad_metal, diff --git a/development/16-setereochemistry_control.ipynb b/development/16-setereochemistry_control.ipynb new file mode 100644 index 0000000..fb2cfe1 --- /dev/null +++ b/development/16-setereochemistry_control.ipynb @@ -0,0 +1,343 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from architector import convert_io_molecule, view_structures, build_complex" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "application/3dmoljs_load.v0": "
\n

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n
\n", + "text/html": [ + "
\n", + "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", + "
\n", + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "view_structures(\"[O-][C@H](Cl)C[C@@H](Br)[O-]\",labelinds=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "application/3dmoljs_load.v0": "
\n

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n
\n", + "text/html": [ + "
\n", + "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", + "
\n", + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ligand = {'smiles':\"[O-][C@H](Cl)C[C@@H](Br)[O-]\",\n", + " 'coordList':[0,6],\n", + " 'ligType':'bi_cis'}\n", + "out = build_complex({'core':{'metal':'Fe','coreCN':6},\n", + " 'ligands':[ligand]*1,\n", + " 'parameters':{'enforce_ligand_internal_symmetry':True,\n", + " 'return_only_1':True}})\n", + "view_structures(out)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "application/3dmoljs_load.v0": "
\n

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n
\n", + "text/html": [ + "
\n", + "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", + "
\n", + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "view_structures(\"[O-][C@H](Cl)C[C@H](Br)[O-]\",labelinds=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "application/3dmoljs_load.v0": "
\n

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n
\n", + "text/html": [ + "
\n", + "

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

\n", + "
\n", + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ligand = {'smiles':\"[O-][C@H](Cl)C[C@H](Br)[O-]\",\n", + " 'coordList':[0,6],\n", + " 'ligType':'bi_cis'}\n", + "out = build_complex({'core':{'metal':'Fe','coreCN':6},\n", + " 'ligands':[ligand]*1,\n", + " 'parameters':{'enforce_ligand_internal_symmetry':True,\n", + " 'return_only_1':True}})\n", + "view_structures(out)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}