def _get_rotor_wbo(
        cls, molecule: Molecule, rotor_bonds: List[BondTuple]
    ) -> Dict[BondTuple, float]:
        """Cache the WBO of each bond in a specific set of rotor bonds..

        Parameters
        ----------
            molecule
                The molecule containing the rotors.
            rotor_bonds
                The map indices of the rotor bonds to return the WBOs of.

        Returns
        -------
            The WBO of each rotor bond.
        """

        if any(bond.fractional_bond_order is None for bond in molecule.bonds):

            raise RuntimeError(
                "WBO was not calculated for this molecule. Calculating WBO..."
            )

        rotors_wbo = {}

        for bond_indices in rotor_bonds:

            bond = molecule.get_bond_between(
                get_atom_index(molecule, bond_indices[0]),
                get_atom_index(molecule, bond_indices[1]),
            )

            rotors_wbo[bond_indices] = bond.fractional_bond_order

        return rotors_wbo
    def _compare_wbo(
        cls, fragment: Molecule, bond_tuple: BondTuple, parent_wbo: float, **kwargs
    ) -> float:
        """Compare Wiberg Bond order of rotatable bond in a fragment to the parent.

        Parameters
        ----------
        fragment
            The fragment containing the rotatable bond.
        bond_tuple
            The map indices of the rotatable bond.
        parent_wbo
            The WBO of the parent bond with map indices matching ``bond_tuple``.

        Returns
        -------
            The absolute difference between the fragment and parent WBOs.
        """

        # Create new fragment object because sometimes the molecule created from atom
        # bond set is wonky and then the WBOs are not reproducible
        fragment = Molecule.from_smiles(
            fragment.to_smiles(mapped=True), allow_undefined_stereo=True
        )

        fragment_map = fragment.properties.pop("atom_map", None)

        try:
            fragment = assign_elf10_am1_bond_orders(fragment, **kwargs)

        except RuntimeError:

            # Most of the time it fails because it is either missing parameters or a
            # functional group that should not be fragmented was fragmented
            logger.warning(
                f"Cannot calculate WBO for fragment {fragment.to_smiles()}. Continue "
                f"growing fragment"
            )

            # TODO: handle different kinds of failures instead of just continuing to
            #      grow until the failure goes away. Some fail because there are
            #      functional groups that should not be fragmented.
            return 1.0

        if fragment_map is not None:
            fragment.properties["atom_map"] = fragment_map

        bond = fragment.get_bond_between(
            get_atom_index(fragment, bond_tuple[0]),
            get_atom_index(fragment, bond_tuple[1]),
        )

        fragment_wbo = bond.fractional_bond_order

        return abs(parent_wbo - fragment_wbo)
def test_get_ring_and_fgroup_ortho(input_smiles, bond_smarts,
                                   expected_pattern):
    """Ensure that FGs and rings attached to ortho groups are correctly
    detected.

    The expected values were generated using fragmenter=0.0.7
    """

    molecule, _, functional_groups, ring_systems = Fragmenter._prepare_molecule(
        smiles_to_molecule(input_smiles, True), default_functional_groups(),
        False)

    bond = tuple(
        get_map_index(molecule, i)
        for i in molecule.chemical_environment_matches(bond_smarts)[0])

    # noinspection PyTypeChecker
    atoms, bonds = Fragmenter._get_torsion_quartet(molecule, bond)
    atoms, bonds = Fragmenter._get_ring_and_fgroups(molecule,
                                                    functional_groups,
                                                    ring_systems, atoms, bonds)

    actual_atoms = {
        map_index
        for map_index in atoms if
        molecule.atoms[get_atom_index(molecule, map_index)].atomic_number != 1
    }
    expected_atoms = {
        get_map_index(molecule, atom_index)
        for match in molecule.chemical_environment_matches(expected_pattern)
        for atom_index in match
    }

    assert actual_atoms == expected_atoms
