def map_atoms(mol_a: Mol, mol_b: Mol) -> Dict[int, int]: """Find all atoms in mol_a in mol_b Args: mol_a: mol_b: Returns: Dictionary where key are atom indices from mol_a and values are atom indices from mol_b """ a_conf = mol_a.GetConformer() b_conf = mol_b.GetConformer() # find mapping of atoms from mol to parent a2b_atoms = {} b2a_atoms = {} for a in mol_a.GetAtoms(): a_pos = a_conf.GetAtomPosition(a.GetIdx()) for b in mol_b.GetAtoms(): if b.GetIdx() in b2a_atoms: # Don't try to map same atom twice continue bp_pos = b_conf.GetAtomPosition(b.GetIdx()) if b.Match( a ) and a_pos.x == bp_pos.x and a_pos.y == bp_pos.y and a_pos.z == bp_pos.z: a2b_atoms[a.GetIdx()] = b.GetIdx() b2a_atoms[b.GetIdx()] = a.GetIdx() return a2b_atoms
def embed_r_groups(mol: Mol, parent: Mol): """Assign coordinates to R groups Args: mol: Molecule with atoms with '*' as symbol parent: Molecule that was used to generate `mol` Raises: LookupError: When atoms bonded to R group can not be found in parent """ mol2parent_idxs = map_atoms(mol, parent) parent2mol_idxs = {v for v in mol2parent_idxs.values()} idx2parent_atom = {a.GetIdx(): a for a in parent.GetAtoms()} mol_conf = mol.GetConformer() parent_conf = parent.GetConformer() r_pos_cache = set() for r_atom in mol.GetAtoms(): if r_atom.GetSymbol() == '*': for r_bond in r_atom.GetBonds(): # atom bonded to r group in mol r_bonded_atom = r_bond.GetOtherAtom(r_atom) r_bonded_idx = r_bonded_atom.GetIdx() if r_bonded_idx in mol2parent_idxs: # same atom in parent r_bonded_atom_parent_idx = mol2parent_idxs[r_bonded_idx] r_bonded_atom_parent = idx2parent_atom[ r_bonded_atom_parent_idx] for r_bonded_atom_parent_bond in r_bonded_atom_parent.GetBonds( ): # Atom bonded to atom which is bonded to R group in parent r_bonded_atom_parend_bonded_atom = r_bonded_atom_parent_bond.GetOtherAtom( r_bonded_atom_parent) r_bonded_atom_parend_bonded_atom_idx = r_bonded_atom_parend_bonded_atom.GetIdx( ) if r_bonded_atom_parend_bonded_atom_idx not in parent2mol_idxs: # atom in parent but not in mol which was replaced R group point = parent_conf.GetAtomPosition( r_bonded_atom_parend_bonded_atom_idx) if serialize_point(point) not in r_pos_cache: # Position has not been used for other R group mol_conf.SetAtomPosition( r_atom.GetIdx(), point) r_pos_cache.add(serialize_point(point)) break else: raise LookupError( 'Atom bonded to R group not found in parent')
def get_atom_distance(molecule: Mol, i1: int, i2: int, conf_id: int = 0) -> float: conformer = molecule.GetConformer(conf_id) vector = conformer.GetAtomPosition(i1) - conformer.GetAtomPosition(i2) return vector.Length()
def from_mol_to_ani_input(mol: Chem.Mol) -> dict: """ Generates atom_list and coord_list entries from rdkit mol. Parameters ---------- mol : rdkit.Chem.Mol Returns ------- { 'ligand_atoms' : atoms, 'ligand_coords' : coord_list} """ atom_list = [] coord_list = [] for a in mol.GetAtoms(): atom_list.append(a.GetSymbol()) pos = mol.GetConformer().GetAtomPosition(a.GetIdx()) coord_list.append([pos.x, pos.y, pos.z]) _ = write_pdb(mol, 'tmp.pdb') topology = md.load('tmp.pdb').topology os.remove('tmp.pdb') return { 'ligand_atoms': ''.join(atom_list), 'ligand_coords': np.array(coord_list) * unit.angstrom, 'ligand_topology': topology, }
def generate_orca_script_for_gas_phase(mol: Chem.Mol, conformer_id: int = 0) -> str: """Returns a psi4 Molecule object instance for a single conformer from a rdkit mol object. Parameters ---------- mol : Chem.Mol rdkit mol object conformer_id : int specifies the conformer to use Returns ------- mol : psi4.core.Molecule a psi4 molecule object instance """ assert type(mol) == Chem.Mol or type(mol) == Chem.PropertyMol.PropertyMol atoms = mol.GetAtoms() header_str = "! B3LYP 6-31G* \n" xyz_string_str = f"*xyz 0 1\n" for _, atom in enumerate(atoms): pos = mol.GetConformer(conformer_id).GetAtomPosition(atom.GetIdx()) xyz_string_str += f"{atom.GetSymbol()} {pos.x:.7f} {pos.y:.7f} {pos.z:.7f}\n" xyz_string_str += "*" orca_input = header_str + xyz_string_str return orca_input
def _aromatic_direction_vector(molecule: Chem.Mol, atom_indxs: Tuple[int], conformer_idx: int) -> np.ndarray: """ Compute the direction vector for an aromatic feature. Parameters ---------- molecule : rdkit.Chem.rdchem.Mol Molecule that contains the feature which direction vector will be computed. atom_indxs : tuple of int Indices of the aromatic atoms. conformer_idx : int Index of the conformer for which the direction vector will be computed. Returns ------- direction : numpy.ndarray; shape(3,) Coordinates of the direction vector. """ coords = np.zeros((3, 3)) # Take just the first three atoms for j, idx in enumerate(atom_indxs[0:3]): position = molecule.GetConformer(conformer_idx).GetAtomPosition( idx) coords[j, 0] = position.x coords[j, 1] = position.y coords[j, 2] = position.z # Find the vector normal to the plane defined by the three atoms u = coords[1, :] - coords[0, :] v = coords[2, :] - coords[0, :] direction = np.cross(u, v) return direction
def get_pharmacophoric_point(ligand: Chem.Mol, feat_name: str, atom_indices: Sequence, conformer_index: int, radius: float, directionality: bool) -> PharmacophoricPoint: """ Obtain the coordinates and if specified the direction vector and return a pharmacophoric point. Parameters ---------- ligand : rdkit.Chem.Mol A ligand conformer_index : int The conformer whose coordinates will be used to obtain the pharmacophoric points. radius : float Lenght of the radius in angstroms of the parmacohporic point. directionality : bool Whether to compute the direction vectgor of that point. Returns ------- openpharmacophore.PharmacophoricPoint A pharmacophoric point. """ if len(atom_indices) > 1: # Find the centroid # Aromatic, hydrophobic, positive or negative feature coords = PharmacophoricPointExtractor._feature_centroid( ligand, atom_indices, conformer_index) # Find direction vector if directionality: direction = PharmacophoricPointExtractor._aromatic_direction_vector( ligand, atom_indices, conformer_index) else: direction = None else: # Find the centroid # Donor or acceptor feature position = ligand.GetConformer(conformer_index).GetAtomPosition( atom_indices[0]) coords = np.zeros((3, )) coords[0] = position.x coords[1] = position.y coords[2] = position.z # Find direction vector if directionality: direction = PharmacophoricPointExtractor._donor_acceptor_direction_vector( ligand, atom_indices[0], coords, conformer_index) else: direction = None return PharmacophoricPoint(feat_type=feat_name, center=puw.quantity(coords, "angstroms"), radius=puw.quantity(radius, "angstroms"), direction=direction, atom_indices=atom_indices)
def prune_conformers( mol: Chem.Mol, energies: list, rmsd_threshold: float ) -> (Chem.Mol, list): """ Adopted from: https://github.com/skearnes/rdkit-utils/blob/master/rdkit_utils/conformers.py Prune conformers from a molecule using an RMSD threshold, starting with the lowest energy conformer. Parameters ---------- mol : RDKit Mol Molecule. Returns ------- A new RDKit Mol containing the chosen conformers, sorted by increasing energy. """ from typing import List rmsd = get_conformer_rmsd(mol) sort = np.argsort( [x.value_in_unit(unit.kilocalorie_per_mole) for x in energies] ) # sort by increasing energy keep: List[int] = [] # always keep lowest-energy conformer discard: List[int] = [] for i in sort: # always keep lowest-energy conformer if len(keep) == 0: keep.append(i) continue # get RMSD to selected conformers this_rmsd = rmsd[i][np.asarray(keep, dtype=int)] # discard conformers within the RMSD threshold if np.all(this_rmsd >= rmsd_threshold): keep.append(i) else: discard.append(i) # create a new molecule to hold the chosen conformers # this ensures proper conformer IDs and energy-based ordering new_mol = Chem.Mol(mol) new_mol.RemoveAllConformers() conf_ids = [conf.GetId() for conf in mol.GetConformers()] filtered_energies = [] logger.debug(f"keep: {keep}") for i in keep: logger.debug(i) conf = mol.GetConformer(conf_ids[i]) filtered_energies.append(energies[i]) new_mol.AddConformer(conf, assignId=True) return new_mol, filtered_energies
def mol2xyz_by_confid(molecule: Mol, prefix='rdmol', confid=0, comment_line=''): natoms = molecule.GetNumAtoms() filename = "{}_{}.xyz".format(prefix, confid) s = "{}\n{}\n".format(natoms, comment_line) for i in range(natoms): position = molecule.GetConformer(confid).GetAtomPosition(i) symbol = molecule.GetAtomWithIdx(i).GetSymbol() s += "{}\t{:.6} {:.6} {:.6}\n".format(symbol, position.x, position.y, position.z) with open(filename, 'w') as f: f.write(s)
def conformer_to_xyz(molecule: Mol, conf_id=0, comment=None) -> str: num_atoms = molecule.GetNumAtoms() string = f'{num_atoms}\n' if comment: string += comment conformer = molecule.GetConformer(conf_id) for atom_idx in range(molecule.GetNumAtoms()): atom = molecule.GetAtomWithIdx(atom_idx) position = conformer.GetAtomPosition(atom_idx) string += f'\n{atom.GetSymbol()} {position.x} {position.y} {position.z}' return string
def store_positions(self, mol: Chem.Mol) -> Chem.Mol: """ Saves positional data as _x, _y, _z and majorly ``_ori_i``, the original index. The latter gets used by ``_get_new_index``. :param mol: :return: """ conf = mol.GetConformer() name = mol.GetProp('_Name') for i, atom in enumerate(mol.GetAtoms()): pos = conf.GetAtomPosition(i) atom.SetIntProp('_ori_i', i) atom.SetProp('_ori_name', name) atom.SetDoubleProp('_x', pos.x) atom.SetDoubleProp('_y', pos.y) atom.SetDoubleProp('_z', pos.z) return mol
def check_conformer( mol: Mol, conformer: Conformer, ) -> bool: # throw warnings if # - the conformer is actually 2D (useless Z coordinate in graph) # - some atoms have all-zero coordinates, which implies bad conformer _positions: np.array = conformer.GetPositions() if not _positions[:, 2].any(): _warning_msg = f'Conformer has no Z coordinates. Continuing ...' _LOGGER.warning(_warning_msg) if not np.array([_p.any() for _p in _positions]).all(): _warning_msg = f'Conformer has atom(s) with invalid coordinates ' \ f'(0.0, 0.0, 0.0). Continuing ...' _LOGGER.warning(_warning_msg) # make sure that the molecule and conformer are of the same size return len(mol.GetAtoms()) == len(mol.GetConformer().GetPositions())
def _donor_acceptor_direction_vector(molecule: Chem.Mol, feat_type: str, atom_indx: int, coords: np.ndarray, conformer_idx: int) -> np.ndarray: """ Compute the direction vector for an H bond donor or H bond acceptor feature Parameters ---------- molecule : rdkit.Chem.rdchem.Mol Molecule that contains the feature which direction vector will be computed. feat_type : str Type of feature. Wheter is donor or acceptor. atom_indx : int Index of the H bond acceptor or donor atom. coords : numpy.ndarray; shape(3,) Coordiantes of the H bond acceptor or donor atom. conformer_idx : int Index of the conformer for which the direction vector will be computed. Returns ------- direction : numpy.ndarray; shape(3,) Coordinates of the direction vector. """ direction = np.zeros((3, )) atom = molecule.GetAtomWithIdx(atom_indx) for a in atom.GetNeighbors(): if a.GetSymbol() == "H": continue position = molecule.GetConformer(conformer_idx).GetAtomPosition( a.GetIdx()) direction[0] += position.x - coords[0] direction[1] += position.y - coords[1] direction[2] += position.z - coords[2] if feat_type == "Donor": direction = -direction return direction
def measure_map(self, mol: Chem.Mol, mapping: Dict[int, int]) -> np.array: """ :param mol: :param mapping: followup to comined :return: """ conf = mol.GetConformer() d = np.array([]) for fi, ci in mapping.items(): fatom = self.followup.GetAtomWithIdx(fi) for neigh in fatom.GetNeighbors(): ni = neigh.GetIdx() if ni not in mapping: continue nci = mapping[ni] a = np.array(conf.GetAtomPosition(ci)) b = np.array(conf.GetAtomPosition(nci)) d = np.append(d, np.linalg.norm(a - b)) return d
def from_rdmol(self, rdmol: Chem.Mol, atom_subset: Optional[Iterable[Atom]] = None) -> None: """Update the atomic coordinates of this instance with coordinates from an RDKit molecule. Alternatively, update only a subset of atoms. Parameters ---------- rdmol : |rdkit.Chem.Mol|_ An RDKit molecule. atom_subset : |list|_ [|plams.Atom|_] Optional: A subset of atoms in **self**. """ at_subset = atom_subset or self.atoms conf = rdmol.GetConformer() for at1, at2 in zip(at_subset, rdmol.GetAtoms()): pos = conf.GetAtomPosition(at2.GetIdx()) at1.coords = (pos.x, pos.y, pos.z)
def mol2psi4(mol: Chem.Mol, conformer_id: int = 0) -> psi4.core.Molecule: """Returns a psi4 Molecule object instance for a single conformer from a rdkit mol object. Parameters ---------- mol : Chem.Mol rdkit mol object conformer_id : int specifies the conformer to use Returns ------- mol : psi4.core.Molecule a psi4 molecule object instance """ assert type(mol) == Chem.Mol atoms = mol.GetAtoms() string = "\n" for _, atom in enumerate(atoms): pos = mol.GetConformer(conformer_id).GetAtomPosition(atom.GetIdx()) string += "{} {} {} {}\n".format(atom.GetSymbol(), pos.x, pos.y, pos.z) string += "units angstrom\n" return psi4.geometry(string)
def cluster_by_rmsd(m: Chem.Mol, energies, rmsd_threshold: float): """ conformer in m should be assigned consecutive ids with delta=1 :param energies: :param m: :param rmsd_threshold: :return: """ rmsd_condensed = AllChem.GetConformerRMSMatrix(m) # rmsd_condensed = ConfGen.GetBestRMSMatrix(m) from rdkit.ML.Cluster.Butina import ClusterData clusters = ClusterData(rmsd_condensed, len(m.GetConformers()), distThresh=rmsd_threshold, isDistData=True) m_after_prune = Chem.Mol(m) m_after_prune.RemoveAllConformers() n_energies = [] for c in clusters: conf = m.GetConformer(c[0]) n_energies.append(energies[c[0]]) m_after_prune.AddConformer(conf, assignId=True) return m_after_prune, n_energies
def _feature_centroid(molecule: Chem.Mol, atom_indxs: Tuple[int], conformer_index: int) -> np.ndarray: """ Get the 3D coordinates of the centroid of a feature that encompasses more than one atom. This could be aromatic, hydrophobic, negative and positive features Parameters ---------- molecule : rdkit.Chem.Mol Molecule that contains the feature which centroid will be computed atom_indxs : tuple of int Indices of the atoms that belong to the feature conformer_index : int Index of the conformer for which the feature centroid will be computed Returns ------- centroid : numpy.ndarray of shape (3, ) Array with the coordinates of the centroid of the feature. """ n_atoms = len(atom_indxs) coords = np.zeros((n_atoms, 3)) for j, idx in enumerate(atom_indxs): position = molecule.GetConformer(conformer_index).GetAtomPosition( idx) coords[j, 0] = position.x coords[j, 1] = position.y coords[j, 2] = position.z centroid = coords.mean(axis=0) return centroid
def mol_coords(m: Chem.Mol): AllChem.Compute2DCoords(m) c = m.GetConformer() return c.GetPositions()
def _from_mol_to_ani_input(self, mol: Chem.Mol, enforceChirality: bool): """ Helper function - does not need to be called directly. Generates ANI input from a rdkit mol object """ # generate atom list atom_list = [] for a in mol.GetAtoms(): atom_list.append(a.GetSymbol()) if ("S" in atom_list or "P" in atom_list or "Cl" in atom_list or "Br" in atom_list or "I" in atom_list): raise NotImplementedError("Atom not yet included in ANI.") # generate conformations mol = self._generate_conformations_from_mol(mol, self.nr_of_conformations, enforceChirality) # generate coord list coord_list = [] # add conformations to coord_list for conf_idx in range(mol.GetNumConformers()): tmp_coord_list = [] for a in mol.GetAtoms(): pos = mol.GetConformer(conf_idx).GetAtomPosition(a.GetIdx()) tmp_coord_list.append([pos.x, pos.y, pos.z]) coord_list.append(tmp_coord_list) coord_list = np.asarray(coord_list) assert coord_list.shape == (mol.GetNumConformers(), mol.GetNumAtoms(), 3) coord_list *= unit.angstrom # generate bond list bond_list = [] for b in mol.GetBonds(): a1 = b.GetBeginAtom() a2 = b.GetEndAtom() bond_list.append((a1.GetIdx(), a2.GetIdx())) # get mdtraj topology n = random.randint(1, 10000000) # TODO: use tmpfile for this https://stackabuse.com/the-python-tempfile-module/ or io.StringIO _ = write_pdb(mol, f"tmp{n}.pdb") topology = md.load(f"tmp{n}.pdb").topology os.remove(f"tmp{n}.pdb") ani_input = { "ligand_atoms": "".join(atom_list), "ligand_coords": coord_list, "ligand_topology": topology, "ligand_bonds": bond_list, } # generate ONE ASE object if thermocorrections are needed ase_atom_list = [] for e, c in zip(ani_input["ligand_atoms"], coord_list[0]): c_list = ( c[0].value_in_unit(unit.angstrom), c[1].value_in_unit(unit.angstrom), c[2].value_in_unit(unit.angstrom), ) ase_atom_list.append(Atom(e, c_list)) mol = Atoms(ase_atom_list) ani_input["ase_mol"] = mol return ani_input
def place_from_map(self, target_mol: Chem.Mol, template_mol: Chem.Mol, atom_map: Optional[Dict] = None) -> Chem.Mol: """ This method places the atoms with known mapping and places the 'uniques' (novel) via an aligned mol (the 'sextant') This sextant business is a workaround for the fact that only minimised molecules can use the partial embedding function of RDKit. :param target_mol: target mol :param template_mol: the template/scaffold to place the mol :param atom_map: something that get_mcs_mapping would return. :return: """ # Note none of this malarkey: AllChem.MMFFOptimizeMolecule(ref) # prealignment if target_mol is None: target_mol = self.initial_mol sextant = Chem.Mol(target_mol) Chem.SanitizeMol(sextant) AllChem.EmbedMolecule(sextant) AllChem.MMFFOptimizeMolecule(sextant) ###################################################### # mapping retrieval and sextant alignment # variables: atom_map sextant -> uniques if atom_map is None: atom_map, mode = self.get_mcs_mapping(target_mol, template_mol) msg = { **{k: str(v) for k, v in mode.items()}, 'N_atoms': len(atom_map) } self.journal.debug(f"followup-chimera' = {msg}") rdMolAlign.AlignMol(sextant, template_mol, atomMap=list(atom_map.items()), maxIters=500) # place atoms that have a known location putty = Chem.Mol(sextant) pconf = putty.GetConformer() chimera_conf = template_mol.GetConformer() uniques = set() # unique atoms in followup for i in range(putty.GetNumAtoms()): p_atom = putty.GetAtomWithIdx(i) p_atom.SetDoubleProp('_Stdev', 0.) p_atom.SetProp('_Origin', 'none') if i in atom_map: ci = atom_map[i] c_atom = template_mol.GetAtomWithIdx(ci) if c_atom.HasProp('_Stdev'): stdev = c_atom.GetDoubleProp('_Stdev') origin = c_atom.GetProp('_Origin') p_atom.SetDoubleProp('_Stdev', stdev) p_atom.SetProp('_Origin', origin) pconf.SetAtomPosition(i, chimera_conf.GetAtomPosition(ci)) else: uniques.add(i) ###################################################### # I be using a sextant for dead reckoning! # variables: sextant unique team categories = self._categorise(sextant, uniques) done_already = [] # multi-attachment issue. for unique_idx in categories['pairs']: # attachment unique indices # check the index was not done already (by virtue of a second attachment) if unique_idx in done_already: continue # get other attachments if any. team = self._recruit_team(target_mol, unique_idx, categories['uniques']) other_attachments = ( team & set(categories['pairs'].keys())) - {unique_idx} sights = set() # atoms to align against for att_idx in [unique_idx] + list(other_attachments): for pd in categories['pairs'][att_idx]: first_sight = pd['idx'] sights.add((first_sight, first_sight)) neighs = [ i.GetIdx() for i in sextant.GetAtomWithIdx( first_sight).GetNeighbors() if i.GetIdx() not in uniques ] for n in neighs: sights.add((n, n)) if self.attachment and list(categories['dummies']) and list( categories['dummies'])[0] in team: r = list(categories['dummies'])[0] pconf.SetAtomPosition( r, self.attachment.GetConformer().GetAtomPosition(0)) sights.add((r, r)) rdMolAlign.AlignMol(sextant, putty, atomMap=list(sights), maxIters=500) sconf = sextant.GetConformer() self.journal.debug( f'alignment atoms for {unique_idx} ({team}): {sights}') # self.draw_nicely(sextant, highlightAtoms=[a for a, b in sights]) # copy position over for atom_idx in team: pconf.SetAtomPosition(atom_idx, sconf.GetAtomPosition(atom_idx)) # the ring problem does not apply here but would result in rejiggling atoms. for other in other_attachments: done_already.append(other) # complete AllChem.SanitizeMol(putty) return putty # positioned_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()