def _prevent_weird_rings(self, mol: Chem.Mol): if not isinstance(mol, Chem.RWMol): mol = Chem.RWMol(mol) ringatoms = self._get_ring_info(mol) #GetRingInfo().AtomRings() for ring_A, ring_B in itertools.combinations(ringatoms, r=2): shared = set(ring_A).intersection(set(ring_B)) if len(shared) == 0: log.debug('This molecule has some separate rings') pass # separate rings elif len(shared) == 1: log.debug('This molecule has a spiro bicycle') pass # spiro ring. elif len(shared) == 2: log.debug('This molecule has a fused ring') if mol.GetBondBetweenAtoms(*shared) is not None: pass # indole/naphtalene small, big = sorted([ring_A, ring_B], key=lambda ring: len(ring)) if len(small) == 4: log.warning('This molecule has a benzo-azetine–kind-of-thing: expanding to indole') # Chem.MolFromSmiles('C12CCCCC1CC2') # benzo-azetine is likely an error: add and extra atom a, b = set(small).difference(big) self._place_between(mol, a, b) elif len(small) == 3: log.warning('This molecule has a benzo-cyclopropane–kind-of-thing: expanding to indole') # Chem.MolFromSmiles('C12CCCCC1C2') # benzo-cyclopronane is actually impossible at this stage. a = list(set(small).difference(big))[0] for b in shared: self._place_between(mol, a, b) else: pass # indole and nathalene elif (len(ring_A), len(ring_B)) == (6, 6): raise Exception('This is utterly impossible') else: print(f'mysterious ring system {len(ring_A)} + {len(ring_B)}') pass # ???? elif len(shared) < self.atoms_in_bridge_cutoff: #adamantene/norbornane/tropinone kind of thing log.warning('This molecule has a bridge: leaving') pass # ideally check if planar... else: log.warning('This molecule has a bridge that will be removed') mol = self._prevent_bridge_ring(mol, ring_A) # start from scratch. return self._prevent_weird_rings(mol) return mol.GetMol()
def expand_ring(self, mol: Chem.Mol, bonded_as_original=False): """ Undoes collapse ring :param mol: :param bonded_as_original: if false bonds by proximity, if true bonds as remembered :return: """ mol = Chem.RWMol(mol) rings = self._get_expansion_data(mol) self._place_ring_atoms(mol, rings) mergituri = self._ring_overlap_scenario(mol, rings) # bonding log.debug('Starting ring expansion') if bonded_as_original: self._restore_original_bonding(mol, rings) self._fix_overlap(mol, mergituri) self._delete_collapsed(mol) else: self._connenct_ring(mol, rings) self._mark_neighbors(mol, rings) self._fix_overlap(mol, mergituri) log.debug('Deleting collapsed') self._delete_collapsed(mol) log.debug('Infering bonding') self._infer_bonding_by_proximity(mol) # absorb_overclose and join_overclose # this should not happen... but it can!) mol = self._emergency_joining(mol) # _emergency_joining returns a Chem.Mol not a Chem.RWMol # prevent weird nested rings. mol = self._prevent_conjoined_ring(mol) mol = self._prevent_weird_rings(mol) mol = self._prevent_allene(mol) if mol is None: raise ValueError('(Impossible) Failed at some point...') elif isinstance(mol, Chem.RWMol): return mol.GetMol() else: return mol
def _prevent_conjoined_ring(self, mol: Chem.Mol) -> Chem.Mol: """ This kills bridging bonds with not atoms in the bridge within rings. So it is bridged, fused and spiro safe. It removes only one bond, so andamantane/norbornane are safe. :param mol: :return: """ c = Counter([i for ring in self._get_ring_info(mol) for i in ring]) nested = [k for k in c if c[k] >= 3] pairs = [(idx_a, idx_b) for idx_a, idx_b in itertools.combinations(nested, r=2) if mol.GetBondBetweenAtoms(idx_a, idx_b) is not None] rank = sorted(pairs, key=lambda x: c[x[0]] + c[x[1]], reverse=True) if len(rank) > 0: idx_a, idx_b = rank[0] if not isinstance(mol, Chem.RWMol): mol = Chem.RWMol(mol) mol.RemoveBond(idx_a, idx_b) # SetBoolProp('_IsRingBond') is not important log.info(f'Zero-atom bridged ring issue: bond between {idx_a}-{idx_b} removed') return self._prevent_conjoined_ring(mol) elif isinstance(mol, Chem.RWMol): return mol.GetMol() else: return mol
def collapse_ring(self, mol: Chem.Mol) -> Chem.Mol: """ Collapses a ring(s) into a single dummy atom(s). Stores data as JSON in the atom. :param mol: :return: """ self.store_positions(mol) mol = Chem.RWMol(mol) conf = mol.GetConformer() center_idxs = [] morituri = [] old2center = defaultdict(list) for atomset in mol.GetRingInfo().AtomRings(): morituri.extend(atomset) neighs = [] neighbonds = [] bonds = [] xs = [] ys = [] zs = [] elements = [] # add elemental ring c = mol.AddAtom(Chem.Atom('C')) center_idxs.append(c) central = mol.GetAtomWithIdx(c) name = mol.GetProp('_Name') if mol.HasProp('_Name') else '???' central.SetProp('_ori_name', name), # get data for storage for i in atomset: old2center[i].append(c) atom = mol.GetAtomWithIdx(i) neigh_i = [a.GetIdx() for a in atom.GetNeighbors()] neighs.append(neigh_i) bond = [mol.GetBondBetweenAtoms(i, j).GetBondType().name for j in neigh_i] bonds.append(bond) pos = conf.GetAtomPosition(i) xs.append(pos.x) ys.append(pos.y) zs.append(pos.z) elements.append(atom.GetSymbol()) # store data in elemental ring central.SetIntProp('_ori_i', -1) central.SetProp('_ori_is', json.dumps(atomset)) central.SetProp('_neighbors', json.dumps(neighs)) central.SetProp('_xs', json.dumps(xs)) central.SetProp('_ys', json.dumps(ys)) central.SetProp('_zs', json.dumps(zs)) central.SetProp('_elements', json.dumps(elements)) central.SetProp('_bonds', json.dumps(bonds)) conf.SetAtomPosition(c, Point3D(*[sum(axis) / len(axis) for axis in (xs, ys, zs)])) for atomset, center_i in zip(mol.GetRingInfo().AtomRings(), center_idxs): # bond to elemental ring central = mol.GetAtomWithIdx(center_i) neighss = json.loads(central.GetProp('_neighbors')) bondss = json.loads(central.GetProp('_bonds')) for neighs, bonds in zip(neighss, bondss): for neigh, bond in zip(neighs, bonds): if neigh not in atomset: bt = getattr(Chem.BondType, bond) if neigh not in morituri: mol.AddBond(center_i, neigh, bt) else: for other_center_i in old2center[neigh]: if center_i != other_center_i: if not mol.GetBondBetweenAtoms(center_i, other_center_i): mol.AddBond(center_i, other_center_i, bt) break else: raise ValueError(f'Cannot find what {neigh} became') for i in sorted(set(morituri), reverse=True): mol.RemoveAtom(self._get_new_index(mol, i)) return mol.GetMol()