def test_combine_molecules_deepdiff_offxml( acetone, openff, coumarin, tmpdir, rfree_data ): """When not optimising anything make sure we can round trip openff parameters""" with tmpdir.as_cwd(): openff.run(acetone) acetone_ref_system = xmltodict.parse(open("serialised.xml").read()) openff.run(coumarin) coumarin_ref_system = xmltodict.parse(open("serialised.xml").read()) _combine_molecules_offxml( molecules=[acetone, coumarin], parameters=[], rfree_data=rfree_data, filename="combined.offxml", ) # load up new systems and compare combinded_ff = ForceField("combined.offxml") assert combinded_ff.author == f"QUBEKit_version_{qubekit.__version__}" acetone_combine_system = xmltodict.parse( XmlSerializer.serialize( combinded_ff.create_openmm_system( topology=Molecule.from_rdkit(acetone.to_rdkit()).to_topology() ) ) ) acetone_diff = DeepDiff( acetone_ref_system, acetone_combine_system, ignore_order=True, significant_digits=6, ) # should only be a difference in torsions with zero k values as openff drops these assert len(acetone_diff) == 1 for item in acetone_diff["iterable_item_added"].values(): assert item["@k"] == "0" coumarin_combine_system = xmltodict.parse( XmlSerializer.serialize( combinded_ff.create_openmm_system( topology=Molecule.from_rdkit(coumarin.to_rdkit()).to_topology() ) ) ) coumarin_diff = DeepDiff( coumarin_ref_system, coumarin_combine_system, ignore_order=True, significant_digits=6, ) assert len(coumarin_diff) == 1 for item in coumarin_diff["iterable_item_added"].values(): assert item["@k"] == "0"
def test_scaled_de_energy(): """For a molecule with 1-4 interactions make sure the scaling is correctly applied. Note that only nonbonded parameters are non zero. """ ff = ForceField(load_plugins=True) ff.get_parameter_handler("Electrostatics") ff.get_parameter_handler( "ChargeIncrementModel", { "version": "0.3", "partial_charge_method": "formal_charge" }, ) vdw_handler = ff.get_parameter_handler("vdW") vdw_handler.add_parameter({ "smirks": "[*:1]", "epsilon": 0.0 * unit.kilojoule_per_mole, "sigma": 1.0 * unit.angstrom, }) double_exp = ff.get_parameter_handler("DoubleExponential") double_exp.alpha = 18.7 double_exp.beta = 3.3 double_exp.scale14 = 1 double_exp.add_parameter({ "smirks": "[#6X4:1]", "r_min": 3.816 * unit.angstrom, "epsilon": 0.1094 * unit.kilocalorie_per_mole, }) double_exp.add_parameter({ "smirks": "[#1:1]-[#6X4]", "r_min": 2.974 * unit.angstrom, "epsilon": 0.0157 * unit.kilocalorie_per_mole, }) ethane = Molecule.from_smiles("CC") ethane.generate_conformers(n_conformers=1) off_top = ethane.to_topology() omm_top = off_top.to_openmm() system_no_scale = ff.create_openmm_system(topology=off_top) energy_no_scale = evaluate_energy(system=system_no_scale, topology=omm_top, positions=ethane.conformers[0]) # now scale 1-4 by half double_exp.scale14 = 0.5 system_scaled = ff.create_openmm_system(topology=off_top) energy_scaled = evaluate_energy(system=system_scaled, topology=omm_top, positions=ethane.conformers[0]) assert double_exp.scale14 * energy_no_scale == pytest.approx(energy_scaled, abs=1e-6)
def _build_system(mol, constrained): if constrained: parsley = ForceField("openff-1.0.0.offxml") else: parsley = ForceField("openff_unconstrained-1.0.0.offxml") mol = Molecule.from_file(get_data_file_path("molecules/" + mol), file_format="sdf") if type(mol) == Molecule: off_top = mol.to_topology() positions = mol.conformers[0] elif type(mol) == list: # methane_multiconformer case is a list of two mols off_top = Topology() for mol_i in mol: off_top.add_molecule(mol_i) positions = (np.vstack([mol[0].conformers[0], mol[1].conformers[0]]) * unit.angstrom) from openff.toolkit.utils.toolkits import ( AmberToolsToolkitWrapper, RDKitToolkitWrapper, ToolkitRegistry, ) toolkit_registry = ToolkitRegistry( toolkit_precedence=[RDKitToolkitWrapper, AmberToolsToolkitWrapper]) omm_sys = parsley.create_openmm_system(off_top, toolkit_registry=toolkit_registry) return omm_sys, positions, off_top
def test_combine_molecules_offxml_plugin_deepdiff(tmpdir, coumarin, rfree_data): """Make sure that systems made from molecules using the xml method match offxmls with plugins""" # we need to recalculate the Nonbonded terms using the fake Rfree coumarin_copy = coumarin.copy(deep=True) # apply symmetry to make sure systems match MBISCharges.apply_symmetrisation(coumarin_copy) with tmpdir.as_cwd(): # make the offxml using the plugin interface _combine_molecules_offxml( molecules=[coumarin_copy], parameters=elements, rfree_data=rfree_data, filename="openff.offxml", water_model="tip3p", ) offxml = ForceField( "openff.offxml", load_plugins=True, allow_cosmetic_attributes=True ) # check the plugin is being used vdw = offxml.get_parameter_handler("QUBEKitvdWTS") # make sure we have the parameterize tags assert len(vdw._cosmetic_attribs) == 1 assert len(vdw.parameters) == 28 alpha = rfree_data.pop("alpha") beta = rfree_data.pop("beta") lj = LennardJones612(free_parameters=rfree_data, alpha=alpha, beta=beta) # get new Rfree data lj.run(coumarin_copy) coumarin_copy.write_parameters("coumarin.xml") coumarin_ref_system = xmltodict.parse( XmlSerializer.serialize( app.ForceField("coumarin.xml").createSystem( topology=coumarin_copy.to_openmm_topology(), nonbondedCutoff=9 * unit.angstroms, removeCMMotion=False, ) ) ) coumarin_off_system = xmltodict.parse( XmlSerializer.serialize( offxml.create_openmm_system( Molecule.from_rdkit(coumarin_copy.to_rdkit()).to_topology() ) ) ) coumarin_diff = DeepDiff( coumarin_ref_system, coumarin_off_system, ignore_order=True, significant_digits=6, exclude_regex_paths="mass", ) assert len(coumarin_diff) == 1 for item in coumarin_diff["iterable_item_added"].values(): assert item["@k"] == "0"
def _create_impropers_only_system( smiles: str = "CC1=C(C(=O)C2=C(C1=O)N3CC4C(C3(C2COC(=O)N)OC)N4)N", ) -> mm.System: """Create a simulation that contains only improper torsion terms, by parameterizing with openff-1.2.0 and deleting all terms but impropers """ molecule = Molecule.from_smiles(smiles, allow_undefined_stereo=True) g = esp.Graph(molecule) topology = Topology.from_molecules(molecule) forcefield = ForceField("openff-1.2.0.offxml") openmm_system = forcefield.create_openmm_system(topology) # delete all forces except PeriodicTorsionForce is_torsion = ( lambda force: "PeriodicTorsionForce" in force.__class__.__name__ ) for i in range(openmm_system.getNumForces())[::-1]: if not is_torsion(openmm_system.getForce(i)): openmm_system.removeForce(i) assert openmm_system.getNumForces() == 1 torsion_force = openmm_system.getForce(0) assert is_torsion(torsion_force) # set k = 0 for any torsion that's not an improper indices = set( map( tuple, esp.graphs.utils.offmol_indices.improper_torsion_indices( molecule ), ) ) num_impropers_retained = 0 for i in range(torsion_force.getNumTorsions()): ( p1, p2, p3, p4, periodicity, phase, k, ) = torsion_force.getTorsionParameters(i) if (p1, p2, p3, p4) in indices: num_impropers_retained += 1 else: torsion_force.setTorsionParameters( i, p1, p2, p3, p4, periodicity, phase, 0.0 ) assert ( num_impropers_retained > 0 ) # otherwise this molecule is not a useful test case! return openmm_system, topology, g
def test_smirnoff_hack(): """Test basic behavior of smirnoff_hack.py, in particular the compatibility with Molecule API""" from openff.toolkit.topology import Molecule, Topology from openff.toolkit.typing.engines.smirnoff import ForceField from openff.toolkit.utils.toolkits import (AmberToolsToolkitWrapper, RDKitToolkitWrapper, ToolkitRegistry) from forcebalance import smirnoff_hack top = Topology.from_molecules(Molecule.from_smiles("CCO")) parsley = ForceField("openff-1.0.0.offxml") registry = ToolkitRegistry() registry.register_toolkit(RDKitToolkitWrapper) registry.register_toolkit(AmberToolsToolkitWrapper) parsley.create_openmm_system(top, toolkit_registry=registry)
def test_offxml_round_trip(tmpdir, openff, molecule): """ Test round tripping offxml parameters through qubekit """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data(molecule)) offmol = Molecule.from_file(get_data(molecule)) openff.run(mol) mol.to_offxml("test.offxml") # build another openmm system and serialise to compare with deepdiff offxml = ForceField("test.offxml") assert offxml.author == f"QUBEKit_version_{qubekit.__version__}" qubekit_system = offxml.create_openmm_system( topology=offmol.to_topology()) qubekit_xml = xmltodict.parse( openmm.XmlSerializer.serialize(qubekit_system)) with open("qubekit_xml", "w") as output: output.write(openmm.XmlSerializer.serialize(qubekit_system)) openff_system = xmltodict.parse(open("serialised.xml").read()) offxml_diff = DeepDiff( qubekit_xml, openff_system, ignore_order=True, significant_digits=6, ) # the only difference should be in torsions with a 0 barrier height which are excluded from an offxml for item in offxml_diff["iterable_item_removed"].values(): assert item["@k"] == "0" # load both systems and compute the energy qubekit_top = load_topology( mol.to_openmm_topology(), system=qubekit_system, xyz=mol.openmm_coordinates(), ) qubekit_energy = energy_decomposition_system(qubekit_top, qubekit_system, platform="Reference") ref_system = XmlSerializer.deserializeSystem( open("serialised.xml").read()) parm_top = load_topology(mol.to_openmm_topology(), system=ref_system, xyz=mol.openmm_coordinates()) ref_energy = energy_decomposition_system(parm_top, ref_system, platform="Reference") # compare the decomposed energies of the groups for force_group, energy in ref_energy: for qube_force, qube_e in qubekit_energy: if force_group == qube_force: assert energy == pytest.approx(qube_e, abs=2e-3)
def test_from_openmm_single_mols(mol, n_mols): """ Test that ForceField.create_openmm_system and Interchange.to_openmm produce objects with similar energies TODO: Tighten tolerances TODO: Test periodic and non-periodic """ parsley = ForceField(get_test_file_path("parsley.offxml")) mol = Molecule.from_smiles(mol) mol.generate_conformers(n_conformers=1) top = Topology.from_molecules(n_mols * [mol]) mol.conformers[0] -= np.min(mol.conformers) * simtk_unit.angstrom top.box_vectors = np.eye(3) * np.asarray([15, 15, 15]) * simtk_unit.nanometer if n_mols == 1: positions = mol.conformers[0] elif n_mols == 2: positions = np.vstack( [mol.conformers[0], mol.conformers[0] + 3 * simtk_unit.nanometer] ) positions = positions * simtk_unit.angstrom toolkit_system = parsley.create_openmm_system(top) native_system = Interchange.from_smirnoff( force_field=parsley, topology=top ).to_openmm() toolkit_energy = _get_openmm_energies( omm_sys=toolkit_system, box_vectors=toolkit_system.getDefaultPeriodicBoxVectors(), positions=positions, ) native_energy = _get_openmm_energies( omm_sys=native_system, box_vectors=native_system.getDefaultPeriodicBoxVectors(), positions=positions, ) toolkit_energy.compare(native_energy)
def single_molecule_coverage(molecule: Molecule, forcefield: ForceField): #-> Dict[str, Dict[str, int]], List[Molecule], List[Molecule, Exception] """ For a single molecule generate a coverage report and try to build an openmm system this will also highlight any missing parameters and dificulties with charging the molecule. Parameters ---------- molecule: The openff-toolkit molecule object for which the report should be generated. ff: The openff-toolkit typing engine that should be used to check coverage and build an openmm system. Returns ------- report: dict A dictionary of the coverage report e: Exception or None. The exception raised in this step, if any. If not None, it should be assumed that coverage is invalid. """ coverage = { "Angles": {}, "Bonds": {}, "ProperTorsions": {}, "ImproperTorsions": {}, "vdW": {} } coverage["molecule"] = molecule try: labels = forcefield.label_molecules(molecule.to_topology())[0] for param_type, params in labels.items(): for param in params.values(): if param.id not in coverage[param_type]: coverage[param_type][param.id] = 1 else: coverage[param_type][param.id] += 1 # now generate a system this will catch any missing parameters # and molecules that can not be charged _ = forcefield.create_openmm_system(molecule.to_topology()) return coverage, None except Exception as e: return coverage, e
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 test_water_dimer(): tip3p = ForceField(get_test_file_path("tip3p.offxml")) water = Molecule.from_smiles("O") top = Topology.from_molecules(2 * [water]) top.mdtop = md.Topology.from_openmm(top.to_openmm()) pdbfile = openmm.app.PDBFile(get_test_file_path("water-dimer.pdb")) positions = pdbfile.positions openff_sys = Interchange.from_smirnoff(tip3p, top) openff_sys.positions = positions openff_sys.box = [10, 10, 10] * unit.nanometer omm_energies = get_openmm_energies( openff_sys, hard_cutoff=True, electrostatics=False, ) toolkit_energies = _get_openmm_energies( tip3p.create_openmm_system(top), openff_sys.box, openff_sys.positions, hard_cutoff=True, electrostatics=False, ) omm_energies.compare(toolkit_energies) # TODO: Fix GROMACS energies by handling SETTLE constraints # gmx_energies, _ = get_gromacs_energies(openff_sys) # compare_gromacs_openmm(omm_energies=omm_energies, gmx_energies=gmx_energies) openff_sys["Electrostatics"].method = "cutoff" omm_energies_cutoff = get_gromacs_energies(openff_sys) lmp_energies = get_lammps_energies(openff_sys) lmp_energies.compare(omm_energies_cutoff)
def openff_openmm_pmd_gmx( topology: Topology, forcefield: ForceField, box: ArrayQuantity, prefix: str, ) -> None: """Pipeline to write GROMACS files from and OpenMM interchange through ParmEd""" topology.box_vectors = box.to( unit.nanometer).magnitude * omm_unit.nanometer # type: ignore omm_sys = forcefield.create_openmm_system(topology) struct = pmd.openmm.load_topology( system=omm_sys, topology=topology.to_openmm(), xyz=topology.topology_molecules[0].reference_molecule.conformers[0], ) # Assign dummy residue names, GROMACS will not accept empty strings # TODO: Patch upstream? for res in struct.residues: res.name = "FOO" struct.save(prefix + ".gro") struct.save(prefix + ".top")
from openff.toolkit.utils.toolkits import ( GLOBAL_TOOLKIT_REGISTRY, AmberToolsToolkitWrapper, RDKitToolkitWrapper, ) assert RDKitToolkitWrapper().is_available() assert AmberToolsToolkitWrapper().is_available() print(GLOBAL_TOOLKIT_REGISTRY.registered_toolkit_versions) from openff.toolkit.topology import Molecule, Topology from openff.toolkit.typing.engines.smirnoff import ForceField offmol = Molecule.from_smiles('CCO') ff = ForceField('openff-1.0.0.offxml') ff.create_openmm_system(offmol.to_topology())
def compute_conformer_energies_from_file(filename): # Load in the molecule and its conformers. # Note that all conformers of the same molecule are loaded as separate Molecule objects # If using a OFF Toolkit version before 0.7.0, loading SDFs through RDKit and OpenEye may provide # different behavior in some cases. So, here we force loading through RDKit to ensure the correct behavior rdktkw = RDKitToolkitWrapper() loaded_molecules = Molecule.from_file(filename, toolkit_registry=rdktkw) # The logic below only works for lists of molecules, so if a # single molecule was loaded, cast it to list if type(loaded_molecules) is not list: loaded_molecules = [loaded_molecules] # Collatate all conformers of the same molecule # NOTE: This isn't necessary if you have already loaded or created multi-conformer molecules; # it is just needed because our SDF reader does not automatically collapse conformers. molecules = [loaded_molecules[0]] for molecule in loaded_molecules[1:]: if molecule == molecules[-1]: for conformer in molecule.conformers: molecules[-1].add_conformer(conformer) else: molecules.append(molecule) n_molecules = len(molecules) n_conformers = sum([mol.n_conformers for mol in molecules]) print( f'{n_molecules} unique molecule(s) loaded, with {n_conformers} total conformers' ) # Load the openff-1.1.0 force field appropriate for vacuum calculations (without constraints) from openff.toolkit.typing.engines.smirnoff import ForceField forcefield = ForceField('openff_unconstrained-1.1.0.offxml') # Loop over molecules and minimize each conformer for molecule in molecules: # If the molecule doesn't have a name, set mol.name to be the hill formula if molecule.name == '': molecule.name = Topology._networkx_to_hill_formula( molecule.to_networkx()) print('%s : %d conformers' % (molecule.name, molecule.n_conformers)) # Make a temporary copy of the molecule that we can update for each minimization mol_copy = Molecule(molecule) # Make an OpenFF Topology so we can parameterize the system off_top = molecule.to_topology() print( f"Parametrizing {molecule.name} (may take a moment to calculate charges)" ) system = forcefield.create_openmm_system(off_top) # Use OpenMM to compute initial and minimized energy for all conformers integrator = openmm.VerletIntegrator(1 * unit.femtoseconds) platform = openmm.Platform.getPlatformByName('Reference') omm_top = off_top.to_openmm() simulation = openmm.app.Simulation(omm_top, system, integrator, platform) # Print text header print( 'Conformer Initial PE Minimized PE RMS between initial and minimized conformer' ) output = [[ 'Conformer', 'Initial PE (kcal/mol)', 'Minimized PE (kcal/mol)', 'RMS between initial and minimized conformer (Angstrom)' ]] for conformer_index, conformer in enumerate(molecule.conformers): simulation.context.setPositions(conformer) orig_potential = simulation.context.getState( getEnergy=True).getPotentialEnergy() simulation.minimizeEnergy() min_state = simulation.context.getState(getEnergy=True, getPositions=True) min_potential = min_state.getPotentialEnergy() # Calculate the RMSD between the initial and minimized conformer min_coords = min_state.getPositions() min_coords = np.array([[atom.x, atom.y, atom.z] for atom in min_coords]) * unit.nanometer mol_copy._conformers = None mol_copy.add_conformer(conformer) mol_copy.add_conformer(min_coords) rdmol = mol_copy.to_rdkit() rmslist = [] rdMolAlign.AlignMolConformers(rdmol, RMSlist=rmslist) minimization_rms = rmslist[0] # Save the minimized conformer to file mol_copy._conformers = None mol_copy.add_conformer(min_coords) mol_copy.to_file( f'{molecule.name}_conf{conformer_index+1}_minimized.sdf', file_format='sdf') print( '%5d / %5d : %8.3f kcal/mol %8.3f kcal/mol %8.3f Angstroms' % (conformer_index + 1, molecule.n_conformers, orig_potential / unit.kilocalories_per_mole, min_potential / unit.kilocalories_per_mole, minimization_rms)) output.append([ str(conformer_index + 1), f'{orig_potential/unit.kilocalories_per_mole:.3f}', f'{min_potential/unit.kilocalories_per_mole:.3f}', f'{minimization_rms:.3f}' ]) # Write the results out to CSV with open(f'{molecule.name}.csv', 'w') as of: for line in output: of.write(','.join(line) + '\n') # Clean up OpenMM Simulation del simulation, integrator
def test_energies_single_mol(constrained, mol_smi): import mbuild as mb mol = Molecule.from_smiles(mol_smi) mol.generate_conformers(n_conformers=1) mol.name = "FOO" top = mol.to_topology() top.box_vectors = None # [10, 10, 10] * simtk_unit.nanometer if constrained: parsley = ForceField("openff-1.0.0.offxml") else: parsley = ForceField("openff_unconstrained-1.0.0.offxml") off_sys = Interchange.from_smirnoff(parsley, top) off_sys.handlers["Electrostatics"].method = "cutoff" mol.to_file("out.xyz", file_format="xyz") compound: mb.Compound = mb.load("out.xyz") packed_box: mb.Compound = mb.fill_box(compound=compound, n_compounds=1, box=mb.Box(lengths=[10, 10, 10])) positions = packed_box.xyz * unit.nanometer off_sys.positions = positions # Compare directly to toolkit's reference implementation omm_energies = get_openmm_energies(off_sys, round_positions=8) omm_reference = parsley.create_openmm_system(top) reference_energies = _get_openmm_energies( omm_sys=omm_reference, box_vectors=off_sys.box, positions=off_sys.positions, round_positions=8, ) omm_energies.compare(reference_energies) mdp = "cutoff_hbonds" if constrained else "auto" # Compare GROMACS writer and OpenMM export gmx_energies = get_gromacs_energies(off_sys, mdp=mdp) custom_tolerances = { "Bond": 2e-5 * simtk_unit.kilojoule_per_mole, "Electrostatics": 2 * simtk_unit.kilojoule_per_mole, "vdW": 2 * simtk_unit.kilojoule_per_mole, "Nonbonded": 2 * simtk_unit.kilojoule_per_mole, "Angle": 1e-4 * simtk_unit.kilojoule_per_mole, } gmx_energies.compare( omm_energies, custom_tolerances=custom_tolerances, ) if not constrained: other_energies = get_openmm_energies( off_sys, round_positions=8, hard_cutoff=True, electrostatics=True, ) lmp_energies = get_lammps_energies(off_sys) custom_tolerances = { "vdW": 5.0 * simtk_unit.kilojoule_per_mole, "Electrostatics": 5.0 * simtk_unit.kilojoule_per_mole, } lmp_energies.compare(other_energies, custom_tolerances=custom_tolerances)
def simulate( force_field: ForceField, topology: Topology, positions: unit.Quantity, box_vectors: Optional[unit.Quantity], n_steps: int, temperature: unit.Quantity, pressure: Optional[unit.Quantity], platform: Literal["Reference", "OpenCL", "CUDA", "CPU"] = "Reference", output_directory: Optional[str] = None, ): """A helper function for simulating a system parameterised with a specific OpenFF force field using OpenMM. Parameters ---------- force_field The force field to apply. topology The topology detailing the system to simulate. positions The starting coordinates of the molecules in the system. box_vectors The box vectors to use. These will overwrite the topology box vectors. n_steps The number of steps to simulate for. temperature The temperature to simulate at. pressure The pressure to simulate at. platform The platform to simulate using. output_directory The optional directory to store the simulation outputs in. """ assert pressure is None or ( pressure is not None and box_vectors is not None ), "box vectors must be provided when the pressure is specified." topology.box_vectors = box_vectors # Create an OpenMM system by applying the parameters to the topology. omm_system, topology = force_field.create_openmm_system( topology, return_topology=True) if output_directory is not None: os.makedirs(output_directory, exist_ok=True) # Add the virtual sites to the OpenMM topology and positions. omm_topology = topology.to_openmm() omm_chain = [*omm_topology.chains()][0] omm_residue = omm_topology.addResidue("", chain=omm_chain) for particle in topology.topology_particles: if isinstance(particle, TopologyAtom): continue omm_topology.addAtom(particle.virtual_site.name, app.Element.getByMass(0), omm_residue) positions = numpy.vstack( [positions, numpy.zeros((topology.n_topology_virtual_sites, 3))]) with temporary_cd(output_directory): __simulate( positions=positions * unit.angstrom, box_vectors=box_vectors, omm_topology=omm_topology, omm_system=omm_system, n_steps=n_steps, temperature=temperature, pressure=pressure, platform=platform, )
def evaluate_water_energy_at_distances(force_field: ForceField, distances: List[float]) -> List[float]: """ Evaluate the energy of a system of two water molecules at the requested distances using the provided force field. Parameters ---------- force_field: The openff.toolkit force field object that should be used to parameterise the system. distances: The list of absolute distances between the oxygen atoms in angstroms. Returns ------- A list of energies evaluated at the given distances in kj/mol """ # build the topology water = Molecule.from_smiles("O") water.generate_conformers(n_conformers=1) topology = Topology.from_molecules([water, water]) # make the openmm system omm_system, topology = force_field.create_openmm_system( topology, return_topology=True) # generate positions at the requested distance positions = [ numpy.vstack([ water.conformers[0].value_in_unit(unit.angstrom), water.conformers[0].value_in_unit(unit.angstrom) + numpy.array([x, 0, 0]), ]) * unit.angstrom for x in distances ] # Add the virtual sites to the OpenMM topology and positions. omm_topology = topology.to_openmm() omm_chain = [*omm_topology.chains()][-1] omm_residue = omm_topology.addResidue("", chain=omm_chain) for particle in topology.topology_particles: if isinstance(particle, TopologyAtom): continue omm_topology.addAtom(particle.virtual_site.name, app.Element.getByMass(0), omm_residue) positions = [ numpy.vstack( [p, numpy.zeros( (topology.n_topology_virtual_sites, 3))]) * unit.angstrom for p in positions ] integrator = openmm.LangevinIntegrator( 300 * unit.kelvin, # simulation temperature, 1.0 / unit.picosecond, # friction 2.0 * unit.femtoseconds, # simulation timestep ) platform = openmm.Platform.getPlatformByName("CPU") simulation = app.Simulation(omm_topology, omm_system, integrator, platform) energies = [] for i, p in enumerate(positions): simulation.context.setPositions(p) simulation.context.computeVirtualSites() state = simulation.context.getState(getEnergy=True) energies.append(state.getPotentialEnergy().value_in_unit( unit.kilojoule_per_mole)) return energies
def system_subset( parameter_key: ParameterGradientKey, force_field: "ForceField", topology: "Topology", scale_amount: Optional[float] = None, ) -> Tuple["openmm.System", "simtk_unit.Quantity"]: """Produces an OpenMM system containing the minimum number of forces while still containing a specified force field parameter, and those other parameters which may interact with it (e.g. in the case of vdW parameters). The value of the parameter of interest may optionally be perturbed by an amount specified by ``scale_amount``. Parameters ---------- parameter_key The parameter of interest. force_field The force field to create the system from (and optionally perturb). topology The topology of the system to apply the force field to. scale_amount: float, optional The optional amount to perturb the ``parameter`` by such that ``parameter = (1.0 + scale_amount) * parameter``. Returns ------- The created system as well as the value of the specified ``parameter``. """ # As this method deals mainly with the toolkit, we stick to # simtk units here. from openff.toolkit.typing.engines.smirnoff import ForceField # Create the force field subset. force_field_subset = ForceField() handlers_to_register = {parameter_key.tag} if parameter_key.tag in {"ChargeIncrementModel", "LibraryCharges"}: # Make sure to retain all of the electrostatic handlers when dealing with # charges as the applied charges will depend on which charges have been applied # by previous handlers. handlers_to_register.update( {"Electrostatics", "ChargeIncrementModel", "LibraryCharges"}) registered_handlers = force_field.registered_parameter_handlers for handler_to_register in handlers_to_register: if handler_to_register not in registered_handlers: continue force_field_subset.register_parameter_handler( copy.deepcopy( force_field.get_parameter_handler(handler_to_register))) handler = force_field_subset.get_parameter_handler(parameter_key.tag) parameter = handler.parameters[parameter_key.smirks] parameter_value = getattr(parameter, parameter_key.attribute) # Optionally perturb the parameter of interest. if scale_amount is not None: if numpy.isclose(parameter_value.value_in_unit(parameter_value.unit), 0.0): # Careful thought needs to be given to this. Consider cases such as # epsilon or sigma where negative values are not allowed. parameter_value = (scale_amount if scale_amount > 0.0 else 0.0) * parameter_value.unit else: parameter_value *= 1.0 + scale_amount setattr(parameter, parameter_key.attribute, parameter_value) # Create the parameterized sub-system. system = force_field_subset.create_openmm_system(topology) return system, parameter_value
def test_molecule_and_water_offxml(coumarin, water, tmpdir, rfree_data, parameters): """Test that an offxml can parameterize a molecule and water mixture.""" coumarin_copy = coumarin.copy(deep=True) MBISCharges.apply_symmetrisation(coumarin_copy) with tmpdir.as_cwd(): # run the lj method to make sure the parameters match alpha = rfree_data.pop("alpha") beta = rfree_data.pop("beta") lj = LennardJones612(free_parameters=rfree_data, alpha=alpha, beta=beta) # get new Rfree data lj.run(coumarin_copy) # remake rfree rfree_data["alpha"] = alpha rfree_data["beta"] = beta _combine_molecules_offxml( molecules=[coumarin_copy], parameters=parameters, rfree_data=rfree_data, filename="combined.offxml", water_model="tip3p", ) combinded_ff = ForceField( "combined.offxml", load_plugins=True, allow_cosmetic_attributes=True ) mixed_top = Topology.from_molecules( molecules=[ Molecule.from_rdkit(water.to_rdkit()), Molecule.from_rdkit(coumarin_copy.to_rdkit()), ] ) system = combinded_ff.create_openmm_system(topology=mixed_top) # make sure we have 3 constraints assert system.getNumConstraints() == 3 # check each constraint reference_constraints = [ [0, 1, unit.Quantity(0.9572, unit=unit.angstroms)], [0, 2, unit.Quantity(0.9572, unit=unit.angstroms)], [1, 2, unit.Quantity(1.5139006545247014, unit=unit.angstroms)], ] for i in range(3): a, b, constraint = system.getConstraintParameters(i) assert a == reference_constraints[i][0] assert b == reference_constraints[i][1] assert constraint == reference_constraints[i][2].in_units_of( unit.nanometers ) # now loop over the forces and make sure water was correctly parameterised forces = dict((force.__class__.__name__, force) for force in system.getForces()) nonbonded_force: openmm.NonbondedForce = forces["NonbondedForce"] # first check water has the correct parameters water_reference = [ [ unit.Quantity(-0.834, unit.elementary_charge), unit.Quantity(3.1507, unit=unit.angstroms), unit.Quantity(0.1521, unit=unit.kilocalorie_per_mole), ], [ unit.Quantity(0.417, unit.elementary_charge), unit.Quantity(1, unit=unit.angstroms), unit.Quantity(0, unit=unit.kilocalorie_per_mole), ], [ unit.Quantity(0.417, unit.elementary_charge), unit.Quantity(1, unit=unit.angstroms), unit.Quantity(0, unit=unit.kilocalorie_per_mole), ], ] for i in range(3): charge, sigma, epsilon = nonbonded_force.getParticleParameters(i) assert charge == water_reference[i][0] assert sigma.in_units_of(unit.angstroms) == water_reference[i][1] assert ( epsilon.in_units_of(unit.kilocalorie_per_mole) == water_reference[i][2] ) # now check coumarin for i in range(coumarin_copy.n_atoms): ref_params = coumarin_copy.NonbondedForce[(i,)] charge, sigma, epsilon = nonbonded_force.getParticleParameters(i + 3) assert charge.value_in_unit(unit.elementary_charge) == float( ref_params.charge ) assert sigma.value_in_unit(unit.nanometers) == ref_params.sigma assert epsilon.value_in_unit(unit.kilojoule_per_mole) == ref_params.epsilon
amber_energy = calc_energy(amber_system, amber_top, amber_struct.positions) print(amber_energy) # prepare openff system mol = Molecule.from_file(f'{prefix}.mol2') #mol = Molecule.from_file('%s.mol2' %(prefix)) #print(mol.to_smiles()) from utils import fix_carboxylate_bond_orders fix_carboxylate_bond_orders(mol) print(mol.to_smiles()) off_top = mol.to_topology() #off_top.box_vectors = [[48, 0, 0], [0, 48, 0], [0, 0, 48]] * unit.angstrom #print('off_box', off_top.box_vectors) #print('amb_box', pmd_struct.box) off_sys = ff.create_openmm_system(mol.to_topology(), ) #allow_nonintegral_charges=True) #nonbonded_force = [force for force in off_sys.getForces() if isinstance(force, openmm.NonbondedForce)][0] #nonbonded_force.createExceptionsFromBonds([(bond.atom1.molecule_atom_index, # bond.atom2.molecule_atom_index) for bond in mol.bonds], # ) #nonbonded_force.set with open('off_sys.xml', 'w') as of: of.write(XmlSerializer.serialize(off_sys)) off_energy = calc_energy( off_sys, off_top, amber_struct.positions, #mol.conformers[0] ) print(off_energy) off_struct = ParmEd.openmm.load_topology(
class SMIRNOFF(OpenMM): """ Derived from Engine object for carrying out OpenMM calculations that use the SMIRNOFF force field. """ def __init__(self, name="openmm", **kwargs): self.valkwd = [ 'ffxml', 'pdb', 'mol2', 'platname', 'precision', 'mmopts', 'vsite_bonds', 'implicit_solvent', 'restrain_k', 'freeze_atoms' ] if not toolkit_import_success: warn_once( "Note: Failed to import the OpenFF Toolkit - SMIRNOFF Engine will not work. " ) super(SMIRNOFF, self).__init__(name=name, **kwargs) def readsrc(self, **kwargs): """ SMIRNOFF simulations always require the following passed in via kwargs: Parameters ---------- pdb : string Name of a .pdb file containing the topology of the system mol2 : list A list of .mol2 file names containing the molecule/residue templates of the system Also provide 1 of the following, containing the coordinates to be used: mol : Molecule forcebalance.Molecule object coords : string Name of a file (readable by forcebalance.Molecule) This could be the same as the pdb argument from above. """ pdbfnm = None # Determine the PDB file name. if 'pdb' in kwargs and os.path.exists(kwargs['pdb']): # Case 1. The PDB file name is provided explicitly pdbfnm = kwargs['pdb'] if not os.path.exists(pdbfnm): logger.error("%s specified but doesn't exist\n" % pdbfnm) raise RuntimeError if 'mol' in kwargs: self.mol = kwargs['mol'] elif 'coords' in kwargs: if not os.path.exists(kwargs['coords']): logger.error("%s specified but doesn't exist\n" % kwargs['coords']) raise RuntimeError self.mol = Molecule(kwargs['coords']) else: logger.error( 'Must provide either a molecule object or coordinate file.\n') raise RuntimeError # Here we cannot distinguish the .mol2 files linked by the target # vs. the .mol2 files to be provided by the force field. # But we can assume that these files should exist when this function is called. self.mol2 = kwargs.get('mol2') if self.mol2: for fnm in self.mol2: if not os.path.exists(fnm): if hasattr(self, 'FF') and fnm in self.FF.fnms: continue logger.error("%s doesn't exist" % fnm) raise RuntimeError else: logger.error("Must provide a list of .mol2 files.\n") if pdbfnm is not None: self.abspdb = os.path.abspath(pdbfnm) mpdb = Molecule(pdbfnm) for i in ["chain", "atomname", "resid", "resname", "elem"]: self.mol.Data[i] = mpdb.Data[i] # Store a separate copy of the molecule for reference restraint positions. self.ref_mol = deepcopy(self.mol) @staticmethod def _openff_to_openmm_topology(openff_topology): """Convert an OpenFF topology to an OpenMM topology. Currently this requires manually adding the v-sites as OpenFF currently does not.""" from openff.toolkit.topology import TopologyAtom openmm_topology = openff_topology.to_openmm() # Return the topology if the number of OpenMM particles matches the number # expected by the OpenFF topology. This may happen if there are no virtual sites # in the system OR if a new version of the the OpenFF toolkit includes virtual # sites in the OpenMM topology it returns. if openmm_topology.getNumAtoms( ) == openff_topology.n_topology_particles: return openmm_topology openmm_chain = openmm_topology.addChain() openmm_residue = openmm_topology.addResidue("", chain=openmm_chain) for particle in openff_topology.topology_particles: if isinstance(particle, TopologyAtom): continue openmm_topology.addAtom(particle.virtual_site.name, app.Element.getByMass(0), openmm_residue) return openmm_topology def prepare(self, pbc=False, mmopts={}, **kwargs): """ Prepare the calculation. Note that we don't create the Simulation object yet, because that may depend on MD integrator parameters, thermostat, barostat etc. This is mostly copied and modified from openmmio.py's OpenMM.prepare(), but we are calling ForceField() from the OpenFF toolkit and ignoring AMOEBA stuff. """ if hasattr(self, 'abspdb'): self.pdb = PDBFile(self.abspdb) else: pdb1 = "%s-1.pdb" % os.path.splitext(os.path.basename( self.mol.fnm))[0] self.mol[0].write(pdb1) self.pdb = PDBFile(pdb1) os.unlink(pdb1) # Create the OpenFF ForceField object. if hasattr(self, 'FF'): self.offxml = [self.FF.offxml] self.forcefield = self.FF.openff_forcefield else: self.offxml = listfiles(kwargs.get('offxml'), 'offxml', err=True) self.forcefield = OpenFF_ForceField(*self.offxml, load_plugins=True) ## Load mol2 files for smirnoff topology openff_mols = [] for fnm in self.mol2: try: mol = OffMolecule.from_file(fnm) except Exception as e: logger.error("Error when loading %s" % fnm) raise e openff_mols.append(mol) self.off_topology = OffTopology.from_openmm( self.pdb.topology, unique_molecules=openff_mols) ## OpenMM options for setting up the System. self.mmopts = dict(mmopts) ## Specify frozen atoms and restraint force constant if 'restrain_k' in kwargs: self.restrain_k = kwargs['restrain_k'] if 'freeze_atoms' in kwargs: self.freeze_atoms = kwargs['freeze_atoms'][:] ## Set system options from ForceBalance force field options. fftmp = False if hasattr(self, 'FF'): self.mmopts['rigidWater'] = self.FF.rigid_water if not all([os.path.exists(f) for f in self.FF.fnms]): # If the parameter files don't already exist, create them for the purpose of # preparing the engine, but then delete them afterward. fftmp = True self.FF.make(np.zeros(self.FF.np)) ## Set system options from periodic boundary conditions. self.pbc = pbc ## print warning for 'nonbonded_cutoff' keywords if 'nonbonded_cutoff' in kwargs: logger.warning( "nonbonded_cutoff keyword ignored because it's set in the offxml file\n" ) # Apply the FF parameters to the system. Currently this is the only way to # determine if the FF will apply virtual sites to the system. _, openff_topology = self.forcefield.create_openmm_system( self.off_topology, return_topology=True) ## Generate OpenMM-compatible positions self.xyz_omms = [] for I in range(len(self.mol)): xyz = self.mol.xyzs[I] xyz_omm = ([Vec3(i[0], i[1], i[2]) for i in xyz] # Add placeholder positions for an v-sites. + [Vec3(0.0, 0.0, 0.0)] * openff_topology.n_topology_virtual_sites) * angstrom if self.pbc: # Obtain the periodic box if self.mol.boxes[I].alpha != 90.0 or self.mol.boxes[ I].beta != 90.0 or self.mol.boxes[I].gamma != 90.0: logger.error('OpenMM cannot handle nonorthogonal boxes.\n') raise RuntimeError box_omm = np.diag([ self.mol.boxes[I].a, self.mol.boxes[I].b, self.mol.boxes[I].c ]) * angstrom else: box_omm = None # Finally append it to list. self.xyz_omms.append((xyz_omm, box_omm)) # used in create_simulation() openmm_topology = SMIRNOFF._openff_to_openmm_topology(openff_topology) openmm_positions = ( self.pdb.positions.value_in_unit(angstrom) + # Add placeholder positions for an v-sites. [Vec3(0.0, 0.0, 0.0)] * openff_topology.n_topology_virtual_sites) * angstrom self.mod = Modeller(openmm_topology, openmm_positions) ## Build a topology and atom lists. Top = self.mod.getTopology() Atoms = list(Top.atoms()) # vss = [(i, [system.getVirtualSite(i).getParticle(j) for j in range(system.getVirtualSite(i).getNumParticles())]) \ # for i in range(system.getNumParticles()) if system.isVirtualSite(i)] self.AtomLists = defaultdict(list) self.AtomLists['Mass'] = [ a.element.mass.value_in_unit(dalton) if a.element is not None else 0 for a in Atoms ] self.AtomLists['ParticleType'] = [ 'A' if m >= 1.0 else 'D' for m in self.AtomLists['Mass'] ] self.AtomLists['ResidueNumber'] = [a.residue.index for a in Atoms] self.AtomMask = [a == 'A' for a in self.AtomLists['ParticleType']] self.realAtomIdxs = [ i for i, a in enumerate(self.AtomMask) if a is True ] if hasattr(self, 'FF') and fftmp: for f in self.FF.fnms: os.unlink(f) def update_simulation(self, **kwargs): """ Create the simulation object, or update the force field parameters in the existing simulation object. This should be run when we write a new force field XML file. """ if len(kwargs) > 0: self.simkwargs = kwargs # Because self.forcefield is being updated in forcebalance.forcefield.FF.make() # there is no longer a need to create a new force field object here. try: self.system, openff_topology = self.forcefield.create_openmm_system( self.off_topology, return_topology=True) except Exception as error: logger.error("Error when creating system for %s" % self.mol2) raise error # Commenting out all virtual site stuff for now. # self.vsinfo = PrepareVirtualSites(self.system) self.nbcharges = np.zeros(self.system.getNumParticles()) #---- # If the virtual site parameters have changed, # the simulation object must be remade. #---- # vsprm = GetVirtualSiteParameters(self.system) # if hasattr(self,'vsprm') and len(self.vsprm) > 0 and np.max(np.abs(vsprm - self.vsprm)) != 0.0: # if hasattr(self, 'simulation'): # delattr(self, 'simulation') # self.vsprm = vsprm.copy() if openff_topology.n_topology_virtual_sites > 0: # For now always assume that the v-sites have changed. This is currently # needed as the FB checks don't support the ``LocalCoordinatesSite`` based # virtual sites that OpenFF uses. if hasattr(self, 'simulation'): delattr(self, 'simulation') if hasattr(self, 'simulation'): UpdateSimulationParameters(self.system, self.simulation) else: self.create_simulation(**self.simkwargs) def _update_positions(self, X1, disable_vsite): # X1 is a numpy ndarray not vec3 if disable_vsite: super(SMIRNOFF, self)._update_positions(X1, disable_vsite) return n_v_sites = (self.mod.getTopology().getNumAtoms() - self.pdb.topology.getNumAtoms()) # Add placeholder positions for an v-sites. if isinstance(X1, np.ndarray): X1 = numpy.vstack([X1, np.zeros((n_v_sites, 3))]) * angstrom else: X1 = (X1 + [Vec3(0.0, 0.0, 0.0)] * n_v_sites) * angstrom self.simulation.context.setPositions(X1) self.simulation.context.computeVirtualSites() def interaction_energy(self, fraga, fragb): """ Calculate the interaction energy for two fragments. Because this creates two new objects and requires passing in the mol2 argument, the codes are copied and modified from the OpenMM class. """ self.update_simulation() if self.name == 'A' or self.name == 'B': logger.error("Don't name the engine A or B!\n") raise RuntimeError # Create two subengines. if hasattr(self, 'target'): if not hasattr(self, 'A'): self.A = SMIRNOFF(name="A", mol=self.mol.atom_select(fraga), mol2=self.mol2, target=self.target) if not hasattr(self, 'B'): self.B = SMIRNOFF(name="B", mol=self.mol.atom_select(fragb), mol2=self.mol2, target=self.target) else: if not hasattr(self, 'A'): self.A = SMIRNOFF(name="A", mol=self.mol.atom_select(fraga), mol2=self.mol2, platname=self.platname, \ precision=self.precision, offxml=self.offxml, mmopts=self.mmopts) if not hasattr(self, 'B'): self.B = SMIRNOFF(name="B", mol=self.mol.atom_select(fragb), mol2=self.mol2, platname=self.platname, \ precision=self.precision, offxml=self.offxml, mmopts=self.mmopts) # Interaction energy needs to be in kcal/mol. D = self.energy() A = self.A.energy() B = self.B.energy() return (D - A - B) / 4.184 def get_smirks_counter(self): """Get a counter for the time of appreance of each SMIRKS""" smirks_counter = Counter() molecule_force_list = self.forcefield.label_molecules( self.off_topology) for mol_idx, mol_forces in enumerate(molecule_force_list): for force_tag, force_dict in mol_forces.items(): # e.g. force_tag = 'Bonds' for parameters in force_dict.values(): if not isinstance(parameters, list): parameters = [parameters] for parameter in parameters: smirks_counter[parameter.smirks] += 1 return smirks_counter
def reparm(ligands, base): print( '**Running reparameterization of ligand(s) using open force fields\'s SMIRNOFF with openff 2.0.0**' ) # Load already parm'd system in_prmtop = base + '.prmtop' in_crd = base + '.inpcrd' # Create parmed strucuture orig_structure = parmed.amber.AmberParm(in_prmtop, in_crd) # Split orig_stucuture into unique structure instances e.g. protein, water, ligand, etc. pieces = orig_structure.split() for piece in pieces: # TODO: Figure out how to know which piece is which print(f"There are {len(piece[1])} instance(s) of {piece[0]}") # Generate an openff topology for the ligand # Openff Molecule does not support mol2 so conversion is needed ligs_w_sdf = [] for ligand in ligands: obabel[ligand[0], '-O', util.get_base(ligand[0]) + '.sdf']() ligs_w_sdf.append( (ligand[0], ligand[1], util.get_base(ligand[0]) + '.sdf')) # Keep track of ligands that were successfully reparmed so we know to skip them when putting the pieces back together reparmed_pieces = [] complex_structure = parmed.Structure() force_field = ForceField("openff_unconstrained-2.0.0.offxml") for lig in ligs_w_sdf: # Set up openff topology ligand_off_molecule = Molecule(lig[2]) ligand_pdbfile = PDBFile(lig[0]) ligand_off_topology = Topology.from_openmm( ligand_pdbfile.topology, unique_molecules=[ligand_off_molecule], ) # Parameterizing the ligand # Find ligand "piece", reparm, add to the new structure for piece in pieces: new_ligand_structure = None # TODO: Figure out how to know which piece is which if (ligand_off_molecule.n_atoms == len(piece[0].atoms)): if (ligand_off_molecule.n_bonds == len(piece[0].bonds)): if ([ atom.atomic_number for atom in ligand_off_molecule.atoms ] == [atom.element for atom in piece[0].atoms]): print('Found ligand piece', piece) try: # Since the method of matching the piece to ligand is imperfect, ligands that are isomers could mess things up. # So try any piece that matches and see if we get an error print('Reparameterizing ligand using SMIRNOFF') ligand_system = force_field.create_openmm_system( ligand_off_topology) new_ligand_structure = parmed.openmm.load_topology( ligand_off_topology.to_openmm(), ligand_system, xyz=piece[0].positions, ) # A quick check to make sure things were not messed up during param if check_discrepencies(new_ligand_structure, piece): # Add the newly parameterized ligand the complex structure reparmed_pieces.append(piece) new_ligand_structure *= len(piece[1]) complex_structure += parmed.amber.AmberParm.from_structure( new_ligand_structure) break except: pass # Stick all the pieces back together for piece in pieces: if (piece not in reparmed_pieces): curr_structure = parmed.Structure() curr_structure += piece[0] curr_structure *= len(piece[1]) complex_structure += parmed.amber.AmberParm.from_structure( curr_structure) # print("Unique atom names:",sorted(list({atom.atom_type.name for atom in complex_structure})),) # print("Number of unique atom types:", len({atom.atom_type for atom in complex_structure})) # print("Number of unique epsilons:", len({atom.epsilon for atom in complex_structure})) # print("Number of unique sigmas:", len({atom.sigma for atom in complex_structure})) # # Copy over the original coordinates and box vectors complex_structure.coordinates = orig_structure.coordinates complex_structure.box_vectors = orig_structure.box_vectors # Save the newly parameterized system complex_structure.save(base + ".prmtop", overwrite=True) complex_structure.save(base + ".inpcrd", overwrite=True)
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, }, )
def serialise_system(self): """Create the OpenMM system; parametrise using frost; serialise the system.""" # Create an openFF molecule from the rdkit molecule off_molecule = Molecule.from_rdkit(self.molecule.to_rdkit(), allow_undefined_stereo=True) # Make the OpenMM system off_topology = off_molecule.to_topology() forcefield = ForceField("openff_unconstrained-1.3.0.offxml") try: # Parametrise the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology) except ( UnassignedValenceParameterException, UnassignedBondParameterException, UnassignedProperTorsionParameterException, UnassignedAngleParameterException, UnassignedMoleculeChargeException, TypeError, ): # If this does not work then we have a molecule that is not in SMIRNOFF so we must add generics # and remove the charge handler to get some basic parameters for the moleucle new_bond = BondHandler.BondType( smirks="[*:1]~[*:2]", length="0 * angstrom", k="0.0 * angstrom**-2 * mole**-1 * kilocalorie", ) new_angle = AngleHandler.AngleType( smirks="[*:1]~[*:2]~[*:3]", angle="0.0 * degree", k="0.0 * mole**-1 * radian**-2 * kilocalorie", ) new_torsion = ProperTorsionHandler.ProperTorsionType( smirks="[*:1]~[*:2]~[*:3]~[*:4]", periodicity1="1", phase1="0.0 * degree", k1="0.0 * mole**-1 * kilocalorie", periodicity2="2", phase2="180.0 * degree", k2="0.0 * mole**-1 * kilocalorie", periodicity3="3", phase3="0.0 * degree", k3="0.0 * mole**-1 * kilocalorie", periodicity4="4", phase4="180.0 * degree", k4="0.0 * mole**-1 * kilocalorie", idivf1="1.0", idivf2="1.0", idivf3="1.0", idivf4="1.0", ) new_vdw = vdWHandler.vdWType( smirks="[*:1]", epsilon=0 * unit.kilocalories_per_mole, sigma=0 * unit.angstroms, ) new_generics = { "Bonds": new_bond, "Angles": new_angle, "ProperTorsions": new_torsion, "vdW": new_vdw, } for key, val in new_generics.items(): forcefield.get_parameter_handler(key).parameters.insert(0, val) # This has to be removed as sqm will fail with unknown elements del forcefield._parameter_handlers["ToolkitAM1BCC"] del forcefield._parameter_handlers["Electrostatics"] # Parametrize the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology) # This will tag the molecule so run.py knows that generics have been used. self.fftype = "generics" # Serialise the OpenMM system into the xml file with open("serialised.xml", "w+") as out: out.write(XmlSerializer.serializeSystem(system))
class SMIRNOFF(OpenMM): """ Derived from Engine object for carrying out OpenMM calculations that use the SMIRNOFF force field. """ def __init__(self, name="openmm", **kwargs): self.valkwd = [ 'ffxml', 'pdb', 'mol2', 'platname', 'precision', 'mmopts', 'vsite_bonds', 'implicit_solvent', 'restrain_k', 'freeze_atoms' ] if not toolkit_import_success: warn_once( "Note: Failed to import the OpenFF Toolkit - SMIRNOFF Engine will not work. " ) super(SMIRNOFF, self).__init__(name=name, **kwargs) def readsrc(self, **kwargs): """ SMIRNOFF simulations always require the following passed in via kwargs: Parameters ---------- pdb : string Name of a .pdb file containing the topology of the system mol2 : list A list of .mol2 file names containing the molecule/residue templates of the system Also provide 1 of the following, containing the coordinates to be used: mol : Molecule forcebalance.Molecule object coords : string Name of a file (readable by forcebalance.Molecule) This could be the same as the pdb argument from above. """ pdbfnm = kwargs.get('pdb') # Determine the PDB file name. if not pdbfnm: raise RuntimeError('Name of PDB file not provided.') elif not os.path.exists(pdbfnm): logger.error("%s specified but doesn't exist\n" % pdbfnm) raise RuntimeError if 'mol' in kwargs: self.mol = kwargs['mol'] elif 'coords' in kwargs: if not os.path.exists(kwargs['coords']): logger.error("%s specified but doesn't exist\n" % kwargs['coords']) raise RuntimeError self.mol = Molecule(kwargs['coords']) else: logger.error( 'Must provide either a molecule object or coordinate file.\n') raise RuntimeError # Here we cannot distinguish the .mol2 files linked by the target # vs. the .mol2 files to be provided by the force field. # But we can assume that these files should exist when this function is called. self.mol2 = kwargs.get('mol2') if self.mol2: for fnm in self.mol2: if not os.path.exists(fnm): if hasattr(self, 'FF') and fnm in self.FF.fnms: continue logger.error("%s doesn't exist" % fnm) raise RuntimeError else: logger.error("Must provide a list of .mol2 files.\n") self.abspdb = os.path.abspath(pdbfnm) mpdb = Molecule(pdbfnm) for i in ["chain", "atomname", "resid", "resname", "elem"]: self.mol.Data[i] = mpdb.Data[i] # Store a separate copy of the molecule for reference restraint positions. self.ref_mol = deepcopy(self.mol) def prepare(self, pbc=False, mmopts={}, **kwargs): """ Prepare the calculation. Note that we don't create the Simulation object yet, because that may depend on MD integrator parameters, thermostat, barostat etc. This is mostly copied and modified from openmmio.py's OpenMM.prepare(), but we are calling ForceField() from the OpenFF toolkit and ignoring AMOEBA stuff. """ self.pdb = PDBFile(self.abspdb) # Create the OpenFF ForceField object. if hasattr(self, 'FF'): self.offxml = [self.FF.offxml] self.forcefield = self.FF.openff_forcefield else: self.offxml = listfiles(kwargs.get('offxml'), 'offxml', err=True) self.forcefield = OpenFF_ForceField(*self.offxml) ## Load mol2 files for smirnoff topology openff_mols = [] for fnm in self.mol2: try: mol = OffMolecule.from_file(fnm) except Exception as e: logger.error("Error when loading %s" % fnm) raise e openff_mols.append(mol) self.off_topology = OffTopology.from_openmm( self.pdb.topology, unique_molecules=openff_mols) # used in create_simulation() self.mod = Modeller(self.pdb.topology, self.pdb.positions) ## OpenMM options for setting up the System. self.mmopts = dict(mmopts) ## Specify frozen atoms and restraint force constant if 'restrain_k' in kwargs: self.restrain_k = kwargs['restrain_k'] if 'freeze_atoms' in kwargs: self.freeze_atoms = kwargs['freeze_atoms'][:] ## Set system options from ForceBalance force field options. fftmp = False if hasattr(self, 'FF'): self.mmopts['rigidWater'] = self.FF.rigid_water if not all([os.path.exists(f) for f in self.FF.fnms]): # If the parameter files don't already exist, create them for the purpose of # preparing the engine, but then delete them afterward. fftmp = True self.FF.make(np.zeros(self.FF.np)) ## Set system options from periodic boundary conditions. self.pbc = pbc ## print warning for 'nonbonded_cutoff' keywords if 'nonbonded_cutoff' in kwargs: logger.warning( "nonbonded_cutoff keyword ignored because it's set in the offxml file\n" ) ## Generate OpenMM-compatible positions self.xyz_omms = [] for I in range(len(self.mol)): position = self.mol.xyzs[I] * angstrom # xyz_omm = [Vec3(i[0],i[1],i[2]) for i in xyz]*angstrom # An extra step with adding virtual particles # mod = Modeller(self.pdb.topology, xyz_omm) # LPW commenting out because we don't have virtual sites yet. # mod.addExtraParticles(self.forcefield) if self.pbc: # Obtain the periodic box if self.mol.boxes[I].alpha != 90.0 or self.mol.boxes[ I].beta != 90.0 or self.mol.boxes[I].gamma != 90.0: logger.error('OpenMM cannot handle nonorthogonal boxes.\n') raise RuntimeError box_omm = np.diag([ self.mol.boxes[I].a, self.mol.boxes[I].b, self.mol.boxes[I].c ]) * angstrom else: box_omm = None # Finally append it to list. self.xyz_omms.append((position, box_omm)) ## Build a topology and atom lists. Top = self.pdb.topology Atoms = list(Top.atoms()) Bonds = [(a.index, b.index) for a, b in list(Top.bonds())] # vss = [(i, [system.getVirtualSite(i).getParticle(j) for j in range(system.getVirtualSite(i).getNumParticles())]) \ # for i in range(system.getNumParticles()) if system.isVirtualSite(i)] self.AtomLists = defaultdict(list) self.AtomLists['Mass'] = [ a.element.mass.value_in_unit(dalton) if a.element is not None else 0 for a in Atoms ] self.AtomLists['ParticleType'] = [ 'A' if m >= 1.0 else 'D' for m in self.AtomLists['Mass'] ] self.AtomLists['ResidueNumber'] = [a.residue.index for a in Atoms] self.AtomMask = [a == 'A' for a in self.AtomLists['ParticleType']] self.realAtomIdxs = [ i for i, a in enumerate(self.AtomMask) if a is True ] if hasattr(self, 'FF') and fftmp: for f in self.FF.fnms: os.unlink(f) def update_simulation(self, **kwargs): """ Create the simulation object, or update the force field parameters in the existing simulation object. This should be run when we write a new force field XML file. """ if len(kwargs) > 0: self.simkwargs = kwargs # Because self.forcefield is being updated in forcebalance.forcefield.FF.make() # there is no longer a need to create a new force field object here. try: self.system = self.forcefield.create_openmm_system( self.off_topology) except Exception as error: logger.error("Error when creating system for %s" % self.mol2) raise error # Commenting out all virtual site stuff for now. # self.vsinfo = PrepareVirtualSites(self.system) self.nbcharges = np.zeros(self.system.getNumParticles()) #---- # If the virtual site parameters have changed, # the simulation object must be remade. #---- # vsprm = GetVirtualSiteParameters(self.system) # if hasattr(self,'vsprm') and len(self.vsprm) > 0 and np.max(np.abs(vsprm - self.vsprm)) != 0.0: # if hasattr(self, 'simulation'): # delattr(self, 'simulation') # self.vsprm = vsprm.copy() if hasattr(self, 'simulation'): UpdateSimulationParameters(self.system, self.simulation) else: self.create_simulation(**self.simkwargs) def optimize(self, shot=0, align=True, crit=1e-4): return super(SMIRNOFF, self).optimize(shot=shot, align=align, crit=crit, disable_vsite=True) def interaction_energy(self, fraga, fragb): """ Calculate the interaction energy for two fragments. Because this creates two new objects and requires passing in the mol2 argument, the codes are copied and modified from the OpenMM class. """ self.update_simulation() if self.name == 'A' or self.name == 'B': logger.error("Don't name the engine A or B!\n") raise RuntimeError # Create two subengines. if hasattr(self, 'target'): if not hasattr(self, 'A'): self.A = SMIRNOFF(name="A", mol=self.mol.atom_select(fraga), mol2=self.mol2, target=self.target) if not hasattr(self, 'B'): self.B = SMIRNOFF(name="B", mol=self.mol.atom_select(fragb), mol2=self.mol2, target=self.target) else: if not hasattr(self, 'A'): self.A = SMIRNOFF(name="A", mol=self.mol.atom_select(fraga), mol2=self.mol2, platname=self.platname, \ precision=self.precision, offxml=self.offxml, mmopts=self.mmopts) if not hasattr(self, 'B'): self.B = SMIRNOFF(name="B", mol=self.mol.atom_select(fragb), mol2=self.mol2, platname=self.platname, \ precision=self.precision, offxml=self.offxml, mmopts=self.mmopts) # Interaction energy needs to be in kcal/mol. D = self.energy() A = self.A.energy() B = self.B.energy() return (D - A - B) / 4.184 def get_smirks_counter(self): """Get a counter for the time of appreance of each SMIRKS""" smirks_counter = Counter() molecule_force_list = self.forcefield.label_molecules( self.off_topology) for mol_idx, mol_forces in enumerate(molecule_force_list): for force_tag, force_dict in mol_forces.items(): # e.g. force_tag = 'Bonds' for parameter in force_dict.values(): smirks_counter[parameter.smirks] += 1 return smirks_counter
def _build_system(self, molecule: "Ligand", input_files: Optional[List[str]] = None) -> System: """Create the OpenMM system; parametrise using frost; serialise the system.""" # Create an openFF molecule from the rdkit molecule, we always have hydrogen by this point off_molecule = Molecule.from_rdkit( molecule.to_rdkit(), allow_undefined_stereo=True, hydrogens_are_explicit=True, ) # Make the OpenMM system off_topology = off_molecule.to_topology() forcefield = ForceField(self.force_field) # we need to remove the constraints if "Constraints" in forcefield._parameter_handlers: del forcefield._parameter_handlers["Constraints"] try: # Parametrise the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology, ) except ( UnassignedValenceParameterException, UnassignedBondParameterException, UnassignedProperTorsionParameterException, UnassignedAngleParameterException, UnassignedMoleculeChargeException, TypeError, ): # If this does not work then we have a molecule that is not in SMIRNOFF so we must add generics # and remove the charge handler to get some basic parameters for the moleucle new_bond = BondHandler.BondType( smirks="[*:1]~[*:2]", length="0 * angstrom", k="0.0 * angstrom**-2 * mole**-1 * kilocalorie", ) new_angle = AngleHandler.AngleType( smirks="[*:1]~[*:2]~[*:3]", angle="0.0 * degree", k="0.0 * mole**-1 * radian**-2 * kilocalorie", ) new_torsion = ProperTorsionHandler.ProperTorsionType( smirks="[*:1]~[*:2]~[*:3]~[*:4]", periodicity1="1", phase1="0.0 * degree", k1="0.0 * mole**-1 * kilocalorie", periodicity2="2", phase2="180.0 * degree", k2="0.0 * mole**-1 * kilocalorie", periodicity3="3", phase3="0.0 * degree", k3="0.0 * mole**-1 * kilocalorie", periodicity4="4", phase4="180.0 * degree", k4="0.0 * mole**-1 * kilocalorie", idivf1="1.0", idivf2="1.0", idivf3="1.0", idivf4="1.0", ) new_vdw = vdWHandler.vdWType( smirks="[*:1]", epsilon=0 * unit.kilocalories_per_mole, sigma=0 * unit.angstroms, ) new_generics = { "Bonds": new_bond, "Angles": new_angle, "ProperTorsions": new_torsion, "vdW": new_vdw, } for key, val in new_generics.items(): forcefield.get_parameter_handler(key).parameters.insert(0, val) # This has to be removed as sqm will fail with unknown elements del forcefield._parameter_handlers["ToolkitAM1BCC"] del forcefield._parameter_handlers["Electrostatics"] # Parametrize the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology) return system