Example #4
0
    def _process_fragments(
        cls, fragments: "FragmentationResult", component_result: ComponentResult
    ):
        """Process the resulting fragments and tag the targeted bonds ready for torsiondrives."""
        from openff.fragmenter.utils import get_atom_index

        for bond_map, fragment in fragments.fragments_by_bond.items():
            fragment_mol = fragment.molecule
            # get the index of the atoms in the fragment
            atom1, atom2 = get_atom_index(fragment_mol, bond_map[0]), get_atom_index(
                fragment_mol, bond_map[1]
            )
            bond = fragment_mol.get_bond_between(atom1, atom2)
            symmetry_classes = get_symmetry_classes(fragment_mol)
            symmetry_group = get_symmetry_group(
                atom_group=(bond.atom1_index, bond.atom2_index),
                symmetry_classes=symmetry_classes,
            )
            torsion = get_torsion(bond)
            torsion_tag = TorsionIndexer()
            torsion_tag.add_torsion(torsion=torsion, symmetry_group=symmetry_group)
            fragment_mol.properties["dihedrals"] = torsion_tag
            del fragment_mol.properties["atom_map"]
            component_result.add_molecule(fragment_mol)
def _extract_rd_fragment(
    molecule: Molecule, atom_indices: Set[int], bond_indices: Set[Tuple[int, int]]
) -> Molecule:

    from rdkit import Chem

    rd_molecule = Chem.RWMol(molecule.to_rdkit())
    rd_atoms_by_map: Dict[int, Chem.Atom] = {}

    # Restore the map indices as to_rdkit does not automatically add them.
    for atom in rd_molecule.GetAtoms():
        atom.SetAtomMapNum(get_map_index(molecule, atom.GetIdx()))

        rd_atoms_by_map[atom.GetAtomMapNum()] = atom

    atoms_to_use = [get_atom_index(molecule, i) for i in atom_indices]
    bonds_to_use = [
        rd_molecule.GetBondBetweenAtoms(
            get_atom_index(molecule, pair[0]), get_atom_index(molecule, pair[1])
        ).GetIdx()
        for pair in bond_indices
    ]

    # Make sure to include any Hs bonded to the included atom set otherwise radicals
    # will form.
    for map_index in atom_indices:

        for neighbour in rd_atoms_by_map[map_index].GetNeighbors():

            if (
                neighbour.GetAtomicNum() != 1
                or neighbour.GetAtomMapNum() < 1
                or neighbour.GetAtomMapNum() in atom_indices
            ):
                continue

            atoms_to_use.append(neighbour.GetIdx())
            bonds_to_use.append(
                rd_molecule.GetBondBetweenAtoms(
                    rd_atoms_by_map[map_index].GetIdx(), neighbour.GetIdx()
                ).GetIdx()
            )

    # Add additional hydrogens to atoms where the total valence will change likewise to
    # ensure the valence does not change.
    rd_atoms_by_index = {atom.GetIdx(): atom for atom in rd_molecule.GetAtoms()}

    for atom_index in [*atoms_to_use]:

        atom = rd_atoms_by_index[atom_index]

        old_valence = atom.GetTotalValence()
        new_valence = atom.GetTotalValence()

        for neighbour_bond in rd_atoms_by_index[atom_index].GetBonds():

            if (
                neighbour_bond.GetBeginAtomIdx() in atoms_to_use
                and neighbour_bond.GetEndAtomIdx() in atoms_to_use
            ):
                continue

            new_valence -= neighbour_bond.GetValenceContrib(atom)

        if numpy.isclose(old_valence, new_valence):
            # Skip the cases where the valence won't change
            continue

        if (
            atom.GetAtomicNum() == 6
            and atom.GetIsAromatic()
            and sum(
                1 for bond_tuple in bond_indices if atom.GetAtomMapNum() in bond_tuple
            )
            == 1
        ):

            # This is likely a cap carbon which was retained from an existing ring. It's
            # aromaticity needs to be cleared before calling ``MolFragmentToSmiles``
            # otherwise will (understandably) be confused and throw an exception.
            atom.SetIsAromatic(False)

        # Add a hydrogen to the atom whose valence will change.
        for _ in range(int(numpy.rint(old_valence - new_valence))):

            new_atom = Chem.Atom(1)
            new_atom_index = rd_molecule.AddAtom(new_atom)

            rd_molecule.AddBond(atom_index, new_atom_index)

            new_bond = rd_molecule.GetBondBetweenAtoms(atom_index, new_atom_index)
            new_bond.SetBondType(Chem.BondType.SINGLE)
            new_bond.SetIsAromatic(False)

            atoms_to_use.append(new_atom_index)
            bonds_to_use.append(new_bond.GetIdx())

    fragment_smiles = Chem.MolFragmentToSmiles(rd_molecule, atoms_to_use, bonds_to_use)
    fragment = Molecule.from_smiles(fragment_smiles, allow_undefined_stereo=True)

    return fragment
