def test_atom_ordering(self): """Test that atom indices in bonds are ordered consistently between the slot map and topology""" import foyer from openff.interchange.components.interchange import Interchange from openff.interchange.drivers import ( get_gromacs_energies, get_lammps_energies, get_openmm_energies, ) oplsaa = foyer.forcefields.load_OPLSAA() benzene = Molecule.from_file(get_test_file_path("benzene.sdf")) benzene.name = "BENZ" biotop = OFFBioTop.from_molecules(benzene) biotop.mdtop = md.Topology.from_openmm(biotop.to_openmm()) out = Interchange.from_foyer(force_field=oplsaa, topology=biotop) out.box = [4, 4, 4] out.positions = benzene.conformers[0] # Violates OPLS-AA, but the point here is just to make sure everything runs out["vdW"].mixing_rule = "lorentz-berthelot" get_gromacs_energies(out) get_openmm_energies(out) get_lammps_energies(out)
def test_electrostatics_charge_increments(self): top = OFFBioTop.from_molecules( Molecule.from_mapped_smiles("[Cl:1][H:2]")) charge_increment_handler = ChargeIncrementModelHandler(version=0.3) charge_increment_handler.add_parameter({ "smirks": "[#17:1]-[#1:2]", "charge_increment1": 0.1 * simtk_unit.elementary_charge, "charge_increment2": -0.1 * simtk_unit.elementary_charge, }) parameter_handlers = [ ElectrostaticsHandler(version=0.3), charge_increment_handler, ] electrostatics_handler = SMIRNOFFElectrostaticsHandler._from_toolkit( parameter_handlers, top) # AM1-Mulliken charges are [-0.168, 0.168], increments are [0.1, -0.1], # sum is [-0.068, 0.068] np.testing.assert_allclose( [ charge.m_as(unit.e) for charge in electrostatics_handler.charges.values() ], [-0.068, 0.068], )
def interchanges_from_path(molecule_path): molecule_or_molecules = Molecule.from_file(molecule_path) mol_name = Path(molecule_path).stem[0:4].upper() if isinstance(molecule_or_molecules, list): for idx, mol in enumerate(molecule_or_molecules): mol.name = f"{mol_name}_{idx}" else: molecule_or_molecules.name = mol_name off_bio_top = OFFBioTop.from_molecules(molecule_or_molecules) off_bio_top.mdtop = md.Topology.from_openmm(off_bio_top.to_openmm()) openff_interchange = Interchange.from_foyer(off_bio_top, oplsaa) if isinstance(molecule_or_molecules, list): openff_interchange.positions = np.vstack( tuple( molecule.conformers[0].value_in_unit(omm_unit.nanometer) for molecule in molecule_or_molecules ) ) else: openff_interchange.positions = molecule_or_molecules.conformers[ 0 ].value_in_unit(omm_unit.nanometer) openff_interchange.box = [4, 4, 4] parmed_struct = pmd.openmm.load_topology(off_bio_top.to_openmm()) parmed_struct.positions = openff_interchange.positions.m_as(unit.angstrom) parmed_struct.box = [40, 40, 40, 90, 90, 90] return openff_interchange, parmed_struct
def test_bond_potential_handler(self): top = OFFBioTop.from_molecules(Molecule.from_smiles("O=O")) bond_handler = BondHandler(version=0.3) bond_parameter = BondHandler.BondType( smirks="[*:1]~[*:2]", k=1.5 * simtk_unit.kilocalorie_per_mole / simtk_unit.angstrom**2, length=1.5 * simtk_unit.angstrom, id="b1000", ) bond_handler.add_parameter(bond_parameter.to_dict()) from openff.toolkit.typing.engines.smirnoff import ForceField forcefield = ForceField() forcefield.register_parameter_handler(bond_handler) bond_potentials = SMIRNOFFBondHandler._from_toolkit( parameter_handler=forcefield["Bonds"], topology=top, ) top_key = TopologyKey(atom_indices=(0, 1)) pot_key = bond_potentials.slot_map[top_key] assert pot_key.associated_handler == "Bonds" pot = bond_potentials.potentials[pot_key] kcal_mol_a2 = unit.Unit("kilocalorie / (angstrom ** 2 * mole)") assert pot.parameters["k"].to(kcal_mol_a2).magnitude == pytest.approx( 1.5)
def test_residues(): pdb = app.PDBFile(get_test_file_path("ALA_GLY/ALA_GLY.pdb")) traj = md.load(get_test_file_path("ALA_GLY/ALA_GLY.pdb")) mol = Molecule(get_test_file_path("ALA_GLY/ALA_GLY.sdf"), file_format="sdf") top = OFFBioTop.from_openmm(pdb.topology, unique_molecules=[mol]) top.mdtop = traj.top assert top.n_topology_atoms == 29 assert top.mdtop.n_residues == 4 assert [r.name for r in top.mdtop.residues] == ["ACE", "ALA", "GLY", "NME"] ff = ForceField("openff-1.3.0.offxml") off_sys = Interchange.from_smirnoff(ff, top) # Assign positions and box vectors in order to run MM off_sys.positions = pdb.positions off_sys.box = [4.8, 4.8, 4.8] # Just ensure that a single-point energy can be obtained without error get_openmm_energies(off_sys) assert len(top.mdtop.select("resname ALA")) == 10 assert [*off_sys.topology.mdtop.residues][-1].n_atoms == 6
def test_from_openmm_pdbfile(self, argon_ff, argon_top): pdb_file_path = get_test_file_path("10-argons.pdb") pdbfile = openmm.app.PDBFile(pdb_file_path) mol = Molecule.from_smiles("[#18]") top = OFFBioTop.from_openmm(pdbfile.topology, unique_molecules=[mol]) top.mdtop = md.Topology.from_openmm(top.to_openmm()) box = pdbfile.topology.getPeriodicBoxVectors() box = box.value_in_unit(nm) * unit.nanometer out = Interchange.from_smirnoff(argon_ff, top) out.box = box out.positions = pdbfile.getPositions() assert np.allclose( out.positions.to(unit.nanometer).magnitude, pdbfile.getPositions().value_in_unit(nm), ) get_openmm_energies(out, hard_cutoff=True).compare( _get_openmm_energies( omm_sys=argon_ff.create_openmm_system(top), box_vectors=pdbfile.topology.getPeriodicBoxVectors(), positions=pdbfile.getPositions(), hard_cutoff=True, ) )
def top_from_smiles( smiles: str, n_molecules: int = 1, ) -> OFFBioTop: """Create a gas phase OpenFF Topology from a single-molecule SMILES Parameters ---------- smiles : str The SMILES of the input molecule n_molecules : int, optional, default = 1 The number of copies of the SMILES molecule from which to compose a topology Returns ------- top : opennff.interchange.components.mdtraj.OFFBioTop A single-molecule, gas phase-like topology """ mol = Molecule.from_smiles(smiles) mol.generate_conformers(n_conformers=1) top = OFFBioTop.from_molecules(n_molecules * [mol]) top.mdtop = md.Topology.from_openmm( top.to_openmm()) # type: ignore[attr-defined] # Add dummy box vectors # TODO: Revisit if/after Topology.is_periodic top.box_vectors = np.eye(3) * 10 * simtk_unit.nanometer return top
def test_electrostatics_library_charges(self): top = OFFBioTop.from_molecules(Molecule.from_smiles("C")) library_charge_handler = LibraryChargeHandler(version=0.3) library_charge_handler.add_parameter({ "smirks": "[#6X4:1]-[#1:2]", "charge1": -0.1 * simtk_unit.elementary_charge, "charge2": 0.025 * simtk_unit.elementary_charge, }) parameter_handlers = [ ElectrostaticsHandler(version=0.3), library_charge_handler, ] electrostatics_handler = SMIRNOFFElectrostaticsHandler._from_toolkit( parameter_handlers, top) np.testing.assert_allclose( [ charge.m_as(unit.e) for charge in electrostatics_handler.charges.values() ], [-0.1, 0.025, 0.025, 0.025, 0.025], )
def test_angle_potential_handler(self): top = OFFBioTop.from_molecules(Molecule.from_smiles("CCC")) angle_handler = AngleHandler(version=0.3) angle_parameter = AngleHandler.AngleType( smirks="[*:1]~[*:2]~[*:3]", k=2.5 * simtk_unit.kilocalorie_per_mole / simtk_unit.radian**2, angle=100 * simtk_unit.degree, id="b1000", ) angle_handler.add_parameter(angle_parameter.to_dict()) forcefield = ForceField() forcefield.register_parameter_handler(angle_handler) angle_potentials = SMIRNOFFAngleHandler._from_toolkit( parameter_handler=forcefield["Angles"], topology=top, ) top_key = TopologyKey(atom_indices=(0, 1, 2)) pot_key = angle_potentials.slot_map[top_key] assert pot_key.associated_handler == "Angles" pot = angle_potentials.potentials[pot_key] kcal_mol_rad2 = unit.Unit("kilocalorie / (mole * radian ** 2)") assert pot.parameters["k"].to( kcal_mol_rad2).magnitude == pytest.approx(2.5)
def from_openmm(topology=None, system=None, positions=None, box_vectors=None): from openff.interchange.components.interchange import Interchange openff_sys = Interchange() if system: for force in system.getForces(): if isinstance(force, openmm.NonbondedForce): vdw, coul = _convert_nonbonded_force(force) openff_sys.add_handler(handler_name="vdW", handler=vdw) openff_sys.add_handler(handler_name="Electrostatics", handler=coul) if isinstance(force, openmm.HarmonicBondForce): bond_handler = _convert_harmonic_bond_force(force) openff_sys.add_handler(handler_name="Bonds", handler=bond_handler) if isinstance(force, openmm.HarmonicAngleForce): angle_handler = _convert_harmonic_angle_force(force) openff_sys.add_handler(handler_name="Angles", handler=angle_handler) if isinstance(force, openmm.PeriodicTorsionForce): proper_torsion_handler = _convert_periodic_torsion_force(force) openff_sys.add_handler(handler_name="PeriodicTorsions", handler=proper_torsion_handler) if topology: import mdtraj as md from openff.interchange.components.mdtraj import OFFBioTop mdtop = md.Topology.from_openmm(topology) top = OFFBioTop() top.mdtop = mdtop openff_sys.topoology = top if positions: openff_sys.positions = positions if box_vectors: openff_sys.box = box_vectors return openff_sys
def test_argon_buck(self): """Test that Buckingham potentials are supported and can be exported""" from openff.interchange.components.smirnoff import SMIRNOFFElectrostaticsHandler mol = Molecule.from_smiles("[#18]") top = OFFBioTop.from_molecules([mol, mol]) top.mdtop = md.Topology.from_openmm(top.to_openmm()) # http://www.sklogwiki.org/SklogWiki/index.php/Argon#Buckingham_potential erg_mol = unit.erg / unit.mol * float(unit.avogadro_number) A = 1.69e-8 * erg_mol B = 1 / (0.273 * unit.angstrom) C = 102e-12 * erg_mol * unit.angstrom**6 r = 0.3 * unit.nanometer buck = BuckinghamvdWHandler() coul = SMIRNOFFElectrostaticsHandler(method="pme") pot_key = PotentialKey(id="[#18]") pot = Potential(parameters={"A": A, "B": B, "C": C}) for atom in top.mdtop.atoms: top_key = TopologyKey(atom_indices=(atom.index, )) buck.slot_map.update({top_key: pot_key}) coul.slot_map.update({top_key: pot_key}) coul.potentials.update({ pot_key: Potential(parameters={"charge": 0 * unit.elementary_charge}) }) buck.potentials[pot_key] = pot out = Interchange() out.handlers["Buckingham-6"] = buck out.handlers["Electrostatics"] = coul out.topology = top out.box = [10, 10, 10] * unit.nanometer out.positions = [[0, 0, 0], [0.3, 0, 0]] * unit.nanometer out.to_gro("out.gro", writer="internal") out.to_top("out.top", writer="internal") omm_energies = get_openmm_energies(out) by_hand = A * exp(-B * r) - C * r**-6 resid = omm_energies.energies["Nonbonded"] - by_hand assert resid < 1e-5 * unit.kilojoule / unit.mol # TODO: Add back comparison to GROMACS energies once GROMACS 2020+ # supports Buckingham potentials with pytest.raises(GMXMdrunError): get_gromacs_energies(out, mdp="cutoff_buck")
def test_to_lammps_single_mols(mol, n_mols): """ Test that Interchange.to_openmm Interchange.to_lammps report sufficiently similar energies. TODO: Tighten tolerances TODO: Test periodic and non-periodic """ parsley = ForceField("openff_unconstrained-1.0.0.offxml") mol = Molecule.from_smiles(mol) mol.generate_conformers(n_conformers=1) top = OFFBioTop.from_molecules(n_mols * [mol]) top.mdtop = md.Topology.from_openmm(top.to_openmm()) mol.conformers[0] -= np.min(mol.conformers) * omm_unit.angstrom top.box_vectors = np.eye(3) * np.asarray([10, 10, 10]) * omm_unit.nanometer if n_mols == 1: positions = mol.conformers[0] elif n_mols == 2: positions = np.vstack( [mol.conformers[0], mol.conformers[0] + 3 * omm_unit.nanometer]) positions = positions * omm_unit.angstrom openff_sys = Interchange.from_smirnoff(parsley, top) openff_sys.positions = positions.value_in_unit(omm_unit.nanometer) openff_sys.box = top.box_vectors reference = get_openmm_energies( off_sys=openff_sys, round_positions=3, ) lmp_energies = get_lammps_energies( off_sys=openff_sys, round_positions=3, ) _write_lammps_input( off_sys=openff_sys, file_name="tmp.in", ) lmp_energies.compare( reference, custom_tolerances={ "Nonbonded": 100 * omm_unit.kilojoule_per_mole, "Electrostatics": 100 * omm_unit.kilojoule_per_mole, "vdW": 100 * omm_unit.kilojoule_per_mole, "Torsion": 3e-5 * omm_unit.kilojoule_per_mole, }, )
def test_unsupported_mixing_rule(): molecules = [create_ethanol()] pdbfile = app.PDBFile(get_data_file_path("systems/test_systems/1_ethanol.pdb")) topology = OFFBioTop.from_openmm(pdbfile.topology, unique_molecules=molecules) topology.mdtop = md.Topology.from_openmm(topology.to_openmm()) forcefield = ForceField("test_forcefields/test_forcefield.offxml") openff_sys = Interchange.from_smirnoff(force_field=forcefield, topology=topology) openff_sys["vdW"].mixing_rule = "geometric" with pytest.raises(UnsupportedExportError, match="default NonbondedForce"): openff_sys.to_openmm(combine_nonbonded_forces=True)
def oplsaa_interchange_ethanol(self, oplsaa): molecule = Molecule.from_file( get_test_files_dir_path("foyer_test_molecules") + "/ethanol.sdf" ) molecule.name = "ETH" top = OFFBioTop.from_molecules(molecule) top.mdtop = md.Topology.from_openmm(top.to_openmm()) oplsaa = foyer.Forcefield(name="oplsaa") interchange = Interchange.from_foyer(topology=top, force_field=oplsaa) interchange.positions = molecule.conformers[0].value_in_unit(omm_unit.nanometer) interchange.box = [4, 4, 4] return interchange
def test_residue_names_in_gro_file(self): """Test that residue names > 5 characters don't break .gro file output""" benzene = Molecule.from_file(get_test_file_path("benzene.sdf")) benzene.name = "supercalifragilisticexpialidocious" top = OFFBioTop.from_molecules(benzene) top.mdtop = md.Topology.from_openmm(top.to_openmm()) # Populate an entire interchange because ... force_field = ForceField("openff-1.0.0.offxml") out = Interchange.from_smirnoff(force_field, top) out.box = [4, 4, 4] out.positions = benzene.conformers[0] # ... the easiest way to check the validity of the files # is to see if GROMACS can run them get_gromacs_energies(out)
def test_electrostatics_am1_handler(self): top = OFFBioTop.from_molecules(Molecule.from_smiles("C")) parameter_handlers = [ ElectrostaticsHandler(version=0.3), ToolkitAM1BCCHandler(version=0.3), ] electrostatics_handler = SMIRNOFFElectrostaticsHandler._from_toolkit( parameter_handlers, top) np.testing.assert_allclose( [ charge.m_as(unit.e) for charge in electrostatics_handler.charges.values() ], [-0.1088, 0.0267, 0.0267, 0.0267, 0.0267], )
def test_from_parsley(self): force_field = ForceField("openff-1.3.0.offxml") top = OFFBioTop.from_molecules( [Molecule.from_smiles("CCO"), Molecule.from_smiles("CC")]) out = Interchange.from_smirnoff(force_field, top) assert "Constraints" in out.handlers.keys() assert "Bonds" in out.handlers.keys() assert "Angles" in out.handlers.keys() assert "ProperTorsions" in out.handlers.keys() assert "vdW" in out.handlers.keys() assert type(out.topology) == OFFBioTop assert type(out.topology) != Topology assert isinstance(out.topology, Topology)
def test_basic_combination(self, parsley_unconstrained): """Test basic use of Interchange.__add__() based on the README example""" mol = Molecule.from_smiles("C") mol.generate_conformers(n_conformers=1) top = OFFBioTop.from_molecules([mol]) top.mdtop = md.Topology.from_openmm(top.to_openmm()) openff_sys = Interchange.from_smirnoff(parsley_unconstrained, top) openff_sys.box = [4, 4, 4] * np.eye(3) openff_sys.positions = mol.conformers[0]._value / 10.0 # Copy and translate atoms by [1, 1, 1] other = Interchange() other._inner_data = deepcopy(openff_sys._inner_data) other.positions += 1.0 * unit.nanometer combined = openff_sys + other # Just see if it can be converted into OpenMM and run get_openmm_energies(combined)
def test_from_toolkit_packmol_boxes(self, pdb_path, unique_molecules): """ Test loading some pre-prepared PACKMOL-generated systems. These use PDB files already prepared in the toolkit because PDB files are a pain. """ ff = ForceField("openff-1.0.0.offxml") pdb_file_path = get_data_file_path("systems/packmol_boxes/" + pdb_path) pdbfile = openmm.app.PDBFile(pdb_file_path) top = OFFBioTop.from_openmm( pdbfile.topology, unique_molecules=unique_molecules, ) top.mdtop = md.Topology.from_openmm(top.to_openmm()) box = pdbfile.topology.getPeriodicBoxVectors() box = box.value_in_unit(nm) * unit.nanometer out = Interchange.from_smirnoff(ff, top) out.box = box out.positions = pdbfile.getPositions() assert np.allclose( out.positions.to(unit.nanometer).magnitude, pdbfile.getPositions().value_in_unit(nm), ) get_openmm_energies( out, hard_cutoff=True, combine_nonbonded_forces=True, ).compare( _get_openmm_energies( omm_sys=ff.create_openmm_system(top), box_vectors=pdbfile.topology.getPeriodicBoxVectors(), positions=pdbfile.getPositions(), hard_cutoff=True, ) )
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
def ammonia_top(self): """Fixture that builds a simple ammonia topology""" mol = Molecule.from_smiles("N") top = OFFBioTop.from_molecules(4 * [mol]) top.mdtop = md.Topology.from_openmm(top.to_openmm()) return top
def from_smirnoff( cls, force_field: ForceField, topology: OFFBioTop, box=None, ) -> "Interchange": """Creates a new object by parameterizing a topology using the specified SMIRNOFF force field. Parameters ---------- force_field The force field to parameterize the topology with. topology The topology to parameterize. box The box vectors associated with the interchange. Examples -------- Generate an Interchange object from a single-molecule (OpenFF) topology and OpenFF 1.0.0 "Parsley" .. code-block:: pycon >>> from openff.interchange.components.interchange import Interchange >>> from openff.interchange.components.mdtraj import OFFBioTop >>> from openff.toolkit.topology import Molecule >>> from openff.toolkit.typing.engines.smirnoff import ForceField >>> import mdtraj as md >>> mol = Molecule.from_smiles("CC") >>> mol.generate_conformers(n_conformers=1) >>> top = OFFBioTop.from_molecules([mol]) >>> top.mdtop = md.Topology.from_openmm(top.to_openmm()) >>> parsley = ForceField("openff-1.0.0.offxml") >>> interchange = Interchange.from_smirnoff(topology=top, force_field=parsley) >>> interchange Interchange with 8 atoms, non-periodic topology """ sys_out = Interchange() cls._check_supported_handlers(force_field) if isinstance(topology, OFFBioTop): sys_out.topology = topology elif isinstance(topology, Topology): sys_out.topology = OFFBioTop(other=topology) sys_out.topology.mdtop = md.Topology.from_openmm( topology.to_openmm()) else: raise InvalidTopologyError( "Could not process topology argument, expected Topology or OFFBioTop. " f"Found object of type {type(topology)}.") parameter_handlers_by_type = { force_field[parameter_handler_name].__class__: force_field[parameter_handler_name] for parameter_handler_name in force_field.registered_parameter_handlers } if len(parameter_handlers_by_type) != len( force_field.registered_parameter_handlers): raise NotImplementedError( "Only force fields that contain one instance of each parameter handler " "type are currently supported.") for potential_handler_type in SMIRNOFF_POTENTIAL_HANDLERS: parameter_handlers = [ parameter_handlers_by_type[allowed_type] for allowed_type in potential_handler_type.allowed_parameter_handlers() if allowed_type in parameter_handlers_by_type ] if len(parameter_handlers) == 0: continue # TODO: Might be simpler to rework the bond handler to be self-contained and # move back to the constraint handler dealing with the logic (and # depending on the bond handler) if potential_handler_type == SMIRNOFFBondHandler: SMIRNOFFBondHandler.check_supported_parameters( force_field["Bonds"]) potential_handler = SMIRNOFFBondHandler._from_toolkit( parameter_handler=force_field["Bonds"], topology=topology, # constraint_handler=constraint_handler, ) sys_out.handlers.update({"Bonds": potential_handler}) elif potential_handler_type == SMIRNOFFConstraintHandler: bond_handler = force_field._parameter_handlers.get( "Bonds", None) constraint_handler = force_field._parameter_handlers.get( "Constraints", None) if constraint_handler is None: continue constraints = SMIRNOFFConstraintHandler._from_toolkit( parameter_handler=[ val for val in [bond_handler, constraint_handler] if val is not None ], topology=topology, ) sys_out.handlers.update({"Constraints": constraints}) continue elif len(potential_handler_type.allowed_parameter_handlers()) > 1: potential_handler = potential_handler_type._from_toolkit( parameter_handler=parameter_handlers, topology=topology, ) else: potential_handler_type.check_supported_parameters( parameter_handlers[0]) potential_handler = potential_handler_type._from_toolkit( # type: ignore parameter_handler=parameter_handlers[0], topology=topology, ) sys_out.handlers.update( {potential_handler.type: potential_handler}) # `box` argument is only overriden if passed `None` and the input topology # has box vectors if box is None and topology.box_vectors is not None: sys_out.box = topology.box_vectors else: sys_out.box = box return sys_out
def test_packmol_boxes(toolkit_file_path): # TODO: Isolate a set of systems here instead of using toolkit data # TODO: Fix nonbonded energy differences from openff.toolkit.utils import get_data_file_path pdb_file_path = get_data_file_path(toolkit_file_path) pdbfile = openmm.app.PDBFile(pdb_file_path) ethanol = Molecule.from_smiles("CCO") cyclohexane = Molecule.from_smiles("C1CCCCC1") omm_topology = pdbfile.topology off_topology = OFFBioTop.from_openmm( omm_topology, unique_molecules=[ethanol, cyclohexane]) off_topology.mdtop = md.Topology.from_openmm(omm_topology) parsley = ForceField("openff_unconstrained-1.0.0.offxml") off_sys = Interchange.from_smirnoff(parsley, off_topology) off_sys.box = np.asarray( pdbfile.topology.getPeriodicBoxVectors().value_in_unit( simtk_unit.nanometer)) off_sys.positions = pdbfile.positions sys_from_toolkit = parsley.create_openmm_system(off_topology) omm_energies = get_openmm_energies(off_sys, hard_cutoff=True, electrostatics=False) reference = _get_openmm_energies( sys_from_toolkit, off_sys.box, off_sys.positions, hard_cutoff=True, electrostatics=False, ) omm_energies.compare( reference, custom_tolerances={ "Electrostatics": 2e-2 * simtk_unit.kilojoule_per_mole, }, ) # custom_tolerances={"HarmonicBondForce": 1.0} # Compare GROMACS writer and OpenMM export gmx_energies = get_gromacs_energies(off_sys, electrostatics=False) omm_energies_rounded = get_openmm_energies( off_sys, round_positions=8, hard_cutoff=True, electrostatics=False, ) omm_energies_rounded.compare( other=gmx_energies, custom_tolerances={ "Angle": 1e-2 * simtk_unit.kilojoule_per_mole, "Torsion": 1e-2 * simtk_unit.kilojoule_per_mole, "Electrostatics": 3200 * simtk_unit.kilojoule_per_mole, }, )