def apply(self, mol: RWMol) -> RWMol: atom1 = get_atom_ind(mol, self.atom_map1) atom2 = get_atom_ind(mol, self.atom_map2) if self.bond_type is None: # delete bond bond = mol.GetBondBetweenAtoms(atom1, atom2) if bond is not None: mol.RemoveBond(atom1, atom2) else: b_type = rdchem.BondType.values[self.bond_type] b_stereo = rdchem.BondStereo.values[self.bond_stereo] bond = mol.GetBondBetweenAtoms(atom1, atom2) if bond is None: # add new bond bond_ind = mol.AddBond(atom1, atom2, order=b_type) - 1 bond = mol.GetBondWithIdx(bond_ind) else: # change an existing bond bond.SetBondType(b_type) bond.SetStereo(b_stereo) bond.SetBoolProp('is_edited', True) if b_type == BondType.AROMATIC: bond.SetIsAromatic(True) mol.GetAtomWithIdx(atom1).SetIsAromatic(True) mol.GetAtomWithIdx(atom2).SetIsAromatic(True) return mol
def _prevent_bridge_ring(self, mol: Chem.RWMol, examplar: Tuple[int]): ## This is really # examplar is ring ringatoms = self._get_ring_info(mol) #GetRingInfo().AtomRings() ringatoms = [ring for ring in ringatoms if set(ring).intersection(examplar)] ring_idx = list(range(len(ringatoms))) shared_count = {} for ra, rb in itertools.combinations(ring_idx, r=2): shared_count[(ra, rb)] = len(set(ringatoms[ra]).intersection(set(ringatoms[rb]))) if len(shared_count) == 0: return mol ra, rb = list(shared_count.keys())[0] shared = list(set(ringatoms[ra]).intersection(ringatoms[rb])) pairs = [(a, b) for a, b in itertools.combinations(shared, r=2) if mol.GetBondBetweenAtoms(a, b) is not None] c = Counter([i for pair in pairs for i in pair]) ring_A, ring_B = ringatoms[ra], ringatoms[rb] small, big = sorted([ring_A, ring_B], key=lambda ring: len(ring)) inners = [i for i in c if c[i] > 1] x = list(set(shared).difference(inners)) if len(x) != 2: log.critical(f'This is impossible. {ringatoms} share {shared} with {inners} in the inside and {x} on the edge?') return mol a, b = x if len(big) > 6: log.warning(f'Removing {len(inners)} bridging atoms and replacing with fused ring') # bond the vertices bt = Chem.BondType.SINGLE # ??? if mol.GetBondBetweenAtoms(a, b) is None: mol.AddBond(a, b, bt) else: log.warning('This is really odd! Why is there a bond already??') # remove the middle atoms. for i in sorted(inners, reverse=True): mol.RemoveAtom(i) else: log.warning(f'Shriking the smaller ring to change from bridged to fused.') # get the neighbour in the small atom to a vertex. neighs = [neigh for neigh in mol.GetAtomWithIdx(a).GetNeighbors() if neigh.GetIdx() not in shared and neigh.GetIdx() in small] neigh = sorted(neighs, key=lambda atom: atom.GetSymbol() != 'C')[0] bt = mol.GetBondBetweenAtoms(a, neigh.GetIdx()).GetBondType() mol.RemoveBond(a, neigh.GetIdx()) new_neigh = [neigh for neigh in mol.GetAtomWithIdx(a).GetNeighbors() if neigh.GetIdx() in shared][0] mol.AddBond(neigh.GetIdx(), new_neigh.GetIdx(), bt) neigh.SetBoolProp('_Novel', True) new_neigh.SetBoolProp('_Novel', True) mol.GetAtomWithIdx(a).SetBoolProp('_Novel', True) return mol
def _place_between(self, mol: Chem.RWMol, a: int, b: int, aromatic=True): oribond = mol.GetBondBetweenAtoms(a, b) if oribond is None: print('FAIL') return None # fail elif aromatic: bt = Chem.BondType.AROMATIC else: bt = oribond.GetBondType() idx = mol.AddAtom(Chem.Atom(6)) neoatom = mol.GetAtomWithIdx(idx) atom_a = mol.GetAtomWithIdx(a) atom_b = mol.GetAtomWithIdx(b) if aromatic: neoatom.SetIsAromatic(True) atom_a.SetIsAromatic(True) atom_b.SetIsAromatic(True) # prevent constraints neoatom.SetBoolProp('_Novel', True) atom_a.SetBoolProp('_Novel', True) atom_b.SetBoolProp('_Novel', True) # fix position conf = mol.GetConformer() pos_A = conf.GetAtomPosition(a) pos_B = conf.GetAtomPosition(b) x = pos_A.x / 2 + pos_B.x / 2 y = pos_A.y / 2 + pos_B.y / 2 z = pos_A.z / 2 + pos_B.z / 2 conf.SetAtomPosition(idx, Point3D(x, y, z)) # fix bonds mol.RemoveBond(a, b) mol.AddBond(a, idx, bt) mol.AddBond(b, idx, bt)
def to_rdkit_molecule(data: MoleculeContainer): """ MoleculeContainer to RDKit molecule object converter """ mol = RWMol() mapping = {} bonds = data._bonds for n, a in data.atoms(): ra = Atom(a.atomic_number) ra.SetAtomMapNum(n) if a.charge: ra.SetFormalCharge(a.charge) if a.isotope: ra.SetIsotope(a.isotope) if a.is_radical: ra.SetNumRadicalElectrons(1) mapping[n] = mol.AddAtom(ra) for n, m, b in data.bonds(): mol.AddBond(mapping[n], mapping[m], _bond_map[b.order]) for n in data._atoms_stereo: ra = mol.GetAtomWithIdx(mapping[n]) env = bonds[n] s = data._translate_tetrahedron_sign(n, [x for x in mapping if x in env]) ra.SetChiralTag(_chiral_ccw if s else _chiral_cw) for nm, s in data._cis_trans_stereo.items(): n, m = nm if m in bonds[n]: # cumulenes unsupported nn, nm, *_ = data._stereo_cis_trans[nm] b = mol.GetBondBetweenAtoms(mapping[n], mapping[m]) b.SetStereoAtoms(mapping[nn], mapping[nm]) b.SetStereo(_cis if s else _trans) conf = Conformer() for n, a in data.atoms(): conf.SetAtomPosition(mapping[n], (a.x, a.y, 0)) conf.Set3D(False) mol.AddConformer(conf, assignId=True) for c in data._conformers: conf = Conformer() for n, xyz in c.items(): conf.SetAtomPosition(mapping[n], xyz) mol.AddConformer(conf, assignId=True) SanitizeMol(mol) AssignStereochemistry(mol, flagPossibleStereoCenters=True, force=True) return mol
def _restore_original_bonding(self, mol: Chem.RWMol, rings) -> None: to_be_waited_for = [] for ring in rings: for i in range(len(ring['elements'])): d = self._get_expansion_for_atom(ring, i) new_i = self._get_new_index(mol, d['ori_i'], search_collapsed=False) for old_neigh, bond in zip(d['neighbor'], d['bond']): bt = getattr(Chem.BondType, bond) try: new_neigh = self._get_new_index(mol, old_neigh, search_collapsed=False) present_bond = mol.GetBondBetweenAtoms(new_i, new_neigh) if present_bond is None: mol.AddBond(new_i, new_neigh, bt) elif present_bond.GetBondType().name != bond: if self._debug_draw: print( f'bond between {new_i} {new_neigh} exists already (has {present_bond.GetBondType().name} expected {bt})') present_bond.SetBondType(bt) else: if self._debug_draw: print(f'bond between {new_i} {new_neigh} exists already ' + \ f'(has {present_bond.GetBondType().name} expected {bt})') pass except ValueError: if self._debug_draw: print(f"The neighbour {old_neigh} of {d['ori_i']} with {bt} does not yet exist") to_be_waited_for.append((new_i, old_neigh, bt)) for new_i, old_neigh, bt in to_be_waited_for: try: new_neigh = self._get_new_index(mol, old_neigh, name_restriction=mol.GetAtomWithIdx(new_i).GetProp('_ori_name')) if self._debug_draw: print(f'{old_neigh} was missing, but has appeared since as {new_neigh}') if not mol.GetBondBetweenAtoms(new_i, new_neigh): mol.AddBond(new_i, new_neigh, bt) except (KeyError, ValueError) as err: warn(str(err))
def _minimize_rings(mol): """Private: Minimize rings in a scaffold. In this process, all remaining vertices/atoms of degree two are removed by performing an edge merging operation. The only exception being when both vertices neighbours are connected (i.e. we have a triangle), when edge merging would lead to the loss of a cycle. The result is a minimum cycle topological representation of the original molecule. This function is used in the computation of ring topology scaffolds (Oprea). If a ring contains a non-carbon atom, this atom is maintained. Neighbouring ring atoms which are of the same type are merged together into a single atom of the corresponding type. Parameters ---------- mol : rdkit.Chem.rdchem.Mol Returns ------- rdkit.Chem.rdchem.RWMol Minimum cycle topological graph. """ edit = RWMol(mol) remove_atoms = set() for atom in edit.GetAtoms(): if atom.GetDegree() == 2: n1, n2 = atom.GetNeighbors() n1_idx, n2_idx = n1.GetIdx(), n2.GetIdx() connected = edit.GetBondBetweenAtoms(n1_idx, n2_idx) if not connected and (n1.GetAtomicNum() == atom.GetAtomicNum() or n2.GetAtomicNum() == atom.GetAtomicNum()): a_idx = atom.GetIdx() edit.RemoveBond(n1_idx, a_idx) edit.RemoveBond(n2_idx, a_idx) edit.AddBond(n1_idx, n2_idx, BondType.SINGLE) remove_atoms.add(a_idx) for a_idx in sorted(remove_atoms, reverse=True): edit.RemoveAtom(a_idx) return edit
def _join_atoms(self, combo: Chem.RWMol, anchor_A: int, anchor_B: int, distance: float, linking: bool = True): """ extrapolate positions between. by adding linkers if needed. """ conf = combo.GetConformer() pos_A = conf.GetAtomPosition(anchor_A) pos_B = conf.GetAtomPosition(anchor_B) n_new = int(round(distance / 1.22) - 1) xs = np.linspace(pos_A.x, pos_B.x, n_new + 2)[1:-1] ys = np.linspace(pos_A.y, pos_B.y, n_new + 2)[1:-1] zs = np.linspace(pos_A.z, pos_B.z, n_new + 2)[1:-1] # correcting for ring marker atoms def is_ring_atom(anchor: int) -> bool: atom = combo.GetAtomWithIdx(anchor) if atom.HasProp('_ori_i') and atom.GetIntProp('_ori_i') == -1: return True else: return False if is_ring_atom(anchor_A): distance -= 1.35 + 0.2 # Arbitrary + 0.2 to compensate for the ring not reaching (out of plane). n_new -= 1 xs = xs[1:] ys = ys[1:] zs = zs[1:] if is_ring_atom(anchor_B): distance -= 1.35 + 0.2 # Arbitrary + 0.2 to compensate for the ring not reaching (out of plane). n_new -= 1 xs = xs[:-1] ys = ys[:-1] zs = zs[:-1] # notify that things could be leary. if distance < 0: self.journal.debug( f'Two ring atoms detected to be close. Joining for now.' + ' They will be bonded/fused/spiro afterwards') # check if valid. if distance > self.joining_cutoff: msg = f'Atoms {anchor_A}+{anchor_B} are {distance} Å away. Cutoff is {self.joining_cutoff}.' self.journal.warning(msg) raise ConnectionError(msg) # place new atoms self.journal.debug( f'Molecules will be joined via atoms {anchor_A}+{anchor_B} ({distance} Å) via the addition of {n_new} atoms.' ) previous = anchor_A if linking is False and n_new > 0: self.journal.warning( f'Was going to bond {anchor_A} and {anchor_B} but reconsidered.' ) elif linking is True and n_new <= 0: combo.AddBond(previous, anchor_B, Chem.BondType.SINGLE) new_bond = combo.GetBondBetweenAtoms(previous, anchor_B) BondProvenance.set_bond(new_bond, 'main_novel') elif linking is False and n_new <= 0: combo.AddBond(previous, anchor_B, Chem.BondType.SINGLE) new_bond = combo.GetBondBetweenAtoms(previous, anchor_B) BondProvenance.set_bond(new_bond, 'other_novel') elif linking is True and n_new > 0: for i in range(n_new): # make oxygen the first and last bridging atom. if i == 0 and combo.GetAtomWithIdx( anchor_A).GetSymbol() == 'C': new_atomic = 8 elif i > 2 and i == n_new - 1 and combo.GetAtomWithIdx( anchor_B).GetSymbol() == 'C': new_atomic = 8 else: new_atomic = 6 idx = combo.AddAtom(Chem.Atom(new_atomic)) new = combo.GetAtomWithIdx(idx) new.SetBoolProp('_Novel', True) new.SetIntProp('_ori_i', 999) conf.SetAtomPosition( idx, Point3D(float(xs[i]), float(ys[i]), float(zs[i]))) combo.AddBond(idx, previous, Chem.BondType.SINGLE) new_bond = combo.GetBondBetweenAtoms(idx, previous) BondProvenance.set_bond(new_bond, 'linker') previous = idx combo.AddBond(previous, anchor_B, Chem.BondType.SINGLE) new_bond = combo.GetBondBetweenAtoms(previous, anchor_B) BondProvenance.set_bond(new_bond, 'linker') else: raise ValueError('Impossible') return combo.GetMol()