def test_get_atom_index():

    molecule = Molecule.from_smiles("[C:5]([H:1])([H:2])([H:3])([H:4])")
    assert get_atom_index(molecule, 5) == 0
    def _cap_open_valence(
        cls,
        parent: Molecule,
        parent_groups: FunctionalGroups,
        atoms: Set[int],
        bonds: Set[BondTuple],
    ) -> AtomAndBondSet:
        """Cap with methyl for fragments that ends with N, O or S. Otherwise cap with H

        Parameters
        ----------
        parent
            The molecule being fragmented.
        parent_groups
            A dictionary of the functional groups on the molecule which should not
            be fragmented.
        atoms
            The map indices of the atoms in the fragment being constructed.
        bonds
            The map indices of the bonds in the fragment being constructed.
        """

        map_index_to_functional_group = {
            map_index: functional_group
            for functional_group in parent_groups
            for map_index in parent_groups[functional_group][0]
        }

        atoms_to_add = set()
        bonds_to_add = set()

        for map_index in atoms:

            atom_index = get_atom_index(parent, map_index)
            atom = parent.atoms[atom_index]

            if (
                atom.atomic_number not in (7, 8, 16)
                and map_index not in map_index_to_functional_group
            ):
                continue

            # If atom is N, O or S, it needs to be capped
            should_cap = False

            for neighbour in atom.bonded_atoms:

                neighbour_map_index = get_map_index(
                    parent, neighbour.molecule_atom_index
                )

                if neighbour.atomic_number == 1 or neighbour_map_index in atoms:
                    continue

                should_cap = True
                break

            if not should_cap:
                continue

            for neighbour in atom.bonded_atoms:

                if neighbour.atomic_number != 6:
                    continue

                neighbour_map_index = get_map_index(
                    parent, neighbour.molecule_atom_index
                )

                atoms_to_add.add(neighbour_map_index)
                bonds_to_add.add((map_index, neighbour_map_index))

        atoms.update(atoms_to_add)
        bonds.update(bonds_to_add)

        return atoms, bonds
    def _select_neighbour_by_path_length(
        cls, molecule: Molecule, atoms: Set[int], target_bond: BondTuple
    ) -> Optional[Tuple[int, BondTuple]]:

        atom_indices = {get_atom_index(molecule, atom) for atom in atoms}

        atoms_to_add = [
            (atom_index, neighbour.molecule_atom_index)
            for atom_index in atom_indices
            for neighbour in molecule.atoms[atom_index].bonded_atoms
            if neighbour.atomic_number != 1
            and neighbour.molecule_atom_index not in atom_indices
        ]
        map_atoms_to_add = [
            (
                get_map_index(molecule, j),
                (get_map_index(molecule, i), get_map_index(molecule, j)),
            )
            for i, j in atoms_to_add
        ]

        # Compute the distance from each neighbouring atom to each of the atoms in the
        # target bond.
        nx_molecule = molecule.to_networkx()

        target_indices = [get_atom_index(molecule, atom) for atom in target_bond]

        path_lengths_1, path_lengths_2 = zip(
            *(
                (
                    networkx.shortest_path_length(
                        nx_molecule, target_index, neighbour_index
                    )
                    for target_index in target_indices
                )
                for atom_index, neighbour_index in atoms_to_add
            )
        )

        if len(path_lengths_1) == 0 and len(path_lengths_2) == 0:
            return None

        reverse = False

        min_path_length_1 = min(path_lengths_1)
        min_path_length_2 = min(path_lengths_2)

        if min_path_length_1 < min_path_length_2:
            sort_by = path_lengths_1
        elif min_path_length_2 < min_path_length_1:
            sort_by = path_lengths_2

        else:

            # If there are multiple neighbouring atoms the same path length away
            # from the target bond fall back to sorting by the WBO.
            map_atoms_to_add = [
                map_tuple
                for map_tuple, *path_length_tuple in zip(
                    map_atoms_to_add, path_lengths_1, path_lengths_2
                )
                if min_path_length_1 in path_length_tuple
            ]

            sort_by = [
                molecule.get_bond_between(
                    get_atom_index(molecule, neighbour_bond[0]),
                    get_atom_index(molecule, neighbour_bond[1]),
                ).fractional_bond_order
                for _, neighbour_bond in map_atoms_to_add
            ]

            reverse = True

        sorted_atoms = [
            a for _, a in sorted(zip(sort_by, map_atoms_to_add), reverse=reverse)
        ]

        return None if len(sorted_atoms) == 0 else sorted_atoms[0]