def from_rdkit(cls, rd_atom: Chem.Atom) -> "Atom": """ Build a QUBEKit atom from an rdkit atom instance. """ atomic_number = rd_atom.GetAtomicNum() index = rd_atom.GetIdx() formal_charge = rd_atom.GetFormalCharge() aromatic = rd_atom.GetIsAromatic() bonds = [a.GetIdx() for a in rd_atom.GetNeighbors()] # check for names in the normal places pdb, mol2 and mol if rd_atom.HasProp("_Name"): name = rd_atom.GetProp("_Name") elif rd_atom.HasProp("_TriposAtomName"): name = rd_atom.GetProp("_TriposAtomName") else: try: name = rd_atom.GetMonomerInfo().GetName().strip() except AttributeError: name = None # stereochem if rd_atom.HasProp("_CIPCode"): stereo_code = rd_atom.GetProp("_CIPCode") else: stereo_code = None return cls( atomic_number=atomic_number, atom_index=index, atom_name=name, formal_charge=formal_charge, aromatic=aromatic, stereochemistry=stereo_code, bonds=bonds, )
def _is_count_valid(self, atom: Chem.Atom) -> bool: """ Some atoms are not to be counted as they will be deleted. :param atom: :return: """ if atom.HasProp('DELETE'): return False elif atom.HasProp('_ori_i') and atom.GetIntProp('_ori_i') == -1: return False else: return True
def _get_xyz(cls, atom: Chem.Atom) -> Tuple[float]: if atom.HasProp('_x'): return (atom.GetDoubleProp('_x'), atom.GetDoubleProp('_y'), atom.GetDoubleProp('_z')) else: return ()
def charge_gasteiger_h(atom: Atom) -> float: """Gasteiger partial charge for implicit hydrogens (float). """ if not atom.HasProp('_GasteigerHCharge'): mol = atom.GetOwningMol() AllChem.ComputeGasteigerCharges(mol) return atom.GetDoubleProp('_GasteigerHCharge')
def is_hbond_donor(atom: Atom) -> int: """If the atom is a hydrogen bond donor (0 or 1). """ if not atom.HasProp('_Feature_Donor'): mol = atom.GetOwningMol() _ChemicalFeatureGenerator().assign_features(mol) return atom.GetIntProp('_Feature_Donor')
def _get_origin(cls, atom: Chem.Atom) -> List[str]: if atom.HasProp('_Origin'): o = atom.GetProp('_Origin') if o != 'none': return json.loads(o) else: return [] else: return []
def stereochemistry(atom: Atom) -> str: """CIP sterochemistry label (string). """ mol = atom.GetOwningMol() if not mol.HasProp('_CIPLabelsAssigned'): rdCIPLabeler.AssignCIPLabels(mol) mol.SetProp('_CIPLabelsAssigned', '1') return atom.GetProp('_CIPCode') if atom.HasProp('_CIPCode') else ''
def assess_atom(atom: Chem.Atom, bt: Chem.BondType) -> Tuple[bool, Chem.BondType]: """ True means add, False means delete :param atom: :param bt: :return: """ n_neigh = sum([self._is_count_valid(neigh) for neigh in atom.GetNeighbors()]) if atom.GetAtomicNum() > 8: return True, bt elif atom.HasProp('DELETE'): # if it is to be deleted it should be fine. return True, bt elif n_neigh <= 2 and atom.GetIsAromatic(): return True, Chem.BondType.SINGLE elif n_neigh <= 3 and not atom.GetIsAromatic(): return True, bt else: return False, bt # too bonded already!
def _assess_atom_for_possible_bonding(self, atom: Chem.Atom, bt: Chem.BondType) -> bool: """ Method for add_bond_if_possible True means add, False means delete :param atom: :param bt: :return: """ n_neigh = sum( [self._is_count_valid(neigh) for neigh in atom.GetNeighbors()]) if atom.GetAtomicNum() > 8: return True elif atom.HasProp( 'DELETE'): # if it is to be deleted it should be fine. return True elif n_neigh <= 2 and atom.GetIsAromatic(): return True, Chem.BondType.SINGLE elif n_neigh <= 3 and not atom.GetIsAromatic(): return True else: return False # too bonded already!
def _add_bond_if_possible(self, mol, first: Chem.Atom, second: Chem.Atom, provenance='other_novel'): """ This method is used by _copy_bonding, but triggered when force=False :param mol: :param first: :param second: :param provenance: :return: """ # --- Prep first_idx = first.GetIdx() second_idx = second.GetIdx() present_bond = mol.GetBondBetweenAtoms(first_idx, second_idx) if second.HasProp('_ring_bond'): bt = getattr(Chem.BondType, second.GetProp('_ring_bond')) else: bt = None # === Bond already exists series ============================================ # --- Bond already exists and not bond type is specified if present_bond is not None and bt is None: self.journal.debug( f'Bond between {first_idx} and {second_idx} already exists') return True # exists # --- Bond already exists but has an error elif present_bond is not None and present_bond.GetBondType() is None: present_bond.SetBondType(Chem.BondType.SINGLE) return True # --- Bond already exists and matches the expected bond type elif present_bond is not None and bt.name == present_bond.GetBondType( ).name: return True # --- Bond already exists but does not match the expected bond type elif present_bond is not None: present_bond.SetBondType(bt) return True # === Assess if new bond should be added ============================================ # --- Don't add if it would make a triangle elif self._is_would_be_triangle(first, second): self.journal.debug( f'Bond between {first_idx} and {second_idx} would make a triangle, skipping' ) return False # --- Don't add if it would make a square elif self._is_would_be_square(first, second): self.journal.debug( f'Bond between {first_idx} and {second_idx} would make a square, skipping' ) return False # --- Don't add if it would ruin the warhead elif self._is_connected_warhead(second, first): self.journal.debug( f'Bond between {first_idx} and {second_idx} would break a warhead, skipping' ) return False # --- New bond is green lit elif self._assess_atom_for_possible_bonding( first, bt) and self._assess_atom_for_possible_bonding( second, bt): mol.AddBond(first_idx, second_idx, bt if bt is not None else Chem.BondType.SINGLE) new_bond = mol.GetBondBetweenAtoms(first_idx, second_idx) BondProvenance.set_bond(new_bond, provenance) return True # --- New bond is no go else: # len(Chem.GetMolFrags(mol, sanitizeFrags=False)) ought to be checked. # however, join_neighboring gets called by emergency bonding so should be fine. return False # too bonded etc.