diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 331b79ac..3ae7ec9d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ concurrency: jobs: test: - name: Test on ${{ matrix.os }}, Python ${{ matrix.python-version }}, Latest openff-toolkit ${{ matrix.latest-openff-toolkit }} + name: Test on ${{ matrix.os }}, Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} env: OE_LICENSE: ${{ github.workspace }}/oe_license.txt @@ -28,8 +28,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10"] - latest-openff-toolkit: [true, false] + python-version: ["3.9", "3.10"] # Add 3.11 in with AmberTools 23 exclude: - python-version: "3.10" os: macos-latest @@ -38,10 +37,10 @@ jobs: - uses: actions/checkout@v3 - name: Setup Conda Environment - uses: mamba-org/provision-with-micromamba@main + uses: mamba-org/setup-micromamba@v1 with: environment-file: devtools/conda-envs/test_env.yaml - extra-specs: | + create-args: >- python=${{ matrix.python-version }} - name: License OpenEye @@ -52,16 +51,6 @@ jobs: env: SECRET_OE_LICENSE: ${{ secrets.OE_LICENSE }} - - name: "Install openff-toolkit >= 0.11 API changes" - if: ${{ matrix.latest-openff-toolkit == true }} - run: | - micromamba update -y -c conda-forge "openff-toolkit >=0.11.3" - - - name: "Install openff-toolkit < 0.11 API changes" - if: ${{ matrix.latest-openff-toolkit == false }} - run: | - micromamba install -y -c conda-forge "openff-toolkit==0.10.6" "openff-toolkit-base==0.10.6" - - name: Install Package run: | pip list @@ -77,10 +66,11 @@ jobs: - name: Test Installed Package run: | - pytest -v -x --log-cli-level $LOGLEVEL $COV_ARGS --durations=20 \ + pytest -v --log-cli-level $LOGLEVEL $COV_ARGS --durations=20 \ openmmforcefields/tests/test_amber_import.py \ openmmforcefields/tests/test_template_generators.py \ - openmmforcefields/tests/test_system_generator.py + openmmforcefields/tests/test_system_generator.py \ + -k "not TestEspalomaTemplateGenerator" env: COV_ARGS: --cov=openmmforcefields --cov-config=setup.cfg --cov-append --cov-report=xml LOGLEVEL: "INFO" @@ -106,7 +96,6 @@ jobs: working-directory: ./charmm - name: Run docstrings - if: ${{ matrix.latest-openff-toolkit == true }} continue-on-error: True run: | pytest --doctest-modules openmmforcefields --ignore=openmmforcefields/tests diff --git a/.github/workflows/espaloma_ci.yaml b/.github/workflows/espaloma_ci.yaml new file mode 100644 index 00000000..394dffd3 --- /dev/null +++ b/.github/workflows/espaloma_ci.yaml @@ -0,0 +1,66 @@ +name: EspalomaCI + +on: + push: + branches: + - "main" + pull_request: + branches: + - "main" + schedule: + - cron: "0 0 * * *" + +defaults: + run: + shell: bash -l {0} + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Test on ${{ matrix.os }}, Python ${{ matrix.python-version }}, Latest openff-toolkit ${{ matrix.latest-openff-toolkit }} + runs-on: ${{ matrix.os }} + env: + OE_LICENSE: ${{ github.workspace }}/oe_license.txt + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ["3.9", "3.10"] # Add 3.11 in with AmberTools 23 + exclude: + - python-version: "3.10" + os: macos-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Conda Environment + uses: mamba-org/setup-micromamba@v1 + with: + environment-file: devtools/conda-envs/test_env.yaml + create-args: >- + python=${{ matrix.python-version }} + + - name: Install Package + run: | + pip list + micromamba list + micromamba remove --force openmmforcefields + python -m pip install . + + - name: Conda Environment Information + run: | + micromamba info + micromamba list + python -c "from openmmforcefields import __version__, __file__; print(__version__, __file__)" + + - name: Test Installed Package + run: | + pytest -v --log-cli-level $LOGLEVEL $COV_ARGS --durations=20 \ + -m "espaloma" openmmforcefields/tests --runespaloma + env: + COV_ARGS: --cov=openmmforcefields --cov-config=setup.cfg --cov-append --cov-report=xml + LOGLEVEL: "INFO" + KMP_DUPLICATE_LIB_OK: "True" diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 040ad212..adb60eb2 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -14,17 +14,17 @@ dependencies: # Testing - pytest - pytest-cov - - codecov + - pytest-xdist + - pytest-randomly # Requirements for converted force field installer - openmm >=7.6.0 - - openff-units >=0.1.8 - - openff-amber-ff-ports >=0.0.3 + - openff-toolkit >=0.11 # Requirements for conversion tools - pyyaml - - ambertools >=18.0 # contains sufficiently recent ParmEd + - ambertools =22 - lxml - networkx @@ -41,6 +41,6 @@ dependencies: # TODO: Rework this once espaloma is on conda-forge # - pytorch >=1.8.0 - - dgl + - dgl <1 - qcportal >=0.15.0 - espaloma diff --git a/openmmforcefields/ffxml/amber/opc3.xml b/openmmforcefields/ffxml/amber/opc3.xml index 6be08369..f21f3e89 100644 --- a/openmmforcefields/ffxml/amber/opc3.xml +++ b/openmmforcefields/ffxml/amber/opc3.xml @@ -17,7 +17,7 @@ - + diff --git a/openmmforcefields/generators/template_generators.py b/openmmforcefields/generators/template_generators.py index cd1d8db5..3f5d2755 100644 --- a/openmmforcefields/generators/template_generators.py +++ b/openmmforcefields/generators/template_generators.py @@ -1280,7 +1280,7 @@ def __init__(self, molecules=None, cache=None, forcefield=None, **kwargs): self._smirnoff_forcefield = openff.toolkit.typing.engines.smirnoff.ForceField(filename) except Exception as e: _logger.error(e) - raise ValueError(f"Can't find specified SMIRNOFF force field ({forcefield}) in install paths") + raise ValueError(f"Can't find specified SMIRNOFF force field ({forcefield}) in install paths") from e # Delete constraints, if present if 'Constraints' in self._smirnoff_forcefield._parameter_handlers: diff --git a/openmmforcefields/tests/conftest.py b/openmmforcefields/tests/conftest.py new file mode 100644 index 00000000..cbbb0949 --- /dev/null +++ b/openmmforcefields/tests/conftest.py @@ -0,0 +1,21 @@ +"""Default configuration and objects for tests""" + +import pytest + +def pytest_addoption(parser): + parser.addoption( + "--runespaloma", action="store_true", default=False, help="run espaloma tests" + ) + + +def pytest_configure(config): + config.addinivalue_line("markers", "espaloma: mark test as slow to run") + + +def pytest_collection_modifyitems(config, items): + skip_slow = pytest.mark.skip(reason="need --runespaloma option to run") + + if not config.getoption("--runespaloma"): + for item in items: + if "espaloma" in item.keywords: + item.add_marker(skip_slow) \ No newline at end of file diff --git a/openmmforcefields/tests/test_system_generator.py b/openmmforcefields/tests/test_system_generator.py index 7e24e40a..c00116ed 100644 --- a/openmmforcefields/tests/test_system_generator.py +++ b/openmmforcefields/tests/test_system_generator.py @@ -18,7 +18,90 @@ # Tests ################################################################################ -class TestSystemGenerator(unittest.TestCase): +@pytest.fixture(scope="class", autouse=True) + + +def test_systems(): + testsystems = dict() + for (system_name, prefix) in [ + # TODO: Uncomment these after we fix input files + ('bace', 'Bace'), + # ('cdk1', 'CDK2'), + # ('jnk1', 'Jnk1'), + # ('mcl1', 'MCL1'), + # ('p38', 'p38'), + # ('ptp1b', 'PTP1B'), + # ('thrombin', 'Thrombin'), + # ('tyk2', 'Tyk2'), + ]: + # Load protein + pdb_filename = get_data_filename(os.path.join('perses_jacs_systems', system_name, prefix + '_protein.pdb')) + pdbfile = PDBFile(pdb_filename) + + # Load molecules + sdf_filename = get_data_filename( + os.path.join('perses_jacs_systems', system_name, prefix + '_ligands_shifted.sdf')) + + molecules = Molecule.from_file(sdf_filename, allow_undefined_stereo=True) + print(f'Read {len(molecules)} molecules from {sdf_filename}') + n_molecules = len(molecules) + + # Limit number of molecules for testing + MAX_MOLECULES = 10 if not CI else 2 + if (n_molecules > MAX_MOLECULES): + print(f'Limiting to {MAX_MOLECULES} for testing...') + n_molecules = MAX_MOLECULES + molecules = [molecules[index] for index in range(n_molecules)] + + # Create structures + import parmed + + # NOTE: This does not work because parmed does not correctly assign bonds for HID + # protein_structure = parmed.load_file(pdb_filename) + # NOTE: This is the workaround + protein_structure = parmed.openmm.load_topology(pdbfile.topology, xyz=pdbfile.positions) + + molecules_structure = parmed.load_file(sdf_filename) + molecules_structure = [molecules_structure[index] for index in range(n_molecules)] + + complex_structures = [(molecules_structure[index] + protein_structure) for index in range(n_molecules)] + complex_structures = [molecules_structure[index] for index in range(n_molecules)] # DEBUG + + # Store + testsystem = { + 'name': system_name, + 'protein_pdbfile': pdbfile, + 'molecules': molecules, + 'complex_structures': complex_structures + } + testsystems[system_name] = testsystem + + # DEBUG + for name, testsystem in testsystems.items(): + filename = f'testsystem-{name}.pdb' + print(filename) + structure = testsystem['complex_structures'][0] + # structure.save(filename, overwrite=True) + with open(filename, 'w') as outfile: + PDBFile.writeFile(structure.topology, structure.positions, outfile) + testsystem['molecules'][0].to_file(f'testsystem-{name}-molecule.sdf', file_format="SDF") + testsystem['molecules'][0].to_file(f'testsystem-{name}-molecule.pdb', file_format="PDB") + + # TODO: Create other test topologies + # TODO: Protein-only + # TODO: Protein-ligand topology + # TODO: Solvated protein-ligand topology + # TODO: Host-guest topology + # Suppress DEBUG logging from various packages + + import logging + for name in ['parmed', 'matplotlib']: + logging.getLogger(name).setLevel(logging.WARNING) + + return testsystems + + +class TestSystemGenerator(object): # AMBER force field combination to test amber_forcefields = ['amber/protein.ff14SB.xml', 'amber/tip3p_standard.xml', 'amber/tip3p_HFE_multivalent.xml'] @@ -47,83 +130,6 @@ def filter_molecules(self, molecules): return molecules - """Base class for SystemGenerator tests.""" - def setUp(self): - self.testsystems = dict() - for (system_name, prefix) in [ - # TODO: Uncomment these after we fix input files - ('bace', 'Bace'), - #('cdk1', 'CDK2'), - #('jnk1', 'Jnk1'), - #('mcl1', 'MCL1'), - #('p38', 'p38'), - #('ptp1b', 'PTP1B'), - #('thrombin', 'Thrombin'), - #('tyk2', 'Tyk2'), - ]: - # Load protein - pdb_filename = get_data_filename(os.path.join('perses_jacs_systems', system_name, prefix + '_protein.pdb')) - pdbfile = PDBFile(pdb_filename) - - # Load molecules - sdf_filename = get_data_filename(os.path.join('perses_jacs_systems', system_name, prefix + '_ligands_shifted.sdf')) - - molecules = Molecule.from_file(sdf_filename, allow_undefined_stereo=True) - print(f'Read {len(molecules)} molecules from {sdf_filename}') - n_molecules = len(molecules) - - # Limit number of molecules for testing - MAX_MOLECULES = 10 if not CI else 2 - if (n_molecules > MAX_MOLECULES): - print(f'Limiting to {MAX_MOLECULES} for testing...') - n_molecules = MAX_MOLECULES - molecules = [ molecules[index] for index in range(n_molecules) ] - - # Create structures - import parmed - - # NOTE: This does not work because parmed does not correctly assign bonds for HID - #protein_structure = parmed.load_file(pdb_filename) - # NOTE: This is the workaround - protein_structure = parmed.openmm.load_topology(pdbfile.topology, xyz=pdbfile.positions) - - molecules_structure = parmed.load_file(sdf_filename) - molecules_structure = [ molecules_structure[index] for index in range(n_molecules) ] - - complex_structures = [ (molecules_structure[index] + protein_structure) for index in range(n_molecules) ] - complex_structures = [ molecules_structure[index] for index in range(n_molecules) ] # DEBUG - - # Store - testsystem = { - 'name' : system_name, - 'protein_pdbfile' : pdbfile, - 'molecules' : molecules, - 'complex_structures' : complex_structures - } - self.testsystems[system_name] = testsystem - - # DEBUG - for name, testsystem in self.testsystems.items(): - filename = f'testsystem-{name}.pdb' - print(filename) - structure = testsystem['complex_structures'][0] - #structure.save(filename, overwrite=True) - with open(filename, 'w') as outfile: - PDBFile.writeFile(structure.topology, structure.positions, outfile) - testsystem['molecules'][0].to_file(f'testsystem-{name}-molecule.sdf', file_format="SDF") - testsystem['molecules'][0].to_file(f'testsystem-{name}-molecule.pdb', file_format="PDB") - - # TODO: Create other test topologies - # TODO: Protein-only - # TODO: Protein-ligand topology - # TODO: Solvated protein-ligand topology - # TODO: Host-guest topology - # Suppress DEBUG logging from various packages - - import logging - for name in ['parmed', 'matplotlib']: - logging.getLogger(name).setLevel(logging.WARNING) - def test_create(self): """Test SystemGenerator creation with only OpenMM ffxml force fields""" # Create an empty system generator @@ -169,61 +175,69 @@ def test_barostat(self): assert force.getDefaultTemperature() == temperature assert force.getFrequency() == frequency - def test_create_with_template_generator(self): + @pytest.mark.parametrize("small_molecule_forcefield", [ + 'gaff-2.11', + 'openff-2.0.0', + pytest.param('espaloma-0.2.2', marks=pytest.mark.espaloma)]) + def test_create_with_template_generator(self, small_molecule_forcefield): """Test SystemGenerator creation with small molecule residue template generators""" - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: - # Create a generator that defines AMBER and small molecule force fields + # Create a generator that defines AMBER and small molecule force fields + generator = SystemGenerator(forcefields=self.amber_forcefields, + small_molecule_forcefield=small_molecule_forcefield) + + # Create a generator that also has a database cache + with tempfile.TemporaryDirectory() as tmpdirname: + cache = os.path.join(tmpdirname, 'db.json') + # Create a new database file generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield) - - # Create a generator that also has a database cache - with tempfile.TemporaryDirectory() as tmpdirname: - cache = os.path.join(tmpdirname, 'db.json') - # Create a new database file - generator = SystemGenerator(forcefields=self.amber_forcefields, - cache=cache, small_molecule_forcefield=small_molecule_forcefield) - del generator - # Reopen it (with cache still empty) - generator = SystemGenerator(forcefields=self.amber_forcefields, - cache=cache, small_molecule_forcefield=small_molecule_forcefield) - del generator - - def test_forcefield_default_kwargs(self): + cache=cache, small_molecule_forcefield=small_molecule_forcefield) + del generator + # Reopen it (with cache still empty) + generator = SystemGenerator(forcefields=self.amber_forcefields, + cache=cache, small_molecule_forcefield=small_molecule_forcefield) + del generator + + @pytest.mark.parametrize("small_molecule_forcefield", [ + 'gaff-2.11', + 'openff-2.0.0', + pytest.param('espaloma-0.2.2', marks=pytest.mark.espaloma)]) + def test_forcefield_default_kwargs(self, small_molecule_forcefield, test_systems): """Test that default forcefield kwargs work correctly""" from openmm import unit forcefield_kwargs = dict() from openmmforcefields.generators import SystemGenerator - for name, testsystem in self.testsystems.items(): + for name, testsystem in test_systems.items(): print(testsystem) molecules = testsystem['molecules'] - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: - # Create a SystemGenerator for this force field - generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield, - forcefield_kwargs=forcefield_kwargs, - molecules=molecules) - - # Parameterize molecules - for molecule in molecules: - # Create non-periodic Topology - nonperiodic_openmm_topology = molecule.to_topology().to_openmm() - system = generator.create_system(nonperiodic_openmm_topology) - forces = { force.__class__.__name__ : force for force in system.getForces() } - assert forces['NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.NoCutoff, "Expected CutoffNonPeriodic, got {forces['NonbondedForce'].getNonbondedMethod()}" - - # Create periodic Topology - box_vectors = unit.Quantity(np.diag([30, 30, 30]), unit.angstrom) - periodic_openmm_topology = copy.deepcopy(nonperiodic_openmm_topology) - periodic_openmm_topology.setPeriodicBoxVectors(box_vectors) - system = generator.create_system(periodic_openmm_topology) - forces = { force.__class__.__name__ : force for force in system.getForces() } - assert forces['NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.PME, "Expected LJPME, got {forces['NonbondedForce'].getNonbondedMethod()}" - - def test_forcefield_kwargs(self): + # Create a SystemGenerator for this force field + generator = SystemGenerator(forcefields=self.amber_forcefields, + small_molecule_forcefield=small_molecule_forcefield, + forcefield_kwargs=forcefield_kwargs, + molecules=molecules) + + # Parameterize molecules + for molecule in molecules: + # Create non-periodic Topology + nonperiodic_openmm_topology = molecule.to_topology().to_openmm() + system = generator.create_system(nonperiodic_openmm_topology) + forces = {force.__class__.__name__: force for force in system.getForces()} + assert forces['NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.NoCutoff, "Expected CutoffNonPeriodic, got {forces['NonbondedForce'].getNonbondedMethod()}" + + # Create periodic Topology + box_vectors = unit.Quantity(np.diag([30, 30, 30]), unit.angstrom) + periodic_openmm_topology = copy.deepcopy(nonperiodic_openmm_topology) + periodic_openmm_topology.setPeriodicBoxVectors(box_vectors) + system = generator.create_system(periodic_openmm_topology) + forces = {force.__class__.__name__: force for force in system.getForces()} + assert forces['NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.PME, "Expected LJPME, got {forces['NonbondedForce'].getNonbondedMethod()}" + + @pytest.mark.parametrize("small_molecule_forcefield", [ + 'gaff-2.11', + 'openff-2.0.0', + pytest.param('espaloma-0.2.2', marks=pytest.mark.espaloma)]) + def test_forcefield_kwargs(self, small_molecule_forcefield, test_systems): """Test that forcefield_kwargs and nonbonded method specifications work correctly""" from openmm import unit forcefield_kwargs = { 'hydrogenMass' : 4*unit.amu } @@ -235,156 +249,162 @@ def test_forcefield_kwargs(self): generator = SystemGenerator(forcefield_kwargs={'nonbondedMethod':PME}) assert "nonbondedMethod cannot be specified in forcefield_kwargs" in str(excinfo.value) - for name, testsystem in self.testsystems.items(): + for name, testsystem in test_systems.items(): print(testsystem) molecules = testsystem['molecules'] - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: - # Create a SystemGenerator for this force field - generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield, - forcefield_kwargs=forcefield_kwargs, - periodic_forcefield_kwargs={'nonbondedMethod':LJPME}, - nonperiodic_forcefield_kwargs={'nonbondedMethod':CutoffNonPeriodic}, - molecules=molecules) - - # Parameterize molecules - for molecule in molecules: - # Create non-periodic Topology - nonperiodic_openmm_topology = molecule.to_topology().to_openmm() - system = generator.create_system(nonperiodic_openmm_topology) - forces = { force.__class__.__name__ : force for force in system.getForces() } - assert forces['NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.CutoffNonPeriodic, "Expected CutoffNonPeriodic, got {forces['NonbondedForce'].getNonbondedMethod()}" - - # Create periodic Topology - box_vectors = unit.Quantity(np.diag([30, 30, 30]), unit.angstrom) - periodic_openmm_topology = copy.deepcopy(nonperiodic_openmm_topology) - periodic_openmm_topology.setPeriodicBoxVectors(box_vectors) - system = generator.create_system(periodic_openmm_topology) - forces = { force.__class__.__name__ : force for force in system.getForces() } - assert forces['NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.LJPME, "Expected LJPME, got {forces['NonbondedForce'].getNonbondedMethod()}" - - def test_parameterize_molecules_from_creation(self): + # Create a SystemGenerator for this force field + generator = SystemGenerator(forcefields=self.amber_forcefields, + small_molecule_forcefield=small_molecule_forcefield, + forcefield_kwargs=forcefield_kwargs, + periodic_forcefield_kwargs={'nonbondedMethod': LJPME}, + nonperiodic_forcefield_kwargs={'nonbondedMethod': CutoffNonPeriodic}, + molecules=molecules) + + # Parameterize molecules + for molecule in molecules: + # Create non-periodic Topology + nonperiodic_openmm_topology = molecule.to_topology().to_openmm() + system = generator.create_system(nonperiodic_openmm_topology) + forces = {force.__class__.__name__: force for force in system.getForces()} + assert forces[ + 'NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.CutoffNonPeriodic, "Expected CutoffNonPeriodic, got {forces['NonbondedForce'].getNonbondedMethod()}" + + # Create periodic Topology + box_vectors = unit.Quantity(np.diag([30, 30, 30]), unit.angstrom) + periodic_openmm_topology = copy.deepcopy(nonperiodic_openmm_topology) + periodic_openmm_topology.setPeriodicBoxVectors(box_vectors) + system = generator.create_system(periodic_openmm_topology) + forces = {force.__class__.__name__: force for force in system.getForces()} + assert forces[ + 'NonbondedForce'].getNonbondedMethod() == openmm.NonbondedForce.LJPME, "Expected LJPME, got {forces['NonbondedForce'].getNonbondedMethod()}" + + @pytest.mark.parametrize("small_molecule_forcefield", [ + 'gaff-2.11', + 'openff-2.0.0', + pytest.param('espaloma-0.2.2', marks=pytest.mark.espaloma)]) + def test_parameterize_molecules_from_creation(self, test_systems, small_molecule_forcefield): """Test that SystemGenerator can parameterize pre-specified molecules in vacuum""" - for name, testsystem in self.testsystems.items(): + for name, testsystem in test_systems.items(): print(testsystem) molecules = testsystem['molecules'] - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: - # Create a SystemGenerator for this force field - generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield, - molecules=molecules) - - # Parameterize molecules - for molecule in molecules: - openmm_topology = molecule.to_topology().to_openmm() - with Timer() as t1: - system = generator.create_system(openmm_topology) - assert system.getNumParticles() == molecule.n_atoms - # Molecule should now be cached - with Timer() as t2: - system = generator.create_system(openmm_topology) - assert system.getNumParticles() == molecule.n_atoms - assert (t2.interval() < t1.interval()) - - def test_parameterize_molecules_specified_during_create_system(self): + # Create a SystemGenerator for this force field + generator = SystemGenerator(forcefields=self.amber_forcefields, + small_molecule_forcefield=small_molecule_forcefield, + molecules=molecules) + + # Parameterize molecules + for molecule in molecules: + openmm_topology = molecule.to_topology().to_openmm() + with Timer() as t1: + system = generator.create_system(openmm_topology) + assert system.getNumParticles() == molecule.n_atoms + # Molecule should now be cached + with Timer() as t2: + system = generator.create_system(openmm_topology) + assert system.getNumParticles() == molecule.n_atoms + assert (t2.interval() < t1.interval()) + + @pytest.mark.parametrize("small_molecule_forcefield", [ + 'gaff-2.11', + 'openff-2.0.0', + pytest.param('espaloma-0.2.2', marks=pytest.mark.espaloma)]) + def test_parameterize_molecules_specified_during_create_system(self, test_systems, small_molecule_forcefield): """Test that SystemGenerator can parameterize molecules specified during create_system""" - for name, testsystem in self.testsystems.items(): + for name, testsystem in test_systems.items(): molecules = testsystem['molecules'] - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: - # Create a SystemGenerator for this force field - generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield) - - # Parameterize molecules - for molecule in molecules: - openmm_topology = molecule.to_topology().to_openmm() - # Specify molecules during system creation - system = generator.create_system(openmm_topology, molecules=molecules) - - def test_add_molecules(self): - """Test that Molecules can be added to SystemGenerator later""" - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: # Create a SystemGenerator for this force field generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield) + small_molecule_forcefield=small_molecule_forcefield) + + # Parameterize molecules + for molecule in molecules: + openmm_topology = molecule.to_topology().to_openmm() + # Specify molecules during system creation + system = generator.create_system(openmm_topology, molecules=molecules) + + @pytest.mark.parametrize("small_molecule_forcefield", [ + 'gaff-2.11', + 'openff-2.0.0', + pytest.param('espaloma-0.2.2', marks=pytest.mark.espaloma)]) + def test_add_molecules(self, test_systems, small_molecule_forcefield): + """Test that Molecules can be added to SystemGenerator later""" + # Create a SystemGenerator for this force field + generator = SystemGenerator(forcefields=self.amber_forcefields, + small_molecule_forcefield=small_molecule_forcefield) + + # Add molecules for each test system separately + for name, testsystem in test_systems.items(): + molecules = testsystem['molecules'] + # Add molecules + generator.add_molecules(molecules) + + # Parameterize molecules + for molecule in molecules: + openmm_topology = molecule.to_topology().to_openmm() + with Timer() as t1: + system = generator.create_system(openmm_topology) + assert system.getNumParticles() == molecule.n_atoms + # Molecule should now be cached + with Timer() as t2: + system = generator.create_system(openmm_topology) + assert system.getNumParticles() == molecule.n_atoms + assert (t2.interval() < t1.interval()) + + @pytest.mark.parametrize("small_molecule_forcefield", [ + 'gaff-2.11', + 'openff-2.0.0', + pytest.param('espaloma-0.2.2', marks=pytest.mark.espaloma)]) + def test_cache(self, test_systems, small_molecule_forcefield): + """Test that SystemGenerator correctly manages a cache""" + timing = dict() # timing[(small_molecule_forcefield, smiles)] is the time (in seconds) to parameterize molecule the first time + with tempfile.TemporaryDirectory() as tmpdirname: + # Create a single shared cache for all force fields + cache = os.path.join(tmpdirname, 'db.json') + # Test that we can parameterize all molecules for all test systems + # Create a SystemGenerator + generator = SystemGenerator(forcefields=self.amber_forcefields, + small_molecule_forcefield=small_molecule_forcefield, + cache=cache) # Add molecules for each test system separately - for name, testsystem in self.testsystems.items(): + for name, testsystem in test_systems.items(): molecules = testsystem['molecules'] - # Add molecules generator.add_molecules(molecules) # Parameterize molecules for molecule in molecules: openmm_topology = molecule.to_topology().to_openmm() - with Timer() as t1: + with Timer() as timer: system = generator.create_system(openmm_topology) assert system.getNumParticles() == molecule.n_atoms - # Molecule should now be cached - with Timer() as t2: - system = generator.create_system(openmm_topology) - assert system.getNumParticles() == molecule.n_atoms - assert (t2.interval() < t1.interval()) - - def test_cache(self): - """Test that SystemGenerator correctly manages a cache""" - timing = dict() # timing[(small_molecule_forcefield, smiles)] is the time (in seconds) to parameterize molecule the first time - with tempfile.TemporaryDirectory() as tmpdirname: - # Create a single shared cache for all force fields - cache = os.path.join(tmpdirname, 'db.json') - # Test that we can parameterize all molecules for all test systems - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: - # Create a SystemGenerator - generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield, - cache=cache) - # Add molecules for each test system separately - for name, testsystem in self.testsystems.items(): - molecules = testsystem['molecules'] - # Add molecules - generator.add_molecules(molecules) - - # Parameterize molecules - for molecule in molecules: - openmm_topology = molecule.to_topology().to_openmm() - with Timer() as timer: - system = generator.create_system(openmm_topology) - assert system.getNumParticles() == molecule.n_atoms - # Record time - timing[(small_molecule_forcefield, molecule.to_smiles())] = timer.interval() + # Record time + timing[(small_molecule_forcefield, molecule.to_smiles())] = timer.interval() # Molecules should now be cached; test timing is faster the second time # Test that we can parameterize all molecules for all test systems - SMALL_MOLECULE_FORCEFIELDS = SystemGenerator.SMALL_MOLECULE_FORCEFIELDS if not CI else ['gaff-2.11', 'openff-2.0.0', 'espaloma-0.2.2'] - for small_molecule_forcefield in SMALL_MOLECULE_FORCEFIELDS: - # Create a SystemGenerator - generator = SystemGenerator(forcefields=self.amber_forcefields, - small_molecule_forcefield=small_molecule_forcefield, - cache=cache) - # Add molecules for each test system separately - for name, testsystem in self.testsystems.items(): - molecules = testsystem['molecules'] - # We don't need to add molecules that are already defined in the cache - - # Parameterize molecules - for molecule in molecules: - openmm_topology = molecule.to_topology().to_openmm() - with Timer() as timer: - system = generator.create_system(openmm_topology) - assert system.getNumParticles() == molecule.n_atoms - - def test_complex(self): + # Create a SystemGenerator + generator = SystemGenerator(forcefields=self.amber_forcefields, + small_molecule_forcefield=small_molecule_forcefield, + cache=cache) + # Add molecules for each test system separately + for name, testsystem in test_systems.items(): + molecules = testsystem['molecules'] + # We don't need to add molecules that are already defined in the cache + + # Parameterize molecules + for molecule in molecules: + openmm_topology = molecule.to_topology().to_openmm() + with Timer() as timer: + system = generator.create_system(openmm_topology) + assert system.getNumParticles() == molecule.n_atoms + + def test_complex(self, test_systems): """Test parameterizing a protein:ligand complex in vacuum""" - for name, testsystem in self.testsystems.items(): + for name, testsystem in test_systems.items(): from openmm import unit print(f'Testing parameterization of {name} in vacuum') diff --git a/openmmforcefields/tests/test_template_generators.py b/openmmforcefields/tests/test_template_generators.py index 07d130d3..62cb4264 100644 --- a/openmmforcefields/tests/test_template_generators.py +++ b/openmmforcefields/tests/test_template_generators.py @@ -3,6 +3,7 @@ import os import tempfile import unittest +import pytest import numpy as np import openmm @@ -534,6 +535,11 @@ def test_parameterize(self): for small_molecule_forcefield in self.TEMPLATE_GENERATOR.INSTALLED_FORCEFIELDS: if "ff14sb" in small_molecule_forcefield: continue + if "tip" in small_molecule_forcefield: + continue + if "opc" in small_molecule_forcefield: + continue + print(f'Testing {small_molecule_forcefield}') # Create a generator that knows about a few molecules # TODO: Should the generator also load the appropriate force field files into the ForceField object? @@ -675,7 +681,7 @@ def write_xml(filename, system): print(f'{key:24} {(template_component_energy/unit.kilocalories_per_mole):20.3f} {(reference_component_energy/unit.kilocalories_per_mole):20.3f} kcal/mol') print(f'{"TOTAL":24} {(template_energy["total"]/unit.kilocalories_per_mole):20.3f} {(reference_energy["total"]/unit.kilocalories_per_mole):20.3f} kcal/mol') write_xml('reference_system.xml', reference_system) - write_xml('template_system.xml', template_system) + write_xml('template_system.xml', template_system) # What's this? This variable does not exist raise Exception(f'Energy deviation for {molecule.to_smiles()} ({delta/unit.kilocalories_per_mole} kcal/mol) exceeds threshold ({ENERGY_DEVIATION_TOLERANCE})') # Compare forces @@ -780,6 +786,11 @@ def test_energies(self): for small_molecule_forcefield in SMIRNOFFTemplateGenerator.INSTALLED_FORCEFIELDS: if "ff14sb" in small_molecule_forcefield: continue + if "tip" in small_molecule_forcefield: + continue + if "opc" in small_molecule_forcefield: + continue + print(f'Testing energies for {small_molecule_forcefield}...') # Create a generator that knows about a few molecules # TODO: Should the generator also load the appropriate force field files into the ForceField object? @@ -817,6 +828,11 @@ def test_partial_charges_are_none(self): for small_molecule_forcefield in SMIRNOFFTemplateGenerator.INSTALLED_FORCEFIELDS: if "ff14sb" in small_molecule_forcefield: continue + if "tip" in small_molecule_forcefield: + continue + if "opc" in small_molecule_forcefield: + continue + print(f'Testing energies for {small_molecule_forcefield}...') # Create a generator that knows about a few molecules # TODO: Should the generator also load the appropriate force field files into the ForceField object? @@ -831,6 +847,8 @@ def test_partial_charges_are_none(self): def test_version(self): """Test version""" + # This test does not appear to test the version of anything in particular, but it fails sometimes + # because old versions of the toolkit can't bring in new versions of some water models for forcefield in SMIRNOFFTemplateGenerator.INSTALLED_FORCEFIELDS: generator = SMIRNOFFTemplateGenerator(forcefield=forcefield) assert generator.forcefield == forcefield @@ -838,6 +856,7 @@ def test_version(self): assert os.path.exists(generator.smirnoff_filename) +@pytest.mark.espaloma class TestEspalomaTemplateGenerator(TestGAFFTemplateGenerator): TEMPLATE_GENERATOR = EspalomaTemplateGenerator