def test_invalid_topology(self, parsley): """Test that InvalidTopologyError is caught when passing an unsupported topology type to Interchange.from_smirnoff""" top = Molecule.from_smiles("CC").to_topology().to_openmm() with pytest.raises( InvalidTopologyError, match="Could not process topology argument.*openmm.*"): Interchange.from_smirnoff(force_field=parsley, topology=top)
def test_bogus_smirnoff_handler(self, parsley): top = Molecule.from_smiles("CC").to_topology() bogus_parameter_handler = ParameterHandler(version=0.3) bogus_parameter_handler._TAGNAME = "bogus" parsley.register_parameter_handler(bogus_parameter_handler) with pytest.raises(SMIRNOFFHandlersNotImplementedError, match="SMIRNOFF.*bogus"): Interchange.from_smirnoff(force_field=parsley, topology=top)
def test_catch_unassigned_angles(self, parsley, ethanol_top): for param in parsley["Angles"].parameters: param.smirks = "[#99:1]-[#99:2]-[#99:3]" with pytest.raises( UnassignedValenceParameterException, match="AngleHandler was not able to find par", ): Interchange.from_smirnoff(force_field=parsley, topology=ethanol_top)
def test_catch_unassigned_torsions(self, parsley, ethanol_top): for param in parsley["ProperTorsions"].parameters: param.smirks = "[#99:1]-[#99:2]-[#99:3]-[#99:4]" with pytest.raises( UnassignedProperTorsionParameterException, match="- Topology indices [(]5, 0, 1, 6[)]: " r"names and elements [(](H\d+)? H[)], [(](C\d+)? C[)], [(](C\d+)? C[)], [(](H\d+)? H[)],", ): Interchange.from_smirnoff(force_field=parsley, topology=ethanol_top)
def test_catch_unassigned_bonds(self, parsley, ethanol_top): for param in parsley["Bonds"].parameters: param.smirks = "[#99:1]-[#99:2]" parsley.deregister_parameter_handler(parsley["Constraints"]) with pytest.raises( UnassignedValenceParameterException, match="BondHandler was not able to find par", ): Interchange.from_smirnoff(force_field=parsley, topology=ethanol_top)
def test_catch_bond_order_interpolation_bonds(self): from openff.toolkit.tests.test_forcefield import xml_ff_bo forcefield = ForceField( get_data_file_path("test_forcefields/test_forcefield.offxml"), xml_ff_bo, ) top = Molecule.from_smiles("CCO").to_topology() with pytest.raises(SMIRNOFFParameterAttributeNotImplementedError, match="length_bondorder"): Interchange.from_smirnoff(force_field=forcefield, topology=top)
def test_catch_virtual_sites(self): from openff.toolkit.tests.test_forcefield import TestForceFieldVirtualSites forcefield = ForceField( get_data_file_path("test_forcefields/test_forcefield.offxml"), TestForceFieldVirtualSites. xml_ff_virtual_sites_monovalent_match_once, ) top = Molecule.from_smiles("CCO").to_topology() with pytest.raises(SMIRNOFFHandlersNotImplementedError, match="VirtualSites"): Interchange.from_smirnoff(force_field=forcefield, topology=top)
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_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_amber_energy(): """Basic test to see if the amber energy driver is functional""" mol = Molecule.from_smiles("CCO") mol.generate_conformers(n_conformers=1) top = mol.to_topology() top.mdtop = md.Topology.from_openmm(top.to_openmm()) parsley = ForceField("openff_unconstrained-1.0.0.offxml") off_sys = Interchange.from_smirnoff(parsley, top) off_sys.box = [4, 4, 4] off_sys.positions = mol.conformers[0] omm_energies = get_gromacs_energies(off_sys, mdp="cutoff_hbonds") amb_energies = get_amber_energies(off_sys) omm_energies.compare( amb_energies, custom_tolerances={ "Bond": 3.6 * kj_mol, "Angle": 0.2 * kj_mol, "Torsion": 1.9 * kj_mol, "vdW": 1.5 * kj_mol, "Electrostatics": 36.5 * kj_mol, }, )
def test_cutoff_electrostatics(): ion_ff = ForceField(get_test_file_path("ions.offxml")) ions = Topology.from_molecules([ Molecule.from_smiles("[#3]"), Molecule.from_smiles("[#17]"), ]) out = Interchange.from_smirnoff(ion_ff, ions) out.box = [4, 4, 4] * unit.nanometer gmx = [] lmp = [] for d in np.linspace(0.75, 0.95, 5): positions = np.zeros((2, 3)) * unit.nanometer positions[1, 0] = d * unit.nanometer out.positions = positions out["Electrostatics"].method = "cutoff" gmx.append( get_gromacs_energies(out, mdp="auto").energies["Electrostatics"].m) lmp.append( get_lammps_energies(out).energies["Electrostatics"].m_as( unit.kilojoule / unit.mol)) assert np.sum(np.sqrt(np.square(np.asarray(lmp) - np.asarray(gmx)))) < 1e-3
def compare_single_mol_systems(mol, force_field): top = mol.to_topology() top.box_vectors = _box_vectors try: toolkit_sys = force_field.create_openmm_system( top, charge_from_molecules=[mol], ) except ( UnassignedBondParameterException, UnassignedAngleParameterException, UnassignedProperTorsionParameterException, ): pytest.xfail(f"Molecule failed! (missing valence parameters)\t{mol.to_inchi()}") toolkit_energy = _get_openmm_energies( toolkit_sys, box_vectors=_box_vectors, positions=mol.conformers[0] ) openff_sys = Interchange.from_smirnoff(force_field=force_field, topology=top) openff_sys.positions = mol.conformers[0] system_energy = get_openmm_energies(openff_sys, combine_nonbonded_forces=True) toolkit_energy.compare( system_energy, custom_tolerances={ "Bond": 1e-6 * kj_mol, "Angle": 1e-6 * kj_mol, "Torsion": 4e-5 * kj_mol, "Nonbonded": 1e-5 * kj_mol, }, )
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 test_load_prmtop(self): struct = readparm.LoadParm(get_pmd_fn("trx.prmtop")) other_struct = readparm.AmberParm(get_pmd_fn("trx.prmtop")) prmtop = Interchange._from_parmed(struct) other_prmtop = Interchange._from_parmed(other_struct) for handler_key in prmtop.handlers: # TODO: Closer inspection of data assert handler_key in other_prmtop.handlers assert not prmtop.box struct.box = [20, 20, 20, 90, 90, 90] prmtop_converted = Interchange._from_parmed(struct) np.testing.assert_allclose(prmtop_converted.box, np.eye(3) * 2.0 * unit.nanometer)
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_read_box_parm7(self): top = readparm.LoadParm(get_pmd_fn("solv2.parm7")) out = Interchange._from_parmed(top) # pmd.load_file(get_pmd_fn("solv2.rst7"))) # top = readparm.LoadParm(get_pmd_fn("solv2.parm7"), xyz=coords.coordinates) np.testing.assert_allclose(np.diag(out.box.m_as(unit.angstrom)), top.parm_data["BOX_DIMENSIONS"][1:])
def test_openmm_roundtrip(): mol = Molecule.from_smiles("CCO") mol.generate_conformers(n_conformers=1) top = mol.to_topology() omm_top = top.to_openmm() parsley = ForceField("openff_unconstrained-1.0.0.offxml") off_sys = Interchange.from_smirnoff(parsley, top) off_sys.box = [4, 4, 4] off_sys.positions = mol.conformers[0].value_in_unit(simtk_unit.nanometer) omm_sys = off_sys.to_openmm() converted = from_openmm( topology=omm_top, system=omm_sys, ) converted.topology = off_sys.topology converted.box = off_sys.box converted.positions = off_sys.positions get_openmm_energies(off_sys).compare( get_openmm_energies(converted), custom_tolerances={"Nonbonded": 1.5 * simtk_unit.kilojoule_per_mole}, )
def test_gro_file_all_zero_positions(self, parsley): top = Topology.from_molecules(Molecule.from_smiles("CC")) zero_positions = Interchange.from_smirnoff(force_field=parsley, topology=top) zero_positions.positions = np.zeros( (top.n_topology_atoms, 3)) * unit.nanometer with pytest.warns(UserWarning, match="seem to all be zero"): zero_positions.to_gro("foo.gro")
def test_openmm_nonbonded_methods(inputs): """See test_nonbonded_method_resolution in openff/toolkit/tests/test_forcefield.py""" vdw_method = inputs["vdw_method"] electrostatics_method = inputs["electrostatics_method"] periodic = inputs["periodic"] result = inputs["result"] molecules = [create_ethanol()] forcefield = ForceField("test_forcefields/test_forcefield.offxml") pdbfile = app.PDBFile(get_data_file_path("systems/test_systems/1_ethanol.pdb")) topology = Topology.from_openmm(pdbfile.topology, unique_molecules=molecules) if not periodic: topology.box_vectors = None if type(result) == int: nonbonded_method = result # The method is validated and may raise an exception if it's not supported. forcefield.get_parameter_handler("vdW", {}).method = vdw_method forcefield.get_parameter_handler( "Electrostatics", {} ).method = electrostatics_method openff_interchange = Interchange.from_smirnoff( force_field=forcefield, topology=topology ) openmm_system = openff_interchange.to_openmm(combine_nonbonded_forces=True) for force in openmm_system.getForces(): if isinstance(force, openmm.NonbondedForce): assert force.getNonbondedMethod() == nonbonded_method break else: raise Exception elif issubclass(result, (BaseException, Exception)): exception = result forcefield.get_parameter_handler("vdW", {}).method = vdw_method forcefield.get_parameter_handler( "Electrostatics", {} ).method = electrostatics_method openff_interchange = Interchange.from_smirnoff( force_field=forcefield, topology=topology ) with pytest.raises(exception): openff_interchange.to_openmm(combine_nonbonded_forces=True) else: raise Exception("uh oh")
def test_unsupported_mixing_rule(self, ethanol_top, parsley): # TODO: Update this test when the model supports more mixing rules than GROMACS does openff_sys = Interchange.from_smirnoff(force_field=parsley, topology=ethanol_top) openff_sys["vdW"].mixing_rule = "kong" with pytest.raises(UnsupportedExportError, match="rule `geometric` not compat"): openff_sys.to_top("out.top")
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_basic_conversion_ammonia(self, ammonia_ff, ammonia_top, box): off_sys = Interchange.from_smirnoff(force_field=ammonia_ff, topology=ammonia_top, box=box) off_sys.positions = np.zeros(shape=(ammonia_top.n_topology_atoms, 3)) struct = off_sys._to_parmed() # As partial sanity check, see if it they save without error struct.save("x.top", combine="all") struct.save("x.gro", combine="all") assert np.allclose(struct.box, np.array([40, 40, 40, 90, 90, 90]))
def test_box(self, argon_ff, argon_top, box): off_sys = Interchange.from_smirnoff(force_field=argon_ff, topology=argon_top, box=box) off_sys.positions = (np.zeros(shape=(argon_top.n_topology_atoms, 3)) * unit.angstrom) struct = off_sys._to_parmed() assert np.allclose( struct.box[:3], [40, 40, 40], )
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_liquid_argon(): argon = Molecule.from_smiles("[#18]") pdbfile = app.PDBFile(get_test_file_path("packed-argon.pdb")) top = Topology.from_openmm(pdbfile.topology, unique_molecules=[argon]) argon_ff = ForceField(get_test_file_path("argon.offxml")) out = Interchange.from_smirnoff(argon_ff, top) out.positions = pdbfile.positions omm_energies = get_openmm_energies(out) gmx_energies = get_gromacs_energies( out, mdp="auto", writer="internal", ) omm_energies.compare( gmx_energies, custom_tolerances={ "vdW": 0.008 * simtk_unit.kilojoule_per_mole, }, ) argon_ff_no_switch = deepcopy(argon_ff) argon_ff_no_switch["vdW"].switch_width *= 0 out_no_switch = Interchange.from_smirnoff(argon_ff_no_switch, top) out_no_switch.positions = pdbfile.positions lmp_energies = get_lammps_energies(out_no_switch) omm_energies.compare( lmp_energies, custom_tolerances={ "vdW": 10.5 * simtk_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 get_amber_energies( off_sys: Interchange, writer: str = "parmed", electrostatics=True, ) -> EnergyReport: """ Given an OpenFF Interchange object, return single-point energies as computed by Amber. .. warning :: This API is experimental and subject to change. Parameters ---------- off_sys : openff.interchange.components.interchange.Interchange An OpenFF Interchange object to compute the single-point energy of writer : str, default="parmed" A string key identifying the backend to be used to write GROMACS files. The default value of `"parmed"` results in ParmEd being used as a backend. electrostatics : bool, default=True A boolean indicating whether or not electrostatics should be included in the energy calculation. Returns ------- report : EnergyReport An `EnergyReport` object containing the single-point energies. """ with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): struct = off_sys._to_parmed() struct.save("out.inpcrd") struct.save("out.prmtop") off_sys.to_top("out.top", writer=writer) report = _run_sander( prmtop_file="out.prmtop", inpcrd_file="out.inpcrd", electrostatics=electrostatics, ) return report
def test_parmed_roundtrip(self): original = pmd.load_file(get_test_file_path("ALA_GLY/ALA_GLY.top")) gro = pmd.load_file(get_test_file_path("ALA_GLY/ALA_GLY.gro")) original.box = gro.box original.positions = gro.positions openff_sys = Interchange._from_parmed(original) openff_sys.topology.mdtop = md.Topology.from_openmm(gro.topology) # Some sanity checks, including that residues are stored ... assert openff_sys.topology.mdtop.n_atoms == 29 # TODO: Assert number of topology molecules after refactor assert openff_sys.topology.mdtop.n_residues == 4 # ... and written out openff_sys.to_gro("has_residues.gro", writer="internal") assert len(pmd.load_file("has_residues.gro").residues) == 4 roundtrip = openff_sys._to_parmed() roundtrip.save("conv.gro", overwrite=True) roundtrip.save("conv.top", overwrite=True) original_energy = _run_gmx_energy( top_file=get_test_file_path("ALA_GLY/ALA_GLY.top"), gro_file=get_test_file_path("ALA_GLY/ALA_GLY.gro"), mdp_file=_get_mdp_file("cutoff_hbonds"), ) internal_energy = get_gromacs_energies(openff_sys, mdp="cutoff_hbonds") roundtrip_energy = _run_gmx_energy( top_file="conv.top", gro_file="conv.gro", mdp_file=_get_mdp_file("cutoff_hbonds"), ) # Differences in bond energies appear to be related to ParmEd's rounding # of the force constant and equilibrium bond length original_energy.compare(internal_energy) internal_energy.compare( roundtrip_energy, custom_tolerances={ "Bond": 0.02 * omm_unit.kilojoule_per_mole, }, ) original_energy.compare( roundtrip_energy, custom_tolerances={ "Bond": 0.02 * omm_unit.kilojoule_per_mole, }, )
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_set_mixing_rule(self, ethanol_top, parsley): import parmed as pmd openff_sys = Interchange.from_smirnoff(force_field=parsley, topology=ethanol_top) openff_sys.to_top("lorentz.top") top_file = pmd.load_file("lorentz.top") assert top_file.combining_rule == "lorentz" openff_sys["vdW"].mixing_rule = "geometric" openff_sys.to_top("geometric.top") top_file = pmd.load_file("geometric.top") assert top_file.combining_rule == "geometric"