def openff_molecule_from_networkx(nx_graph: networkx.Graph) -> Molecule: """Attempts to create an OpenFF molecule from a networkx graph representation. Notes: This method will strip all stereochemistry and aromaticity information. Args: nx_graph: The graph representation. Returns: The OpenFF molecule object. """ molecule = Molecule() for node_index in nx_graph.nodes: node = nx_graph.nodes[node_index] molecule.add_atom( Element.getBySymbol(node["element"]).atomic_number, node["formal_charge"], False, ) for atom_index_a, atom_index_b in nx_graph.edges: molecule.add_bond( atom_index_a, atom_index_b, nx_graph[atom_index_a][atom_index_b]["bond_order"], False, ) return molecule
def split_openff_molecule(molecule: Molecule) -> List[Molecule]: """ For a gievn openff molecule split it into its component parts if it is actually a multi-component system. Args: molecule: The openff.toolkit.topology.Molecule which should be split. """ sub_graphs = list(nx.connected_components(molecule.to_networkx())) if len(sub_graphs) == 1: return [ molecule, ] component_molecules = [] for sub_graph in sub_graphs: # map the old index to the new one index_mapping = {} comp_mol = Molecule() for atom in sub_graph: new_index = comp_mol.add_atom(**molecule.atoms[atom].to_dict()) index_mapping[atom] = new_index for bond in molecule.bonds: if bond.atom1_index in sub_graph and bond.atom2_index in sub_graph: bond_data = { "atom1": comp_mol.atoms[index_mapping[bond.atom1_index]], "atom2": comp_mol.atoms[index_mapping[bond.atom2_index]], "bond_order": bond.bond_order, "stereochemistry": bond.stereochemistry, "is_aromatic": bond.is_aromatic, "fractional_bond_order": bond.fractional_bond_order, } comp_mol.add_bond(**bond_data) # move the conformers if molecule.n_conformers != 0: for conformer in molecule.conformers: new_conformer = np.zeros((comp_mol.n_atoms, 3)) for i in sub_graph: new_conformer[index_mapping[i]] = conformer[i] comp_mol.add_conformer(new_conformer * unit.angstrom) component_molecules.append(comp_mol) return component_molecules
def from_parmed(cls) -> "System": from openff.system.components.system import System out = System() if cls.positions: out.positions = np.asarray(cls.positions._value) * unit.angstrom if any(cls.box[3:] != 3 * [90.0]): from openff.system.exceptions import UnsupportedBoxError raise UnsupportedBoxError(f"Found box with angles {cls.box[3:]}. Only" "rectangular boxes are currently supported.") out.box = cls.box[:3] * unit.angstrom from openff.toolkit.topology import Molecule, Topology top = Topology() for res in cls.residues: mol = Molecule() mol.name = res.name for atom in res.atoms: mol.add_atom(atomic_number=atom.atomic_number, formal_charge=0, is_aromatic=False) for atom in res.atoms: for bond in atom.bonds: try: mol.add_bond( atom1=bond.atom1.idx, atom2=bond.atom2.idx, bond_order=int(bond.order), is_aromatic=False, ) # TODO: Use a custom exception after # https://github.com/openforcefield/openff-toolkit/issues/771 except Exception as e: if "Bond already exists" in str(e): pass else: raise e top.add_molecule(mol) out.topology = top from openff.system.components.smirnoff import ( ElectrostaticsMetaHandler, SMIRNOFFAngleHandler, SMIRNOFFBondHandler, SMIRNOFFImproperTorsionHandler, SMIRNOFFProperTorsionHandler, SMIRNOFFvdWHandler, ) vdw_handler = SMIRNOFFvdWHandler() coul_handler = ElectrostaticsMetaHandler() for atom in cls.atoms: atom_idx = atom.idx sigma = atom.sigma * unit.angstrom epsilon = atom.epsilon * kcal_mol charge = atom.charge * unit.elementary_charge top_key = TopologyKey(atom_indices=(atom_idx, )) pot_key = PotentialKey(id=str(atom_idx)) pot = Potential(parameters={"sigma": sigma, "epsilon": epsilon}) vdw_handler.slot_map.update({top_key: pot_key}) vdw_handler.potentials.update({pot_key: pot}) coul_handler.charges.update({top_key: charge}) bond_handler = SMIRNOFFBondHandler() for bond in cls.bonds: atom1 = bond.atom1 atom2 = bond.atom2 k = bond.type.k * kcal_mol_a2 length = bond.type.req * unit.angstrom top_key = TopologyKey(atom_indices=(atom1.idx, atom2.idx)) pot_key = PotentialKey(id=f"{atom1.idx}-{atom2.idx}") pot = Potential(parameters={"k": k * 2, "length": length}) bond_handler.slot_map.update({top_key: pot_key}) bond_handler.potentials.update({pot_key: pot}) out.handlers.update({"vdW": vdw_handler}) out.handlers.update({"Electrostatics": coul_handler}) # type: ignore[dict-item] out.handlers.update({"Bonds": bond_handler}) angle_handler = SMIRNOFFAngleHandler() for angle in cls.angles: atom1 = angle.atom1 atom2 = angle.atom2 atom3 = angle.atom3 k = angle.type.k * kcal_mol_rad2 theta = angle.type.theteq * unit.degree top_key = TopologyKey(atom_indices=(atom1.idx, atom2.idx, atom3.idx)) pot_key = PotentialKey(id=f"{atom1.idx}-{atom2.idx}-{atom3.idx}") pot = Potential(parameters={"k": k * 2, "angle": theta}) angle_handler.slot_map.update({top_key: pot_key}) angle_handler.potentials.update({pot_key: pot}) proper_torsion_handler = SMIRNOFFProperTorsionHandler() improper_torsion_handler = SMIRNOFFImproperTorsionHandler() for dihedral in cls.dihedrals: atom1 = dihedral.atom1 atom2 = dihedral.atom2 atom3 = dihedral.atom3 atom4 = dihedral.atom4 k = dihedral.type.phi_k * kcal_mol_rad2 periodicity = dihedral.type.per * unit.dimensionless phase = dihedral.type.phase * unit.degree if dihedral.improper: # ParmEd stores the central atom _third_ (AMBER style) # SMIRNOFF stores the central atom _second_ # https://parmed.github.io/ParmEd/html/topobj/parmed.topologyobjects.Dihedral.html#parmed-topologyobjects-dihedral # https://open-forcefield-toolkit.readthedocs.io/en/latest/smirnoff.html#impropertorsions top_key = TopologyKey( atom_indices=(atom1.idx, atom2.idx, atom2.idx, atom4.idx), mult=1, ) pot_key = PotentialKey( id=f"{atom1.idx}-{atom3.idx}-{atom2.idx}-{atom4.idx}", mult=1, ) pot = Potential(parameters={ "k": k, "periodicity": periodicity, "phase": phase }) while pot_key in improper_torsion_handler.potentials: pot_key.mult += 1 # type: ignore[operator] top_key.mult += 1 # type: ignore[operator] improper_torsion_handler.slot_map.update({top_key: pot_key}) improper_torsion_handler.potentials.update({pot_key: pot}) else: top_key = TopologyKey( atom_indices=(atom1.idx, atom2.idx, atom3.idx, atom4.idx), mult=1, ) pot_key = PotentialKey( id=f"{atom1.idx}-{atom2.idx}-{atom3.idx}-{atom4.idx}", mult=1, ) pot = Potential(parameters={ "k": k, "periodicity": periodicity, "phase": phase }) while pot_key in proper_torsion_handler.potentials: pot_key.mult += 1 # type: ignore[operator] top_key.mult += 1 # type: ignore[operator] proper_torsion_handler.slot_map.update({top_key: pot_key}) proper_torsion_handler.potentials.update({pot_key: pot}) out.handlers.update({"Electrostatics": coul_handler}) # type: ignore[dict-item] out.handlers.update({"Bonds": bond_handler}) out.handlers.update({"Angles": angle_handler}) out.handlers.update({"ProperTorsions": proper_torsion_handler}) return out
def _from_parmed(cls, structure) -> "Interchange": import parmed as pmd out = cls() if structure.positions: out.positions = np.asarray(structure.positions._value) * unit.angstrom if structure.box is not None: if any(structure.box[3:] != 3 * [90.0]): raise UnsupportedBoxError( f"Found box with angles {structure.box[3:]}. Only" "rectangular boxes are currently supported.") out.box = structure.box[:3] * unit.angstrom from openff.toolkit.topology import Molecule from openff.interchange.components.mdtraj import OFFBioTop if structure.topology is not None: mdtop = md.Topology.from_openmm( structure.topology) # type: ignore[attr-defined] top = OFFBioTop(mdtop=mdtop) out.topology = top else: # TODO: Remove this case # This code should not be reached, since a pathway # OpenFF -> OpenMM -> MDTraj already exists mdtop = md.Topology() # type: ignore[attr-defined] main_chain = md.core.topology.Chain( index=0, topology=mdtop) # type: ignore[attr-defined] top = OFFBioTop(mdtop=None) # There is no way to tell if ParmEd residues are connected (cannot be processed # as separate OFFMols) or disconnected (can be). For now, will have to accept the # inefficiency of putting everything into on OFFMol ... mol = Molecule() mol.name = getattr(structure, "name", "Mol") for res in structure.residues: # ... however, MDTraj's Topology class only stores residues, not molecules, # so this should roughly match up with ParmEd this_res = md.core.topology.Residue( # type: ignore[attr-defined] name=res.name, index=res.idx, chain=main_chain, resSeq=0, ) for atom in res.atoms: mol.add_atom(atomic_number=atom.atomic_number, formal_charge=0, is_aromatic=False) mdtop.add_atom( name=atom.name, element=md.element.Element.getByAtomicNumber( atom.element), # type: ignore[attr-defined] residue=this_res, ) main_chain._residues.append(this_res) for res in structure.residues: for atom in res.atoms: for bond in atom.bonds: try: mol.add_bond( atom1=bond.atom1.idx, atom2=bond.atom2.idx, bond_order=int(bond.order), is_aromatic=False, ) # TODO: Use a custom exception after # https://github.com/openforcefield/openff-toolkit/issues/771 except Exception as e: if "Bond already exists" in str(e): pass else: raise e mdtop.add_bond( atom1=mdtop.atom(bond.atom1.idx), atom2=mdtop.atom(bond.atom2.idx), order=int(bond.order) if bond.order is not None else None, ) # Topology.add_molecule requires a safe .to_smiles() call, so instead # do a dangerous molecule addition ref_mol = FrozenMolecule(mol) # This doesn't work because molecule hashing requires valid SMILES # top._reference_molecule_to_topology_molecules[ref_mol] = [] # so just tack it on for now top._reference_mm_molecule = ref_mol top_mol = TopologyMolecule(reference_molecule=ref_mol, topology=top) top._topology_molecules.append(top_mol) # top._reference_molecule_to_topology_molecules[ref_mol].append(top_mol) mdtop._chains.append(main_chain) out.topology = top from openff.interchange.components.smirnoff import ( SMIRNOFFAngleHandler, SMIRNOFFBondHandler, SMIRNOFFElectrostaticsHandler, SMIRNOFFImproperTorsionHandler, SMIRNOFFProperTorsionHandler, SMIRNOFFvdWHandler, ) vdw_handler = SMIRNOFFvdWHandler() coul_handler = SMIRNOFFElectrostaticsHandler(method="pme") for atom in structure.atoms: atom_idx = atom.idx sigma = atom.sigma * unit.angstrom epsilon = atom.epsilon * kcal_mol charge = atom.charge * unit.elementary_charge top_key = TopologyKey(atom_indices=(atom_idx, )) pot_key = PotentialKey(id=str(atom_idx)) pot = Potential(parameters={"sigma": sigma, "epsilon": epsilon}) vdw_handler.slot_map.update({top_key: pot_key}) vdw_handler.potentials.update({pot_key: pot}) coul_handler.slot_map.update({top_key: pot_key}) coul_handler.potentials.update( {pot_key: Potential(parameters={"charge": charge})}) bond_handler = SMIRNOFFBondHandler() for bond in structure.bonds: atom1 = bond.atom1 atom2 = bond.atom2 k = bond.type.k * kcal_mol_a2 length = bond.type.req * unit.angstrom top_key = TopologyKey(atom_indices=(atom1.idx, atom2.idx)) pot_key = PotentialKey(id=f"{atom1.idx}-{atom2.idx}") pot = Potential(parameters={"k": k * 2, "length": length}) bond_handler.slot_map.update({top_key: pot_key}) bond_handler.potentials.update({pot_key: pot}) out.handlers.update({"vdW": vdw_handler}) out.handlers.update({"Electrostatics": coul_handler}) out.handlers.update({"Bonds": bond_handler}) angle_handler = SMIRNOFFAngleHandler() for angle in structure.angles: atom1 = angle.atom1 atom2 = angle.atom2 atom3 = angle.atom3 k = angle.type.k * kcal_mol_rad2 theta = angle.type.theteq * unit.degree top_key = TopologyKey(atom_indices=(atom1.idx, atom2.idx, atom3.idx)) pot_key = PotentialKey(id=f"{atom1.idx}-{atom2.idx}-{atom3.idx}") pot = Potential(parameters={"k": k * 2, "angle": theta}) angle_handler.slot_map.update({top_key: pot_key}) angle_handler.potentials.update({pot_key: pot}) proper_torsion_handler = SMIRNOFFProperTorsionHandler() improper_torsion_handler = SMIRNOFFImproperTorsionHandler() for dihedral in structure.dihedrals: if isinstance(dihedral.type, pmd.DihedralType): if dihedral.improper: _process_single_dihedral(dihedral, dihedral.type, improper_torsion_handler, 0) else: _process_single_dihedral(dihedral, dihedral.type, proper_torsion_handler, 0) elif isinstance(dihedral.type, pmd.DihedralTypeList): for dih_idx, dihedral_type in enumerate(dihedral.type): if dihedral.improper: _process_single_dihedral(dihedral, dihedral_type, improper_torsion_handler, dih_idx) else: _process_single_dihedral( dihedral, dihedral_type, proper_torsion_handler, dih_idx, ) out.handlers.update({"Electrostatics": coul_handler}) out.handlers.update({"Bonds": bond_handler}) out.handlers.update({"Angles": angle_handler}) out.handlers.update({"ProperTorsions": proper_torsion_handler}) return out