Exemple #1
0
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
Exemple #2
0
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')
Exemple #3
0
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()
Exemple #4
0
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,
    }
Exemple #5
0
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
Exemple #6
0
    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
Exemple #7
0
    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)
Exemple #8
0
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
Exemple #9
0
 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)
Exemple #10
0
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
Exemple #12
0
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())
Exemple #13
0
    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
Exemple #14
0
    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
Exemple #15
0
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)
Exemple #16
0
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)
Exemple #17
0
    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
Exemple #18
0
    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
Exemple #19
0
def mol_coords(m: Chem.Mol):
    AllChem.Compute2DCoords(m)
    c = m.GetConformer()
    return c.GetPositions()
Exemple #20
0
    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
Exemple #21
0
    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()