diff --git a/news/cuda.rst b/news/cuda.rst new file mode 100644 index 000000000..79fadd386 --- /dev/null +++ b/news/cuda.rst @@ -0,0 +1,24 @@ +**Added:** + +* + +**Changed:** + +* The default platform for OpenMM-based Protocols is now CUDA and will fail + by default on a non-Nvidia GPU enabled system. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/openfe/protocols/openmm_utils/omm_settings.py b/openfe/protocols/openmm_utils/omm_settings.py index 12ee31060..e3d3bc7ea 100644 --- a/openfe/protocols/openmm_utils/omm_settings.py +++ b/openfe/protocols/openmm_utils/omm_settings.py @@ -314,10 +314,13 @@ class OpenMMEngineSettings(SettingsBaseModel): * In the future make precision and deterministic forces user defined too. """ - compute_platform: Optional[str] = None + compute_platform: Optional[str] = 'cuda' """ OpenMM compute platform to perform MD integration with. If ``None``, will - choose fastest available platform. Default ``None``. + choose fastest available platform. + Allowed platforms are; ``cuda``, ``opencl``, ``cpu``. + Default ``cuda``. + """ gpu_device_index: Optional[list[int]] = None """ @@ -332,6 +335,15 @@ class OpenMMEngineSettings(SettingsBaseModel): Default ``None``. """ + @field_validator('compute_platform') + def supported_sampler(cls, v): + supported = ['cpu', 'opencl', 'cuda'] + if v is not None and v.lower() not in supported: + errmsg = ("Only the following OpenMM compute backends are " + f"supported: {supported}") + raise ValueError(errmsg) + return v + class IntegratorSettings(SettingsBaseModel): """Settings for the `LangevinDynamicsMove integrator `_. diff --git a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py index bbae1b15d..1d57a9eb7 100644 --- a/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py +++ b/openfe/tests/protocols/openmm_ahfe/test_ahfe_protocol.py @@ -29,6 +29,15 @@ ) +@pytest.fixture() +def protocol_dry_settings(): + settings = AbsoluteSolvationProtocol.default_settings() + settings.vacuum_engine_settings.compute_platform = None + settings.solvent_engine_settings.compute_platform = None + settings.protocol_repeats = 1 + return settings + + @pytest.fixture() def default_settings(): return AbsoluteSolvationProtocol.default_settings() @@ -55,14 +64,16 @@ def test_serialize_protocol(default_settings): @pytest.mark.parametrize('method', [ 'repex', 'sams', 'independent', 'InDePeNdENT' ]) -def test_dry_run_vac_benzene(benzene_modifications, - method, tmpdir): - s = openmm_afe.AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - s.vacuum_simulation_settings.sampler_method = method +def test_dry_run_vac_benzene( + benzene_modifications, + method, + protocol_dry_settings, + tmpdir +): + protocol_dry_settings.vacuum_simulation_settings.sampler_method = method protocol = openmm_afe.AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) stateA = ChemicalSystem({ @@ -98,13 +109,10 @@ def test_dry_run_vac_benzene(benzene_modifications, assert not vac_sampler.is_periodic -def test_confgen_fail_AFE(benzene_modifications, tmpdir): +def test_confgen_fail_AFE(benzene_modifications, protocol_dry_settings, tmpdir): # check system parametrisation works even if confgen fails - s = openmm_afe.AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - protocol = openmm_afe.AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) stateA = ChemicalSystem({ @@ -134,13 +142,13 @@ def test_confgen_fail_AFE(benzene_modifications, tmpdir): assert vac_sampler -def test_dry_run_solv_benzene(benzene_modifications, tmpdir): - s = openmm_afe.AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - s.solvent_output_settings.output_indices = "resname UNK" +def test_dry_run_solv_benzene( + benzene_modifications, protocol_dry_settings, tmpdir +): + protocol_dry_settings.solvent_output_settings.output_indices = "resname UNK" protocol = openmm_afe.AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) stateA = ChemicalSystem({ @@ -179,24 +187,24 @@ def test_dry_run_solv_benzene(benzene_modifications, tmpdir): assert pdb.n_atoms == 12 -def test_dry_run_solv_benzene_tip4p(benzene_modifications, tmpdir): - s = AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - s.vacuum_forcefield_settings.forcefields = [ +def test_dry_run_solv_benzene_tip4p( + benzene_modifications, protocol_dry_settings, tmpdir +): + protocol_dry_settings.vacuum_forcefield_settings.forcefields = [ "amber/ff14SB.xml", # ff14SB protein force field "amber/tip4pew_standard.xml", # FF we are testsing with the fun VS "amber/phosaa10.xml", # Handles THE TPO ] - s.solvent_forcefield_settings.forcefields = [ + protocol_dry_settings.solvent_forcefield_settings.forcefields = [ "amber/ff14SB.xml", # ff14SB protein force field "amber/tip4pew_standard.xml", # FF we are testsing with the fun VS "amber/phosaa10.xml", # Handles THE TPO ] - s.solvation_settings.solvent_model = 'tip4pew' - s.integrator_settings.reassign_velocities = True + protocol_dry_settings.solvation_settings.solvent_model = 'tip4pew' + protocol_dry_settings.integrator_settings.reassign_velocities = True protocol = AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) stateA = ChemicalSystem({ @@ -226,13 +234,12 @@ def test_dry_run_solv_benzene_tip4p(benzene_modifications, tmpdir): def test_dry_run_solv_benzene_noncubic( - benzene_modifications, tmpdir + benzene_modifications, protocol_dry_settings, tmpdir ): - s = AbsoluteSolvationProtocol.default_settings() - s.solvation_settings.solvent_padding = 1.5 * offunit.nanometer - s.solvation_settings.box_shape = 'dodecahedron' + protocol_dry_settings.solvation_settings.solvent_padding = 1.5 * offunit.nanometer + protocol_dry_settings.solvation_settings.box_shape = 'dodecahedron' - protocol = AbsoluteSolvationProtocol(settings=s) + protocol = AbsoluteSolvationProtocol(settings=protocol_dry_settings) stateA = ChemicalSystem({ 'benzene': benzene_modifications['benzene'], @@ -276,17 +283,16 @@ def test_dry_run_solv_benzene_noncubic( ) -def test_dry_run_solv_user_charges_benzene(benzene_modifications, tmpdir): +def test_dry_run_solv_user_charges_benzene( + benzene_modifications, protocol_dry_settings, tmpdir +): """ Create a test system with fictitious user supplied charges and ensure that they are properly passed through to the constructed alchemical system. """ - s = openmm_afe.AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - protocol = openmm_afe.AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) def assign_fictitious_charges(offmol): @@ -385,19 +391,18 @@ def assign_fictitious_charges(offmol): ), ]) def test_dry_run_charge_backends( - CN_molecule, tmpdir, method, backend, ref_key, am1bcc_ref_charges + CN_molecule, tmpdir, method, backend, ref_key, + protocol_dry_settings, am1bcc_ref_charges ): """ Check that partial charge generation with different backends works as expected. """ - s = openmm_afe.AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - s.partial_charge_settings.partial_charge_method = method - s.partial_charge_settings.off_toolkit_backend = backend - s.partial_charge_settings.nagl_model = 'openff-gnn-am1bcc-0.1.0-rc.1.pt' + protocol_dry_settings.partial_charge_settings.partial_charge_method = method + protocol_dry_settings.partial_charge_settings.off_toolkit_backend = backend + protocol_dry_settings.partial_charge_settings.nagl_model = 'openff-gnn-am1bcc-0.1.0-rc.1.pt' - protocol = openmm_afe.AbsoluteSolvationProtocol(settings=s) + protocol = openmm_afe.AbsoluteSolvationProtocol(settings=protocol_dry_settings) # Create ChemicalSystems stateA = ChemicalSystem({ @@ -441,14 +446,12 @@ def test_dry_run_charge_backends( ) -def test_high_timestep(benzene_modifications, tmpdir): - s = AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - s.solvent_forcefield_settings.hydrogen_mass = 1.0 - s.vacuum_forcefield_settings.hydrogen_mass = 1.0 +def test_high_timestep(benzene_modifications, protocol_dry_settings, tmpdir): + protocol_dry_settings.solvent_forcefield_settings.hydrogen_mass = 1.0 + protocol_dry_settings.vacuum_forcefield_settings.hydrogen_mass = 1.0 protocol = AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) stateA = ChemicalSystem({ @@ -474,11 +477,10 @@ def test_high_timestep(benzene_modifications, tmpdir): @pytest.fixture -def benzene_solvation_dag(benzene_modifications): - s = AbsoluteSolvationProtocol.default_settings() - +def benzene_solvation_dag(benzene_modifications, protocol_dry_settings): + protocol_dry_settings.protocol_repeats = 3 protocol = openmm_afe.AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) stateA = ChemicalSystem({ @@ -672,20 +674,21 @@ def test_filenotfound_replica_states(self, protocolresult): [[100 * offunit.picosecond, None], [None, None], [None, 100 * offunit.picosecond]]) -def test_dry_run_vacuum_write_frequency(benzene_modifications, - positions_write_frequency, - velocities_write_frequency, - tmpdir): - s = openmm_afe.AbsoluteSolvationProtocol.default_settings() - s.protocol_repeats = 1 - s.solvent_output_settings.output_indices = "resname UNK" - s.solvent_output_settings.positions_write_frequency = positions_write_frequency - s.solvent_output_settings.velocities_write_frequency = velocities_write_frequency - s.vacuum_output_settings.positions_write_frequency = positions_write_frequency - s.vacuum_output_settings.velocities_write_frequency = velocities_write_frequency +def test_dry_run_vacuum_write_frequency( + benzene_modifications, + positions_write_frequency, + velocities_write_frequency, + protocol_dry_settings, + tmpdir +): + protocol_dry_settings.solvent_output_settings.output_indices = "resname UNK" + protocol_dry_settings.solvent_output_settings.positions_write_frequency = positions_write_frequency + protocol_dry_settings.solvent_output_settings.velocities_write_frequency = velocities_write_frequency + protocol_dry_settings.vacuum_output_settings.positions_write_frequency = positions_write_frequency + protocol_dry_settings.vacuum_output_settings.velocities_write_frequency = velocities_write_frequency protocol = openmm_afe.AbsoluteSolvationProtocol( - settings=s, + settings=protocol_dry_settings, ) stateA = ChemicalSystem({ diff --git a/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py b/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py index 6296cba26..a56860d83 100644 --- a/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py +++ b/openfe/tests/protocols/openmm_md/test_plain_md_protocol.py @@ -26,6 +26,14 @@ from openfe.tests.conftest import HAS_ESPALOMA +@pytest.fixture() +def vac_settings(): + settings = PlainMDProtocol.default_settings() + settings.forcefield_settings.nonbonded_method = 'nocutoff' + settings.engine_settings.compute_platform = None + return settings + + def test_create_default_settings(): settings = PlainMDProtocol.default_settings() @@ -87,10 +95,7 @@ def test_create_independent_repeat_ids(benzene_system): assert len(repeat_ids) == 6 -def test_dry_run_default_vacuum(benzene_vacuum_system, tmpdir): - - vac_settings = PlainMDProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' +def test_dry_run_default_vacuum(benzene_vacuum_system, vac_settings, tmpdir): protocol = PlainMDProtocol( settings=vac_settings, @@ -112,10 +117,8 @@ def test_dry_run_default_vacuum(benzene_vacuum_system, tmpdir): protocol.settings.thermo_settings.temperature)).barostat is None -def test_dry_run_logger_output(benzene_vacuum_system, tmpdir, caplog): +def test_dry_run_logger_output(benzene_vacuum_system, vac_settings, tmpdir, caplog): - vac_settings = PlainMDProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' vac_settings.simulation_settings.equilibration_length_nvt = 1 * unit.picosecond vac_settings.simulation_settings.equilibration_length = 1 * unit.picosecond vac_settings.simulation_settings.production_length = 1 * unit.picosecond @@ -143,10 +146,8 @@ def test_dry_run_logger_output(benzene_vacuum_system, tmpdir, caplog): assert "running production phase" in messages -def test_dry_run_ffcache_none_vacuum(benzene_vacuum_system, tmpdir): +def test_dry_run_ffcache_none_vacuum(benzene_vacuum_system, vac_settings, tmpdir): - vac_settings = PlainMDProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' vac_settings.output_settings.forcefield_cache = None protocol = PlainMDProtocol( @@ -166,9 +167,7 @@ def test_dry_run_ffcache_none_vacuum(benzene_vacuum_system, tmpdir): dag_unit.run(dry=True)['debug']['system'] -def test_dry_run_gaff_vacuum(benzene_vacuum_system, tmpdir): - vac_settings = PlainMDProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' +def test_dry_run_gaff_vacuum(benzene_vacuum_system, vac_settings, tmpdir): vac_settings.forcefield_settings.small_molecule_forcefield = 'gaff-2.11' protocol = PlainMDProtocol( @@ -187,9 +186,7 @@ def test_dry_run_gaff_vacuum(benzene_vacuum_system, tmpdir): @pytest.mark.skipif(not HAS_ESPALOMA, reason='espaloma is not available') -def test_dry_run_espaloma_vacuum(benzene_vacuum_system, tmpdir): - vac_settings = PlainMDProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' +def test_dry_run_espaloma_vacuum(benzene_vacuum_system, vac_settings, tmpdir): vac_settings.forcefield_settings.small_molecule_forcefield = 'espaloma-0.3.2' protocol = PlainMDProtocol( @@ -230,10 +227,8 @@ def test_dry_run_espaloma_vacuum(benzene_vacuum_system, tmpdir): ), ]) def test_dry_run_charge_backends( - CN_molecule, tmpdir, method, backend, ref_key, am1bcc_ref_charges + CN_molecule, tmpdir, method, backend, ref_key, vac_settings, am1bcc_ref_charges ): - vac_settings = PlainMDProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' vac_settings.partial_charge_settings.partial_charge_method = method vac_settings.partial_charge_settings.off_toolkit_backend = backend vac_settings.partial_charge_settings.nagl_model = "openff-gnn-am1bcc-0.1.0-rc.1.pt" @@ -268,6 +263,7 @@ def test_dry_many_molecules_solvent( A basic test flushing "will it work if you pass multiple molecules" """ settings = PlainMDProtocol.default_settings() + settings.engine_settings.compute_platform = None protocol = PlainMDProtocol( settings=settings, @@ -357,6 +353,7 @@ def test_dry_run_ligand_tip4p(benzene_system, tmpdir): environment (waters) """ settings = PlainMDProtocol.default_settings() + settings.engine_settings.compute_platform = None settings.forcefield_settings.forcefields = [ "amber/ff14SB.xml", # ff14SB protein force field "amber/tip4pew_standard.xml", # FF we are testsing with the fun VS @@ -386,6 +383,7 @@ def test_dry_run_ligand_tip4p(benzene_system, tmpdir): def test_dry_run_complex(benzene_complex_system, tmpdir): # this will be very time consuming settings = PlainMDProtocol.default_settings() + settings.engine_settings.compute_platform = None protocol = PlainMDProtocol( settings=settings, diff --git a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py index a3c8b4708..f8fc034d7 100644 --- a/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py +++ b/openfe/tests/protocols/openmm_rfe/test_hybrid_top_protocol.py @@ -44,6 +44,23 @@ ) +@pytest.fixture() +def vac_settings(): + settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + settings.forcefield_settings.nonbonded_method = 'nocutoff' + settings.engine_settings.compute_platform = None + settings.protocol_repeats = 1 + return settings + + +@pytest.fixture() +def solv_settings(): + settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + settings.engine_settings.compute_platform = None + settings.protocol_repeats = 1 + return settings + + def test_compute_platform_warn(): with pytest.warns(UserWarning, match="Non-CUDA platform selected: CPU"): omm_compute.get_openmm_platform('CPU') @@ -188,13 +205,11 @@ def test_validate_alchemical_components_missing_alchem_comp( @pytest.mark.parametrize('method', [ 'repex', 'sams', 'independent', 'InDePeNdENT' ]) -def test_dry_run_default_vacuum(benzene_vacuum_system, toluene_vacuum_system, - benzene_to_toluene_mapping, method, tmpdir, benzene_toluene_topology): - - vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' +def test_dry_run_default_vacuum( + benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, + method, vac_settings, tmpdir, benzene_toluene_topology +): vac_settings.simulation_settings.sampler_method = method - vac_settings.protocol_repeats = 1 protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=vac_settings, @@ -248,10 +263,10 @@ def test_dry_run_default_vacuum(benzene_vacuum_system, toluene_vacuum_system, assert_allclose(new_positions.value_in_unit(omm_unit.angstrom), htf._new_positions.value_in_unit(omm_unit.angstrom)) -def test_dry_run_gaff_vacuum(benzene_vacuum_system, toluene_vacuum_system, - benzene_to_toluene_mapping, tmpdir): - vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' +def test_dry_run_gaff_vacuum( + benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, + vac_settings, tmpdir +): vac_settings.forcefield_settings.small_molecule_forcefield = 'gaff-2.11' protocol = openmm_rfe.RelativeHybridTopologyProtocol( @@ -271,16 +286,17 @@ def test_dry_run_gaff_vacuum(benzene_vacuum_system, toluene_vacuum_system, @pytest.mark.slow def test_dry_many_molecules_solvent( - benzene_many_solv_system, toluene_many_solv_system, - benzene_to_toluene_mapping, tmpdir + benzene_many_solv_system, + toluene_many_solv_system, + benzene_to_toluene_mapping, + solv_settings, + tmpdir ): """ A basic test flushing "will it work if you pass multiple molecules" """ - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=solv_settings, ) # create DAG from protocol and take first (and only) work unit from within @@ -361,7 +377,7 @@ def test_dry_many_molecules_solvent( """ -def test_dry_core_element_change(tmpdir): +def test_dry_core_element_change(vac_settings, tmpdir): benz = openfe.SmallMoleculeComponent(Chem.MolFromMolBlock(BENZ, removeHs=False)) pyr = openfe.SmallMoleculeComponent(Chem.MolFromMolBlock(PYRIDINE, removeHs=False)) @@ -371,11 +387,8 @@ def test_dry_core_element_change(tmpdir): {0: 0, 1: 10, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 8: 9, 9: 8, 10: 7, 11: 6} ) - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.forcefield_settings.nonbonded_method = 'nocutoff' - protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=vac_settings, ) dag = protocol.create( @@ -403,16 +416,16 @@ def test_dry_core_element_change(tmpdir): @pytest.mark.parametrize('method', ['repex', 'sams', 'independent']) -def test_dry_run_ligand(benzene_system, toluene_system, - benzene_to_toluene_mapping, method, tmpdir): +def test_dry_run_ligand( + benzene_system, toluene_system, + benzene_to_toluene_mapping, method, solv_settings, tmpdir +): # this might be a bit time consuming - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.simulation_settings.sampler_method = method - settings.protocol_repeats = 1 - settings.output_settings.output_indices = 'resname UNK' + solv_settings.simulation_settings.sampler_method = method + solv_settings.output_settings.output_indices = 'resname UNK' protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=solv_settings, ) dag = protocol.create( stateA=benzene_system, @@ -434,15 +447,14 @@ def test_dry_run_ligand(benzene_system, toluene_system, assert pdb.n_atoms == 16 -def test_confgen_mocked_fail(benzene_system, toluene_system, - benzene_to_toluene_mapping, tmpdir): +def test_confgen_mocked_fail( + benzene_system, toluene_system, + benzene_to_toluene_mapping, solv_settings, tmpdir +): """ Check that even if conformer generation fails, we can still perform a sim """ - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.protocol_repeats = 1 - - protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings=settings) + protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings=solv_settings) dag = protocol.create(stateA=benzene_system, stateB=toluene_system, mapping=benzene_to_toluene_mapping) @@ -464,6 +476,7 @@ def tip4p_hybrid_factory( Hybrid system with virtual sites in the environment (waters) """ settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + settings.engine_settings.compute_platform = None settings.forcefield_settings.forcefields = [ "amber/ff14SB.xml", # ff14SB protein force field "amber/tip4pew_standard.xml", # FF we are testsing with the fun VS @@ -578,17 +591,17 @@ def test_tip4p_check_vsite_parameters(tip4p_hybrid_factory): 0.9 * unit.nanometer] ) def test_dry_run_ligand_system_cutoff( - cutoff, benzene_system, toluene_system, benzene_to_toluene_mapping, tmpdir + cutoff, benzene_system, toluene_system, benzene_to_toluene_mapping, + solv_settings, tmpdir ): """ Test that the right nonbonded cutoff is propagated to the hybrid system. """ - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.solvation_settings.solvent_padding = 1.5 * unit.nanometer - settings.forcefield_settings.nonbonded_cutoff = cutoff + solv_settings.solvation_settings.solvent_padding = 1.5 * unit.nanometer + solv_settings.forcefield_settings.nonbonded_cutoff = cutoff protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=solv_settings, ) dag = protocol.create( stateA=benzene_system, @@ -633,11 +646,9 @@ def test_dry_run_ligand_system_cutoff( ), ]) def test_dry_run_charge_backends( - CN_molecule, tmpdir, method, backend, ref_key, am1bcc_ref_charges + CN_molecule, tmpdir, method, backend, ref_key, + vac_settings, am1bcc_ref_charges ): - vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' - vac_settings.protocol_repeats = 1 vac_settings.partial_charge_settings.partial_charge_method = method vac_settings.partial_charge_settings.off_toolkit_backend = backend vac_settings.partial_charge_settings.nagl_model = 'openff-gnn-am1bcc-0.1.0-rc.1.pt' @@ -702,16 +713,12 @@ def test_dry_run_charge_backends( @pytest.mark.flaky(reruns=3) # bad minimisation can happen -def test_dry_run_user_charges(benzene_modifications, tmpdir): +def test_dry_run_user_charges(benzene_modifications, vac_settings, tmpdir): """ Create a hybrid system with a set of fictitious user supplied charges and ensure that they are properly passed through to the constructed hybrid topology. """ - vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' - vac_settings.protocol_repeats = 1 - protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=vac_settings, ) @@ -828,26 +835,27 @@ def check_propchgs(smc, charge_array): np.testing.assert_allclose(c_offsets[i], c_exp) -def test_virtual_sites_no_reassign(benzene_system, toluene_system, - benzene_to_toluene_mapping, tmpdir): +def test_virtual_sites_no_reassign( + benzene_system, toluene_system, + benzene_to_toluene_mapping, solv_settings, tmpdir +): """ Because of some as-of-yet not fully identified issue, not reassigning velocities will cause systems to NaN. See https://github.com/choderalab/openmmtools/issues/695 """ - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.forcefield_settings.forcefields = [ + solv_settings.forcefield_settings.forcefields = [ "amber/ff14SB.xml", # ff14SB protein force field "amber/tip4pew_standard.xml", # FF we are testsing with the fun VS "amber/phosaa10.xml", # Handles THE TPO ] - settings.solvation_settings.solvent_padding = 1.0 * unit.nanometer - settings.forcefield_settings.nonbonded_cutoff = 0.9 * unit.nanometer - settings.solvation_settings.solvent_model = 'tip4pew' - settings.integrator_settings.reassign_velocities = False + solv_settings.solvation_settings.solvent_padding = 1.0 * unit.nanometer + solv_settings.forcefield_settings.nonbonded_cutoff = 0.9 * unit.nanometer + solv_settings.solvation_settings.solvent_model = 'tip4pew' + solv_settings.integrator_settings.reassign_velocities = False protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=solv_settings, ) dag = protocol.create( stateA=benzene_system, @@ -862,15 +870,16 @@ def test_virtual_sites_no_reassign(benzene_system, toluene_system, dag_unit.run(dry=True) -def test_dodecahdron_ligand_box(benzene_system, toluene_system, - benzene_to_toluene_mapping, tmpdir): +def test_dodecahdron_ligand_box( + benzene_system, toluene_system, benzene_to_toluene_mapping, + solv_settings, tmpdir +): """ Test that a hybrid system with a dodechadron is built properly. """ - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.solvation_settings.solvent_padding = 1.5 * unit.nanometer - settings.solvation_settings.box_shape = 'dodecahedron' - protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings=settings) + solv_settings.solvation_settings.solvent_padding = 1.5 * unit.nanometer + solv_settings.solvation_settings.box_shape = 'dodecahedron' + protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings=solv_settings) dag = protocol.create( stateA=benzene_system, @@ -902,16 +911,16 @@ def test_dodecahdron_ligand_box(benzene_system, toluene_system, @pytest.mark.slow @pytest.mark.parametrize('method', ['repex', 'sams', 'independent']) -def test_dry_run_complex(benzene_complex_system, toluene_complex_system, - benzene_to_toluene_mapping, method, tmpdir): +def test_dry_run_complex( + benzene_complex_system, toluene_complex_system, + benzene_to_toluene_mapping, method, solv_settings, tmpdir +): # this will be very time consuming - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.simulation_settings.sampler_method = method - settings.protocol_repeats = 1 - settings.output_settings.output_indices = 'protein or resname UNK' + solv_settings.simulation_settings.sampler_method = method + solv_settings.output_settings.output_indices = 'protein or resname UNK' protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=solv_settings, ) dag = protocol.create( stateA=benzene_complex_system, @@ -945,15 +954,17 @@ def test_lambda_schedule(windows): assert len(lambdas.lambda_schedule) == windows -def test_hightimestep(benzene_vacuum_system, - toluene_vacuum_system, - benzene_to_toluene_mapping, tmpdir): - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.forcefield_settings.hydrogen_mass = 1.0 - settings.forcefield_settings.nonbonded_method = 'nocutoff' +def test_hightimestep( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, + tmpdir +): + vac_settings.forcefield_settings.hydrogen_mass = 1.0 p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=vac_settings, ) dag = p.create( @@ -969,15 +980,17 @@ def test_hightimestep(benzene_vacuum_system, dag_unit.run(dry=True) -def test_n_replicas_not_n_windows(benzene_vacuum_system, - toluene_vacuum_system, - benzene_to_toluene_mapping, tmpdir): +def test_n_replicas_not_n_windows( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + vac_settings, + tmpdir +): # For PR #125 we pin such that the number of lambda windows # equals the numbers of replicas used - TODO: remove limitation - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() # default lambda windows is 11 - settings.simulation_settings.n_replicas = 13 - settings.forcefield_settings.nonbonded_method = 'nocutoff' + vac_settings.simulation_settings.n_replicas = 13 errmsg = ("Number of replicas 13 does not equal the number of " "lambda windows 11") @@ -985,7 +998,7 @@ def test_n_replicas_not_n_windows(benzene_vacuum_system, with tmpdir.as_cwd(): with pytest.raises(ValueError, match=errmsg): p = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=vac_settings, ) dag = p.create( stateA=benzene_vacuum_system, @@ -1178,11 +1191,10 @@ def test_element_change_warning(atom_mapping_basic_test_files): ) -def test_ligand_overlap_warning(benzene_vacuum_system, toluene_vacuum_system, - benzene_to_toluene_mapping, tmpdir): - vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' - +def test_ligand_overlap_warning( + benzene_vacuum_system, toluene_vacuum_system, benzene_to_toluene_mapping, + vac_settings, tmpdir +): protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=vac_settings, ) @@ -1521,13 +1533,14 @@ def tyk2_xml(tmp_path_factory): 29: 23, 30: 24, 31: 25, 32: 26, 33: 27} ) - settings: openmm_rfe.RelativeHybridTopologyProtocolSettings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.forcefield_settings.small_molecule_forcefield = 'openff-2.0.0' - settings.forcefield_settings.nonbonded_method = 'nocutoff' - settings.forcefield_settings.hydrogen_mass = 3.0 - settings.protocol_repeats = 1 + vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() + vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' + vac_settings.engine_settings.compute_platform = None + vac_settings.forcefield_settings.small_molecule_forcefield = 'openff-2.0.0' + vac_settings.forcefield_settings.hydrogen_mass = 3.0 + vac_settings.protocol_repeats = 1 - protocol = openmm_rfe.RelativeHybridTopologyProtocol(settings) + protocol = openmm_rfe.RelativeHybridTopologyProtocol(vac_settings) dag = protocol.create( stateA=openfe.ChemicalSystem({'ligand': lig23}), @@ -2070,7 +2083,9 @@ def _assert_total_charge(system, atom_classes, chgA, chgB): assert chgB == pytest.approx(np.sum(stateB_charges)) -def test_dry_run_alchemwater_solvent(benzene_to_benzoic_mapping, tmpdir): +def test_dry_run_alchemwater_solvent( + benzene_to_benzoic_mapping, solv_settings, tmpdir +): stateA_system = openfe.ChemicalSystem( {'ligand': benzene_to_benzoic_mapping.componentA, 'solvent': openfe.SolventComponent()} @@ -2079,7 +2094,6 @@ def test_dry_run_alchemwater_solvent(benzene_to_benzoic_mapping, tmpdir): {'ligand': benzene_to_benzoic_mapping.componentB, 'solvent': openfe.SolventComponent()} ) - solv_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() solv_settings.alchemical_settings.explicit_charge_correction = True protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=solv_settings, @@ -2116,7 +2130,7 @@ def test_dry_run_alchemwater_solvent(benzene_to_benzoic_mapping, tmpdir): ]) def test_dry_run_complex_alchemwater_totcharge( mapping_name, chgA, chgB, correction, core_atoms, - new_uniq, old_uniq, tmpdir, request, T4_protein_component, + new_uniq, old_uniq, tmpdir, request, T4_protein_component, solv_settings ): mapping = request.getfixturevalue(mapping_name) solvent = openfe.SolventComponent() @@ -2131,13 +2145,12 @@ def test_dry_run_complex_alchemwater_totcharge( 'protein': T4_protein_component} ) - settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - settings.solvation_settings.solvent_padding="0.9 nm" - settings.solvation_settings.box_shape="dodecahedron" - settings.alchemical_settings.explicit_charge_correction = correction + solv_settings.solvation_settings.solvent_padding="0.9 nm" + solv_settings.solvation_settings.box_shape="dodecahedron" + solv_settings.alchemical_settings.explicit_charge_correction = correction protocol = openmm_rfe.RelativeHybridTopologyProtocol( - settings=settings, + settings=solv_settings, ) # create DAG from protocol and take first (and only) work unit from within @@ -2174,19 +2187,17 @@ def test_structural_analysis_error(tmpdir): [[100 * unit.picosecond, None], [None, None], [None, 100 * unit.picosecond]]) -def test_dry_run_vacuum_write_frequency(benzene_vacuum_system, - toluene_vacuum_system, - benzene_to_toluene_mapping, - positions_write_frequency, - velocities_write_frequency, - tmpdir, - ): - - vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' +def test_dry_run_vacuum_write_frequency( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + positions_write_frequency, + velocities_write_frequency, + vac_settings, + tmpdir, +): vac_settings.output_settings.positions_write_frequency = positions_write_frequency vac_settings.output_settings.velocities_write_frequency = velocities_write_frequency - vac_settings.protocol_repeats = 1 protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=vac_settings, @@ -2216,19 +2227,18 @@ def test_dry_run_vacuum_write_frequency(benzene_vacuum_system, @pytest.mark.parametrize('positions_write_frequency,velocities_write_frequency', [[100.1 * unit.picosecond, 100 * unit.picosecond], [100 * unit.picosecond, 100.1 * unit.picosecond]]) -def test_pos_write_frequency_not_divisible(benzene_vacuum_system, - toluene_vacuum_system, - benzene_to_toluene_mapping, - positions_write_frequency, - velocities_write_frequency, - tmpdir, - ): +def test_pos_write_frequency_not_divisible( + benzene_vacuum_system, + toluene_vacuum_system, + benzene_to_toluene_mapping, + positions_write_frequency, + velocities_write_frequency, + tmpdir, + vac_settings, +): - vac_settings = openmm_rfe.RelativeHybridTopologyProtocol.default_settings() - vac_settings.forcefield_settings.nonbonded_method = 'nocutoff' vac_settings.output_settings.positions_write_frequency = positions_write_frequency vac_settings.output_settings.velocities_write_frequency = velocities_write_frequency - vac_settings.protocol_repeats = 1 protocol = openmm_rfe.RelativeHybridTopologyProtocol( settings=vac_settings, diff --git a/openfe/tests/protocols/openmm_septop/test_septop_protocol.py b/openfe/tests/protocols/openmm_septop/test_septop_protocol.py index f2320154f..24be9da31 100644 --- a/openfe/tests/protocols/openmm_septop/test_septop_protocol.py +++ b/openfe/tests/protocols/openmm_septop/test_septop_protocol.py @@ -49,9 +49,19 @@ ONE_4PI_EPS0 = 1 / (4 * np.pi * EPSILON0) * EPSILON0.unit * 10.0 # nm -> angstrom +@pytest.fixture() +def protocol_dry_settings(): + # a set of settings for dry run tests + s = SepTopProtocol.default_settings() + s.engine_settings.compute_platform = None + s.protocol_repeats = 1 + return s + + @pytest.fixture() def default_settings(): - return SepTopProtocol.default_settings() + s = SepTopProtocol.default_settings() + return s def test_create_default_settings(): @@ -637,10 +647,9 @@ def test_two_ligands_charges(self, three_particle_system): def benzene_toluene_dag( benzene_complex_system, toluene_complex_system, - default_settings, + protocol_dry_settings, ): - default_settings.protocol_repeats = 1 - protocol = SepTopProtocol(settings=default_settings) + protocol = SepTopProtocol(settings=protocol_dry_settings) return protocol.create( stateA=benzene_complex_system, @@ -710,18 +719,17 @@ def test_dry_run_methods( benzene_complex_system, toluene_complex_system, tmpdir, - default_settings, + protocol_dry_settings, method, ): - default_settings.solvent_simulation_settings.sampler_method = method - default_settings.complex_simulation_settings.sampler_method = method - default_settings.protocol_repeats = 1 - default_settings.complex_output_settings.output_indices = 'resname UNK' - default_settings.solvent_output_settings.output_indices = 'resname UNK' + protocol_dry_settings.solvent_simulation_settings.sampler_method = method + protocol_dry_settings.complex_simulation_settings.sampler_method = method + protocol_dry_settings.complex_output_settings.output_indices = 'resname UNK' + protocol_dry_settings.solvent_output_settings.output_indices = 'resname UNK' protocol = SepTopProtocol( - settings=default_settings, + settings=protocol_dry_settings, ) dag = protocol.create( stateA=benzene_complex_system, @@ -765,16 +773,16 @@ def test_dry_run_ligand_system_pressure( benzene_complex_system, toluene_complex_system, tmpdir, - default_settings, + protocol_dry_settings, ): """ Test that the right nonbonded cutoff is propagated to the system. """ # openfe settings requires openff/pint units - default_settings.thermo_settings.pressure = pressure * offunit.bar + protocol_dry_settings.thermo_settings.pressure = pressure * offunit.bar protocol = SepTopProtocol( - settings=default_settings, + settings=protocol_dry_settings, ) dag = protocol.create( stateA=benzene_complex_system, @@ -801,22 +809,21 @@ def test_virtual_sites_no_reassign( benzene_complex_system, toluene_complex_system, tmpdir, - default_settings, + protocol_dry_settings, ): """ Test that an error is raised when not reassigning velocities in a system with virtual site. """ - default_settings.protocol_repeats = 1 - default_settings.forcefield_settings.forcefields = [ + protocol_dry_settings.forcefield_settings.forcefields = [ "amber/ff14SB.xml", "amber/tip4pew_standard.xml", # FF with VS ] - default_settings.solvent_solvation_settings.solvent_model = 'tip4pew' - default_settings.integrator_settings.reassign_velocities = False + protocol_dry_settings.solvent_solvation_settings.solvent_model = 'tip4pew' + protocol_dry_settings.integrator_settings.reassign_velocities = False protocol = SepTopProtocol( - settings=default_settings, + settings=protocol_dry_settings, ) dag = protocol.create( stateA=benzene_complex_system, @@ -841,18 +848,18 @@ def test_dry_run_ligand_system_cutoff( benzene_complex_system, toluene_complex_system, tmpdir, - default_settings, + protocol_dry_settings, ): """ Test that the right nonbonded cutoff is propagated to the system. """ - default_settings.solvent_solvation_settings.solvent_padding = ( + protocol_dry_settings.solvent_solvation_settings.solvent_padding = ( 1.9 * offunit.nanometer ) - default_settings.forcefield_settings.nonbonded_cutoff = cutoff + protocol_dry_settings.forcefield_settings.nonbonded_cutoff = cutoff protocol = SepTopProtocol( - settings=default_settings, + settings=protocol_dry_settings, ) dag = protocol.create( stateA=benzene_complex_system, @@ -881,18 +888,17 @@ def test_dry_run_benzene_toluene_tip4p( benzene_complex_system, toluene_complex_system, tmpdir, - default_settings, + protocol_dry_settings, ): - default_settings.protocol_repeats = 1 - default_settings.forcefield_settings.forcefields = [ + protocol_dry_settings.forcefield_settings.forcefields = [ "amber/ff14SB.xml", # ff14SB protein force field "amber/tip4pew_standard.xml", # FF we are testsing with the fun VS "amber/phosaa10.xml", # Handles THE TPO ] - default_settings.solvent_solvation_settings.solvent_model = "tip4pew" - default_settings.integrator_settings.reassign_velocities = True + protocol_dry_settings.solvent_solvation_settings.solvent_model = "tip4pew" + protocol_dry_settings.integrator_settings.reassign_velocities = True - protocol = SepTopProtocol(settings=default_settings) + protocol = SepTopProtocol(settings=protocol_dry_settings) # Create DAG from protocol, get the solvent units # and eventually dry run the first solvent unit @@ -926,15 +932,14 @@ def test_dry_run_benzene_toluene_noncubic( benzene_complex_system, toluene_complex_system, tmpdir, - default_settings, + protocol_dry_settings, ): - default_settings.protocol_repeats = 1 - default_settings.solvent_solvation_settings.solvent_padding = ( + protocol_dry_settings.solvent_solvation_settings.solvent_padding = ( 1.5 * offunit.nanometer ) - default_settings.solvent_solvation_settings.box_shape = "dodecahedron" + protocol_dry_settings.solvent_solvation_settings.box_shape = "dodecahedron" - protocol = SepTopProtocol(settings=default_settings) + protocol = SepTopProtocol(settings=protocol_dry_settings) # Create DAG from protocol, get the vacuum and solvent units # and eventually dry run the first solvent unit @@ -977,16 +982,14 @@ def test_dry_run_solv_user_charges_benzene_toluene( benzene_modifications, T4_protein_component, tmpdir, - default_settings, + protocol_dry_settings, ): """ Create a test system with fictitious user supplied charges and ensure that they are properly passed through to the constructed alchemical system. """ - default_settings.protocol_repeats = 1 - - protocol = SepTopProtocol(settings=default_settings) + protocol = SepTopProtocol(settings=protocol_dry_settings) def assign_fictitious_charges(offmol): """ @@ -1091,13 +1094,12 @@ def test_high_timestep( benzene_complex_system, toluene_complex_system, tmpdir, - default_settings, + protocol_dry_settings, ): - default_settings.protocol_repeats = 1 - default_settings.forcefield_settings.hydrogen_mass = 1.0 - default_settings.forcefield_settings.hydrogen_mass = 1.0 + protocol_dry_settings.forcefield_settings.hydrogen_mass = 1.0 + protocol_dry_settings.forcefield_settings.hydrogen_mass = 1.0 - protocol = SepTopProtocol(settings=default_settings) + protocol = SepTopProtocol(settings=protocol_dry_settings) dag = protocol.create( stateA=benzene_complex_system, @@ -1117,14 +1119,14 @@ def T4L_xml( benzene_complex_system, toluene_complex_system, tmp_path_factory, - default_settings, + protocol_dry_settings, ): # Fixing the number of solvent molecules in the solvent settings # to test against reference xml - default_settings.solvent_solvation_settings.solvent_padding = None - default_settings.solvent_solvation_settings.number_of_solvent_molecules = 364 - default_settings.forcefield_settings.small_molecule_forcefield = "openff-2.1.1" - protocol = SepTopProtocol(settings=default_settings) + protocol_dry_settings.solvent_solvation_settings.solvent_padding = None + protocol_dry_settings.solvent_solvation_settings.number_of_solvent_molecules = 364 + protocol_dry_settings.forcefield_settings.small_molecule_forcefield = "openff-2.1.1" + protocol = SepTopProtocol(settings=protocol_dry_settings) dag = protocol.create( stateA=benzene_complex_system, diff --git a/openfe/tests/protocols/openmm_septop/test_septop_slow.py b/openfe/tests/protocols/openmm_septop/test_septop_slow.py index 7f1639da6..17e8bc34a 100644 --- a/openfe/tests/protocols/openmm_septop/test_septop_slow.py +++ b/openfe/tests/protocols/openmm_septop/test_septop_slow.py @@ -22,7 +22,9 @@ @pytest.fixture() def default_settings(): - return SepTopProtocol.default_settings() + s = SepTopProtocol.default_settings() + s.engine_settings.compute_platform = None + return s def compare_energies(alchemical_system, positions): diff --git a/openfe/tests/protocols/test_openmm_settings.py b/openfe/tests/protocols/test_openmm_settings.py index 06a11870d..a8bbceadf 100644 --- a/openfe/tests/protocols/test_openmm_settings.py +++ b/openfe/tests/protocols/test_openmm_settings.py @@ -94,3 +94,15 @@ def test_box_size_properties_non_1d(self): with pytest.raises(ValueError, match="must be a 1-D array"): s.box_size = np.array([[1, 2, 3], [1, 2, 3]]) * unit.angstrom + + +class TestOpenMMEngineSettings: + @pytest.mark.parametrize('platform', ['CUDA', 'OpenCL', 'cPu']) + def test_ok_platforms(self, platform): + s = omm_settings.OpenMMEngineSettings(compute_platform=platform) + assert isinstance(s, omm_settings.OpenMMEngineSettings) + + def test_fail_platform(self): + with pytest.raises(ValueError, match="OpenMM compute backends"): + _ = omm_settings.OpenMMEngineSettings(compute_platform="foo") +