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
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]